create-kubojs 0.2.0 → 0.2.1

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,40 @@ 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 supportsTrueColor = process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0" && (process.env.FORCE_COLOR !== void 0 || process.stdout.isTTY === true);
34
+ function createColor(hex, fallback) {
35
+ if (!supportsTrueColor) return fallback;
36
+ const red = Number.parseInt(hex.slice(1, 3), 16);
37
+ const green = Number.parseInt(hex.slice(3, 5), 16);
38
+ const blue = Number.parseInt(hex.slice(5, 7), 16);
39
+ return (text) => `\u001B[38;2;${red};${green};${blue}m${text}\u001B[39m`;
40
+ }
41
+ const cliColors = {
42
+ signal: createColor(KUBO_COLORS.signal, pc.yellow),
43
+ bright: createColor(KUBO_COLORS.bright, pc.yellow),
44
+ orange: createColor(KUBO_COLORS.orange, pc.yellow),
45
+ cream: createColor(KUBO_COLORS.cream, pc.white)
46
+ };
47
+ //#endregion
26
48
  //#region src/utils/get-package-manager.ts
27
49
  const getUserPkgManager = () => {
28
50
  const userAgent = process.env.npm_config_user_agent;
@@ -42,7 +64,7 @@ const DEFAULT_CONFIG_BASE = {
42
64
  database: "sqlite",
43
65
  orm: "drizzle",
44
66
  auth: "better-auth",
45
- payments: "none",
67
+ payments: [],
46
68
  observability: ["getmonitor"],
47
69
  communication: "none",
48
70
  addons: ["turborepo"],
@@ -63,6 +85,8 @@ function getDefaultConfig() {
63
85
  projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
64
86
  packageManager: getUserPkgManager(),
65
87
  frontend: [...DEFAULT_CONFIG_BASE.frontend],
88
+ payments: [...DEFAULT_CONFIG_BASE.payments],
89
+ observability: [...DEFAULT_CONFIG_BASE.observability],
66
90
  addons: [...DEFAULT_CONFIG_BASE.addons],
67
91
  examples: [...DEFAULT_CONFIG_BASE.examples],
68
92
  testing: [...DEFAULT_CONFIG_BASE.testing]
@@ -82,17 +106,12 @@ const ADDON_COMPATIBILITY = {
82
106
  husky: [],
83
107
  lefthook: [],
84
108
  turborepo: [],
85
- nx: [],
86
109
  "vite-plus": [],
87
- starlight: [],
88
- ultracite: [],
89
110
  mcp: [],
90
111
  oxlint: [],
91
- fumadocs: [],
92
112
  opentui: [],
93
- wxt: [],
94
113
  skills: [],
95
- evlog: [],
114
+ "s3-storage": [],
96
115
  none: []
97
116
  };
98
117
  //#endregion
@@ -429,24 +448,26 @@ const TITLE_TEXT = `
429
448
  ██║ ██╗╚██████╔╝██████╔╝╚██████╔╝
430
449
  ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
431
450
  `;
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
- };
451
+ const KUBO_TITLE_COLORS = [
452
+ "#D6A72B",
453
+ "#F5D76E",
454
+ "#E0B43E",
455
+ "#E08A2E",
456
+ "#E0B43E",
457
+ "#E8A43A",
458
+ "#E08A2E",
459
+ "#E08A2E",
460
+ "#C49314",
461
+ "#E8C978",
462
+ "#E5D3A5"
463
+ ];
445
464
  const renderTitle = () => {
446
465
  const terminalWidth = process.stdout.columns || 80;
447
466
  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));
467
+ if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) {
468
+ const simplifiedTitle = `Kubo`;
469
+ console.log(gradient([...KUBO_TITLE_COLORS]).multiline(simplifiedTitle));
470
+ } else console.log(gradient([...KUBO_TITLE_COLORS]).multiline(TITLE_TEXT));
450
471
  };
451
472
  //#endregion
452
473
  //#region src/commands/history.ts
@@ -493,10 +514,10 @@ async function historyHandler(input) {
493
514
  return;
494
515
  }
495
516
  renderTitle();
496
- intro(pc.magenta(`Project History (${entries.length} entries)`));
517
+ intro(cliColors.signal(`Project History (${entries.length} entries)`));
497
518
  for (const [index, entry] of entries.entries()) {
498
519
  const num = pc.dim(`${index + 1}.`);
499
- const name = pc.cyan(pc.bold(entry.projectName));
520
+ const name = cliColors.bright(pc.bold(entry.projectName));
500
521
  const stack = pc.dim(formatStackSummary(entry));
501
522
  log.message(`${num} ${name}`);
502
523
  log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
@@ -529,7 +550,7 @@ async function openExternalUrl(url, successMessage) {
529
550
  if ((await Result.tryPromise({
530
551
  try: () => openUrl(url),
531
552
  catch: () => null
532
- })).isOk()) log.success(pc.blue(successMessage));
553
+ })).isOk()) log.success(cliColors.signal(successMessage));
533
554
  else log.message(`Please visit ${url}`);
534
555
  }
535
556
  async function openDocsCommand() {
@@ -557,17 +578,9 @@ const WEB_FRAMEWORKS = [
557
578
  ];
558
579
  //#endregion
559
580
  //#region src/utils/compatibility-rules.ts
560
- const TASK_RUNNER_ADDONS$1 = [
561
- "turborepo",
562
- "nx",
563
- "vite-plus"
564
- ];
581
+ const TASK_RUNNER_ADDONS$1 = ["turborepo", "vite-plus"];
565
582
  /** Mutually exclusive code-quality linters (one slot). */
566
- const LINTER_ADDONS = [
567
- "biome",
568
- "oxlint",
569
- "ultracite"
570
- ];
583
+ const LINTER_ADDONS = ["biome", "oxlint"];
571
584
  const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
572
585
  function isLinterAddon(addon) {
573
586
  return LINTER_ADDONS.includes(addon);
@@ -639,23 +652,6 @@ const FULLSTACK_FRONTENDS$1 = [
639
652
  "svelte",
640
653
  "astro"
641
654
  ];
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
655
  function validateSelfBackendCompatibility(providedFlags, options, config) {
660
656
  const backend = config.backend || options.backend;
661
657
  const frontends = config.frontend || options.frontend || [];
@@ -701,7 +697,7 @@ function isFrontendAllowedWithBackend(frontend, backend, auth) {
701
697
  function supportsConvexBetterAuth(frontends = []) {
702
698
  return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.includes(frontend));
703
699
  }
704
- function allowedApisForFrontends(frontends = []) {
700
+ function allowedApisForFrontends(frontends = [], backend) {
705
701
  const includesNuxt = frontends.includes("nuxt");
706
702
  const includesSvelte = frontends.includes("svelte");
707
703
  const includesSolid = frontends.includes("solid");
@@ -711,9 +707,17 @@ function allowedApisForFrontends(frontends = []) {
711
707
  "orpc",
712
708
  "none"
713
709
  ];
714
- if (includesNuxt || includesSvelte || includesSolid || includesAstro) return ["orpc", "none"];
710
+ if (backend === "hono") base.splice(2, 0, "orval");
711
+ if (includesNuxt || includesSvelte || includesSolid || includesAstro) return backend === "hono" ? [
712
+ "orpc",
713
+ "orval",
714
+ "none"
715
+ ] : ["orpc", "none"];
715
716
  return base;
716
717
  }
718
+ function isApiCompatibleWithBackend(api, backend) {
719
+ return api !== "orval" || backend === "hono";
720
+ }
717
721
  function isExampleTodoAllowed(backend, database, api) {
718
722
  if (backend === "convex") return true;
719
723
  if (database === "none" || api === "none") return false;
@@ -773,10 +777,6 @@ function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backe
773
777
  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
778
  }
775
779
  function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
776
- if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
777
- isCompatible: false,
778
- reason: evlogCompatibilityMessage
779
- };
780
780
  if (STATIC_DESKTOP_ADDONS.includes(addon) && auth === "clerk" && frontend.includes("react-router")) return {
781
781
  isCompatible: false,
782
782
  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 +789,13 @@ function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
789
789
  isCompatible: false,
790
790
  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
791
  };
792
+ if (!(addon in ADDON_COMPATIBILITY)) return {
793
+ isCompatible: false,
794
+ reason: `${addon} is not a supported addon`
795
+ };
792
796
  const compatibleFrontends = ADDON_COMPATIBILITY[addon];
793
797
  if (compatibleFrontends.length > 0) {
794
- if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
798
+ if (!frontend.some((f) => compatibleFrontends.some((compatibleFrontend) => compatibleFrontend === f))) return {
795
799
  isCompatible: false,
796
800
  reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
797
801
  };
@@ -808,8 +812,8 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
808
812
  });
809
813
  }
810
814
  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.");
815
+ 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.");
816
+ 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
817
  for (const addon of addons) {
814
818
  if (addon === "none") continue;
815
819
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -822,21 +826,29 @@ function validateAddonsAgainstConfig(addons = [], config) {
822
826
  }
823
827
  function validateCommunicationCompatibility(communication, backend) {
824
828
  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'.`);
829
+ 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'.`);
830
+ return Result.ok(void 0);
831
+ }
832
+ function validateAraraRuntimeCompatibility(communication, backend, runtime, serverDeploy) {
833
+ if (communication !== "arara" || backend === "convex") return Result.ok(void 0);
834
+ 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
835
  return Result.ok(void 0);
827
836
  }
828
837
  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.");
838
+ if (!payments || payments.length === 0) return Result.ok(void 0);
839
+ for (const provider of payments) {
840
+ const issue = getPaymentCompatibilityIssue({
841
+ provider,
842
+ backend,
843
+ frontends,
844
+ database,
845
+ orm
846
+ });
847
+ const label = provider === "stripe" ? "Stripe" : "AbacatePay";
848
+ if (issue === "convex-unsupported") return validationErr$1(`${label} payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.`);
849
+ 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.`);
850
+ 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.`);
851
+ 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
852
  }
841
853
  return Result.ok(void 0);
842
854
  }
@@ -871,12 +883,12 @@ function isGoBack(value) {
871
883
  * These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
872
884
  */
873
885
  const unicode = process.platform !== "win32";
874
- const S_STEP_ACTIVE = unicode ? "◆" : "*";
886
+ const S_STEP_ACTIVE$1 = unicode ? "◆" : "*";
875
887
  const S_STEP_CANCEL = unicode ? "■" : "x";
876
888
  const S_STEP_ERROR = unicode ? "▲" : "x";
877
889
  const S_STEP_SUBMIT = unicode ? "◇" : "o";
878
- const S_BAR = unicode ? "│" : "|";
879
- const S_BAR_END = unicode ? "└" : "—";
890
+ const S_BAR$1 = unicode ? "│" : "|";
891
+ const S_BAR_END$1 = unicode ? "└" : "—";
880
892
  const S_RADIO_ACTIVE = unicode ? "●" : ">";
881
893
  const S_RADIO_INACTIVE = unicode ? "○" : " ";
882
894
  const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
@@ -885,10 +897,10 @@ const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
885
897
  function symbol(state) {
886
898
  switch (state) {
887
899
  case "initial":
888
- case "active": return pc.cyan(S_STEP_ACTIVE);
900
+ case "active": return cliColors.orange(S_STEP_ACTIVE$1);
889
901
  case "cancel": return pc.red(S_STEP_CANCEL);
890
902
  case "error": return pc.yellow(S_STEP_ERROR);
891
- case "submit": return pc.green(S_STEP_SUBMIT);
903
+ case "submit": return cliColors.signal(S_STEP_SUBMIT);
892
904
  }
893
905
  }
894
906
  const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
@@ -923,7 +935,7 @@ async function navigableSelect(opts) {
923
935
  switch (state) {
924
936
  case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
925
937
  case "selected": return `${pc.dim(label)}`;
926
- case "active": return `${pc.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
938
+ case "active": return `${cliColors.bright(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
927
939
  case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
928
940
  default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
929
941
  }
@@ -932,14 +944,14 @@ async function navigableSelect(opts) {
932
944
  options: opts.options,
933
945
  initialValue: opts.initialValue,
934
946
  render() {
935
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
947
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
936
948
  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)}`;
949
+ case "submit": return `${title}${pc.gray(S_BAR$1)} ${opt(this.options[this.cursor], "selected")}`;
950
+ case "cancel": return `${title}${pc.gray(S_BAR$1)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR$1)}`;
939
951
  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`;
952
+ const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${cliColors.signal(S_BAR$1)} `);
953
+ const hint = `\n${pc.gray(S_BAR$1)} ${getHint()}`;
954
+ return `${title}${cliColors.signal(S_BAR$1)} ${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
943
955
  }
944
956
  }
945
957
  }
@@ -950,10 +962,10 @@ async function navigableMultiselect(opts) {
950
962
  const opt = (option, state) => {
951
963
  const label = option.label ?? String(option.value);
952
964
  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})`)}` : ""}`;
965
+ if (state === "active") return `${cliColors.orange(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
966
+ if (state === "selected") return `${cliColors.bright(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
955
967
  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})`)}` : ""}`;
968
+ if (state === "active-selected") return `${cliColors.bright(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
957
969
  if (state === "submitted") return `${pc.dim(label)}`;
958
970
  return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
959
971
  };
@@ -966,7 +978,7 @@ async function navigableMultiselect(opts) {
966
978
  return normalizeValidationMessage(opts.validate?.(selected));
967
979
  },
968
980
  render() {
969
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
981
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
970
982
  const value = this.value ?? [];
971
983
  const styleOption = (option, active) => {
972
984
  if (option.disabled) return opt(option, "disabled");
@@ -978,21 +990,21 @@ async function navigableMultiselect(opts) {
978
990
  switch (this.state) {
979
991
  case "submit": {
980
992
  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}`;
993
+ return `${title}${pc.gray(S_BAR$1)} ${submitText}`;
982
994
  }
983
995
  case "cancel": {
984
996
  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)}`;
997
+ return `${title}${pc.gray(S_BAR$1)} ${label}\n${pc.gray(S_BAR$1)}`;
986
998
  }
987
999
  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`;
1000
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END$1)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1001
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR$1)} `);
1002
+ return `${title}${pc.yellow(S_BAR$1)} ${optionsText}\n${footer}\n`;
991
1003
  }
992
1004
  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`;
1005
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${cliColors.signal(S_BAR$1)} `);
1006
+ const hint = `\n${pc.gray(S_BAR$1)} ${getMultiHint()}`;
1007
+ return `${title}${cliColors.signal(S_BAR$1)} ${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
996
1008
  }
997
1009
  }
998
1010
  }
@@ -1006,14 +1018,14 @@ async function navigableConfirm(opts) {
1006
1018
  inactive,
1007
1019
  initialValue: opts.initialValue ?? true,
1008
1020
  render() {
1009
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
1021
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
1010
1022
  const value = this.value ? active : inactive;
1011
1023
  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)}`;
1024
+ case "submit": return `${title}${pc.gray(S_BAR$1)} ${pc.dim(value)}`;
1025
+ case "cancel": return `${title}${pc.gray(S_BAR$1)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR$1)}`;
1014
1026
  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`;
1027
+ const hint = `\n${pc.gray(S_BAR$1)} ${getHint()}`;
1028
+ 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
1029
  }
1018
1030
  }
1019
1031
  }
@@ -1026,16 +1038,16 @@ async function navigableGroupMultiselect(opts) {
1026
1038
  const isItem = typeof option.group === "string";
1027
1039
  const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
1028
1040
  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)}`;
1041
+ const prefix = isItem ? `${isLast ? S_BAR_END$1 : S_BAR$1} ` : "";
1042
+ if (state === "active") return `${pc.dim(prefix)}${cliColors.orange(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1043
+ if (state === "group-active") return `${prefix}${cliColors.orange(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
1044
+ if (state === "group-active-selected") return `${prefix}${cliColors.bright(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
1033
1045
  if (state === "selected") {
1034
- const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
1046
+ const selectedCheckbox = isItem ? cliColors.bright(S_CHECKBOX_SELECTED) : "";
1035
1047
  return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1036
1048
  }
1037
1049
  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})`)}` : ""}`;
1050
+ if (state === "active-selected") return `${pc.dim(prefix)}${cliColors.bright(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1039
1051
  if (state === "submitted") return `${pc.dim(label)}`;
1040
1052
  const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
1041
1053
  return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
@@ -1050,20 +1062,20 @@ async function navigableGroupMultiselect(opts) {
1050
1062
  return normalizeValidationMessage(opts.validate?.(selected));
1051
1063
  },
1052
1064
  render() {
1053
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
1065
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
1054
1066
  const value = this.value ?? [];
1055
1067
  switch (this.state) {
1056
1068
  case "submit": {
1057
1069
  const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
1058
1070
  const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
1059
- return `${title}${pc.gray(S_BAR)}${optionsText}`;
1071
+ return `${title}${pc.gray(S_BAR$1)}${optionsText}`;
1060
1072
  }
1061
1073
  case "cancel": {
1062
1074
  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)}` : ""}`;
1075
+ return `${title}${pc.gray(S_BAR$1)} ${label.trim() ? `${label}\n${pc.gray(S_BAR$1)}` : ""}`;
1064
1076
  }
1065
1077
  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");
1078
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END$1)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1067
1079
  const optionsText = this.options.map((option, i, options) => {
1068
1080
  const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1069
1081
  const active = i === this.cursor;
@@ -1071,8 +1083,8 @@ async function navigableGroupMultiselect(opts) {
1071
1083
  if (active && selected) return opt(option, "active-selected", options);
1072
1084
  if (selected) return opt(option, "selected", options);
1073
1085
  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`;
1086
+ }).join(`\n${pc.yellow(S_BAR$1)} `);
1087
+ return `${title}${pc.yellow(S_BAR$1)} ${optionsText}\n${footer}\n`;
1076
1088
  }
1077
1089
  default: {
1078
1090
  const optionsText = this.options.map((option, i, options) => {
@@ -1085,10 +1097,10 @@ async function navigableGroupMultiselect(opts) {
1085
1097
  else if (selected) optionText = opt(option, "selected", options);
1086
1098
  else optionText = opt(option, active ? "active" : "inactive", options);
1087
1099
  return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
1088
- }).join(`\n${pc.cyan(S_BAR)}`);
1100
+ }).join(`\n${cliColors.signal(S_BAR$1)}`);
1089
1101
  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`;
1102
+ const hint = `\n${pc.gray(S_BAR$1)} ${getMultiHint()}`;
1103
+ return `${title}${cliColors.signal(S_BAR$1)}${optionsPrefix}${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
1092
1104
  }
1093
1105
  }
1094
1106
  }
@@ -1108,10 +1120,6 @@ function getAddonDisplay(addon) {
1108
1120
  label = "Turborepo";
1109
1121
  hint = "High-performance build system";
1110
1122
  break;
1111
- case "nx":
1112
- label = "Nx";
1113
- hint = "Smart monorepo orchestration and task graph";
1114
- break;
1115
1123
  case "vite-plus":
1116
1124
  label = "Vite+";
1117
1125
  hint = "Unified Vite toolchain and workspace task runner";
@@ -1136,10 +1144,6 @@ function getAddonDisplay(addon) {
1136
1144
  label = "Oxlint";
1137
1145
  hint = "Oxlint + Oxfmt (linting & formatting)";
1138
1146
  break;
1139
- case "ultracite":
1140
- label = "Ultracite";
1141
- hint = "Zero-config preset for Biome or Oxlint with AI integration";
1142
- break;
1143
1147
  case "lefthook":
1144
1148
  label = "Lefthook";
1145
1149
  hint = "Fast and powerful Git hooks manager";
@@ -1148,22 +1152,10 @@ function getAddonDisplay(addon) {
1148
1152
  label = "Husky";
1149
1153
  hint = "Modern native Git hooks made easy";
1150
1154
  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
1155
  case "opentui":
1160
1156
  label = "OpenTUI";
1161
1157
  hint = "Build terminal user interfaces";
1162
1158
  break;
1163
- case "wxt":
1164
- label = "WXT";
1165
- hint = "Build browser extensions";
1166
- break;
1167
1159
  case "skills":
1168
1160
  label = "Skills";
1169
1161
  hint = "AI coding agent skills for your stack";
@@ -1172,9 +1164,9 @@ function getAddonDisplay(addon) {
1172
1164
  label = "MCP";
1173
1165
  hint = "Install MCP servers, including kubojs, via add-mcp";
1174
1166
  break;
1175
- case "evlog":
1176
- label = "evlog";
1177
- hint = "Request logging with Better Auth context and AI SDK telemetry";
1167
+ case "s3-storage":
1168
+ label = "S3-compatible Storage";
1169
+ hint = "Add object storage for AWS S3, Cloudflare R2, MinIO, or Backblaze";
1178
1170
  break;
1179
1171
  default:
1180
1172
  label = addon;
@@ -1186,27 +1178,20 @@ function getAddonDisplay(addon) {
1186
1178
  };
1187
1179
  }
1188
1180
  const ADDON_GROUPS = {
1189
- "Monorepo & Tasks": [
1190
- "turborepo",
1191
- "nx",
1192
- "vite-plus"
1193
- ],
1181
+ "Monorepo & Tasks": ["turborepo", "vite-plus"],
1194
1182
  "Code Quality": [
1195
1183
  "biome",
1196
1184
  "oxlint",
1197
- "ultracite",
1198
1185
  "husky",
1199
1186
  "lefthook"
1200
1187
  ],
1201
- Documentation: ["starlight", "fumadocs"],
1202
1188
  "Platform Extensions": [
1203
1189
  "pwa",
1204
1190
  "tauri",
1205
1191
  "electrobun",
1206
1192
  "opentui",
1207
- "wxt"
1193
+ "s3-storage"
1208
1194
  ],
1209
- Observability: ["evlog"],
1210
1195
  "AI & Agent Tools": ["skills", "mcp"]
1211
1196
  };
1212
1197
  function createGroupedOptions() {
@@ -1231,16 +1216,8 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1231
1216
  });
1232
1217
  }
1233
1218
  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.";
1219
+ if ((selected?.filter((addon) => ["turborepo", "vite-plus"].includes(addon)) ?? []).length > 1) return "Choose Turborepo or Vite+ as your task runner, not both.";
1220
+ if ((selected?.filter((addon) => ["biome", "oxlint"].includes(addon)) ?? []).length > 1) return "Choose Biome or Oxlint as your code-quality tool, not both.";
1244
1221
  }
1245
1222
  async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1246
1223
  if (addons !== void 0) return addons;
@@ -1265,7 +1242,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previo
1265
1242
  required: false,
1266
1243
  validate: validateAddonSelection
1267
1244
  });
1268
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1245
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1269
1246
  return response;
1270
1247
  }
1271
1248
  async function getAddonsToAdd(config) {
@@ -1288,7 +1265,7 @@ async function getAddonsToAdd(config) {
1288
1265
  required: false,
1289
1266
  validate: validateAddonSelection
1290
1267
  });
1291
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1268
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1292
1269
  return response;
1293
1270
  }
1294
1271
  //#endregion
@@ -1364,515 +1341,6 @@ const addPackageDependency = async (opts) => {
1364
1341
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1365
1342
  };
1366
1343
  //#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
1344
  //#region src/prompts/navigable-group.ts
1877
1345
  /**
1878
1346
  * Navigable group - a group of prompts that allows going back
@@ -1915,7 +1383,7 @@ async function navigableGroup(prompts, opts) {
1915
1383
  goingBack = false;
1916
1384
  continue;
1917
1385
  }
1918
- if (isCancel$1(result)) {
1386
+ if (isCancel$2(result)) {
1919
1387
  if (typeof opts?.onCancel === "function") {
1920
1388
  results[name] = "canceled";
1921
1389
  opts.onCancel({ results });
@@ -2036,206 +1504,6 @@ function getPackageRunnerPrefix(packageManager) {
2036
1504
  }
2037
1505
  }
2038
1506
  //#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
1507
  //#region src/helpers/addons/mcp-setup.ts
2240
1508
  const MCP_AGENTS = [
2241
1509
  {
@@ -2310,7 +1578,7 @@ const MCP_AGENTS = [
2310
1578
  }
2311
1579
  ];
2312
1580
  const DEFAULT_SCOPE$1 = "project";
2313
- const DEFAULT_AGENTS$2 = [
1581
+ const DEFAULT_AGENTS$1 = [
2314
1582
  "cursor",
2315
1583
  "claude-code",
2316
1584
  "vscode"
@@ -2434,7 +1702,6 @@ function getAllMcpServers(config) {
2434
1702
  function getRecommendedMcpServers(config, scope) {
2435
1703
  const serversByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
2436
1704
  const recommendedServerKeys = ["kubojs", "context7"];
2437
- if (scope === "project" && config.addons.includes("nx")) recommendedServerKeys.push("nx");
2438
1705
  if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") recommendedServerKeys.push("cloudflare-docs");
2439
1706
  if (config.backend === "convex") recommendedServerKeys.push("convex");
2440
1707
  if (hasReactBasedFrontend$1(config.frontend)) recommendedServerKeys.push("shadcn");
@@ -2473,7 +1740,7 @@ async function setupMcp(config) {
2473
1740
  selectedServerKeys = configuredServerKeys?.filter((k) => availableServerKeys.has(k)) ?? serverOptions;
2474
1741
  if (selectedServerKeys.length === 0) return Result.ok(void 0);
2475
1742
  const agentOptions = filterAgentsForScope(scope);
2476
- const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOptions.some((o) => o.value === a)));
1743
+ const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$1.filter((a) => agentOptions.some((o) => o.value === a)));
2477
1744
  selectedAgents = configuredAgents?.filter((a) => agentOptions.some((o) => o.value === a)) ?? defaultAgents;
2478
1745
  if (selectedAgents.length === 0) return Result.ok(void 0);
2479
1746
  } else {
@@ -2516,7 +1783,7 @@ async function setupMcp(config) {
2516
1783
  if (currentServers !== void 0 && currentServers.length === 0) return [];
2517
1784
  const agentOpts = filterAgentsForScope(currentScope);
2518
1785
  if (agentOpts.length === 0) return [];
2519
- const defaults = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOpts.some((o) => o.value === a)));
1786
+ const defaults = uniqueValues$1(DEFAULT_AGENTS$1.filter((a) => agentOpts.some((o) => o.value === a)));
2520
1787
  if (configuredAgents !== void 0) return configuredAgents.filter((a) => agentOpts.some((o) => o.value === a));
2521
1788
  return navigableMultiselect({
2522
1789
  message: "Select agents to install MCP servers to",
@@ -2762,7 +2029,7 @@ const AVAILABLE_AGENTS = [
2762
2029
  }
2763
2030
  ];
2764
2031
  const DEFAULT_SCOPE = "project";
2765
- const DEFAULT_AGENTS$1 = [
2032
+ const DEFAULT_AGENTS = [
2766
2033
  "cursor",
2767
2034
  "claude-code",
2768
2035
  "github-copilot"
@@ -2797,8 +2064,6 @@ function getRecommendedSourceKeys(config) {
2797
2064
  if (backend === "elysia") sources.push("elysiajs/skills");
2798
2065
  if (backend === "convex") sources.push("waynesutton/convexskills");
2799
2066
  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
2067
  return sources;
2803
2068
  }
2804
2069
  const CURATED_SKILLS_BY_SOURCE = {
@@ -2910,7 +2175,7 @@ async function setupSkills(config) {
2910
2175
  scope = configuredScope ?? DEFAULT_SCOPE;
2911
2176
  selectedSkills = configuredSelections !== void 0 ? configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`)) : allSkillValues;
2912
2177
  if (selectedSkills.length === 0) return Result.ok(void 0);
2913
- selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS$1];
2178
+ selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS];
2914
2179
  if (selectedAgents.length === 0) return Result.ok(void 0);
2915
2180
  } else {
2916
2181
  const results = await navigableGroup({
@@ -2947,7 +2212,7 @@ async function setupSkills(config) {
2947
2212
  message: "Select agents to install skills to",
2948
2213
  options: AVAILABLE_AGENTS,
2949
2214
  required: false,
2950
- initialValues: [...DEFAULT_AGENTS$1]
2215
+ initialValues: [...DEFAULT_AGENTS]
2951
2216
  });
2952
2217
  }
2953
2218
  });
@@ -2996,44 +2261,6 @@ async function setupSkills(config) {
2996
2261
  return Result.ok(void 0);
2997
2262
  }
2998
2263
  //#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
2264
  //#region src/helpers/addons/tauri-setup.ts
3038
2265
  function getWebFrontend(frontend) {
3039
2266
  return frontend.find((value) => types_exports.desktopWebFrontends.includes(value));
@@ -3114,7 +2341,7 @@ async function setupTauri(config) {
3114
2341
  }
3115
2342
  //#endregion
3116
2343
  //#region src/helpers/addons/tui-setup.ts
3117
- const TEMPLATES$1 = {
2344
+ const TEMPLATES = {
3118
2345
  core: {
3119
2346
  label: "Core",
3120
2347
  hint: "Basic OpenTUI template"
@@ -3128,7 +2355,7 @@ const TEMPLATES$1 = {
3128
2355
  hint: "SolidJS-based OpenTUI template"
3129
2356
  }
3130
2357
  };
3131
- const DEFAULT_TEMPLATE$1 = "core";
2358
+ const DEFAULT_TEMPLATE = "core";
3132
2359
  const TUI_LOCKFILES = [
3133
2360
  "bun.lock",
3134
2361
  "package-lock.json",
@@ -3138,7 +2365,7 @@ const TUI_LOCKFILES = [
3138
2365
  function resolveTuiTemplate(config) {
3139
2366
  const configuredTemplate = config.addonOptions?.opentui?.template;
3140
2367
  if (configuredTemplate) return configuredTemplate;
3141
- if (isSilent()) return DEFAULT_TEMPLATE$1;
2368
+ if (isSilent()) return DEFAULT_TEMPLATE;
3142
2369
  }
3143
2370
  async function setupTui(config) {
3144
2371
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
@@ -3149,14 +2376,14 @@ async function setupTui(config) {
3149
2376
  setIsFirstPrompt(true);
3150
2377
  const selectedTemplate = await navigableSelect({
3151
2378
  message: "Choose a template",
3152
- options: Object.entries(TEMPLATES$1).map(([key, templateOption]) => ({
2379
+ options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
3153
2380
  value: key,
3154
2381
  label: templateOption.label,
3155
2382
  hint: templateOption.hint
3156
2383
  })),
3157
- initialValue: DEFAULT_TEMPLATE$1
2384
+ initialValue: DEFAULT_TEMPLATE
3158
2385
  });
3159
- if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
2386
+ if (isCancel$2(selectedTemplate)) return userCancelled("Operation cancelled");
3160
2387
  template = selectedTemplate;
3161
2388
  }
3162
2389
  const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
@@ -3234,324 +2461,6 @@ async function postProcessTuiWorkspace(tuiDir) {
3234
2461
  return Result.ok(void 0);
3235
2462
  }
3236
2463
  //#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
2464
  //#region src/helpers/addons/addons-setup.ts
3556
2465
  async function runSetup(setupFn) {
3557
2466
  const result = await setupFn();
@@ -3572,41 +2481,27 @@ async function runAddonStep(addon, step) {
3572
2481
  if (result.isErr()) cliConsola.error(pc.red(result.error.message));
3573
2482
  }
3574
2483
  async function setupAddons(config) {
3575
- const { addons, testing, frontend, projectDir } = config;
2484
+ const { addons, frontend, projectDir } = config;
3576
2485
  const hasWebFrontend = frontend.some((value) => types_exports.desktopWebFrontends.includes(value));
3577
2486
  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");
2487
+ const hasOxlint = addons.includes("oxlint");
2488
+ const hasBiome = !hasOxlint && addons.includes("biome");
3581
2489
  const hasHusky = addons.includes("husky");
3582
2490
  const hasLefthook = addons.includes("lefthook");
3583
2491
  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
- }
2492
+ if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
2493
+ else if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
2494
+ if (hasHusky || hasLefthook) {
2495
+ let linter;
2496
+ if (hasOxlint) linter = "oxlint";
2497
+ else if (hasBiome) linter = "biome";
2498
+ else if (hasVitePlus) linter = "vite-plus";
2499
+ if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
2500
+ if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3600
2501
  }
3601
- if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
3602
- if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config));
3603
2502
  if (addons.includes("opentui")) await runSetup(() => setupTui(config));
3604
- if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
3605
2503
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
3606
2504
  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
2505
  }
3611
2506
  async function setupBiome(projectDir) {
3612
2507
  await addPackageDependency({
@@ -3679,6 +2574,20 @@ async function setupTesting(projectDir, testing) {
3679
2574
  await updateScripts(projectDir, { "test:e2e": "playwright test" });
3680
2575
  }
3681
2576
  }
2577
+ /** Best-effort browser install after deps; never fails project creation. */
2578
+ async function installPlaywrightBrowsers(projectDir, packageManager) {
2579
+ if (shouldSkipExternalCommands()) return;
2580
+ const s = createSpinner();
2581
+ s.start("Installing Playwright browsers...");
2582
+ const args = getPackageExecutionArgs(packageManager, "playwright install");
2583
+ try {
2584
+ await $({ cwd: projectDir })`${args}`;
2585
+ s.stop(pc.green("Playwright browsers installed"));
2586
+ } catch (e) {
2587
+ s.stop(pc.yellow("Playwright browser install skipped (run playwright install later)"));
2588
+ cliLog.info(pc.dim(`Playwright browsers were not installed automatically: ${e instanceof Error ? e.message : String(e)}`));
2589
+ }
2590
+ }
3682
2591
  //#endregion
3683
2592
  //#region src/utils/config-processing.ts
3684
2593
  function processArrayOption(options) {
@@ -3700,7 +2609,7 @@ function processFlags(options, projectName) {
3700
2609
  if (options.database) config.database = options.database;
3701
2610
  if (options.orm) config.orm = options.orm;
3702
2611
  if (options.auth !== void 0) config.auth = options.auth;
3703
- if (options.payments !== void 0) config.payments = options.payments;
2612
+ if (options.payments !== void 0) config.payments = normalizePayments(options.payments);
3704
2613
  if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
3705
2614
  if (options.disableObservability) config.observability = [];
3706
2615
  if (options.communication !== void 0) config.communication = options.communication;
@@ -3761,6 +2670,7 @@ async function detectProjectConfig(projectDir) {
3761
2670
  frontend: kubojsConfig.frontend,
3762
2671
  addons: kubojsConfig.addons,
3763
2672
  examples: kubojsConfig.examples,
2673
+ testing: kubojsConfig.testing,
3764
2674
  auth: kubojsConfig.auth,
3765
2675
  payments: kubojsConfig.payments,
3766
2676
  observability: normalizeObservability(kubojsConfig.observability),
@@ -3808,9 +2718,8 @@ const ADD_PACKAGE_JSON_PATHS = [
3808
2718
  "apps/web/package.json",
3809
2719
  "apps/native/package.json",
3810
2720
  "apps/desktop/package.json",
3811
- "apps/fumadocs/package.json",
3812
2721
  "apps/docs/package.json",
3813
- "packages/api/package.json",
2722
+ "apps/api/package.json",
3814
2723
  "packages/db/package.json",
3815
2724
  "packages/auth/package.json",
3816
2725
  "packages/backend/package.json",
@@ -3820,17 +2729,21 @@ const ADD_PACKAGE_JSON_PATHS = [
3820
2729
  "packages/ui/package.json"
3821
2730
  ];
3822
2731
  const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
2732
+ const ADD_ENV_FILE_PATHS = [
2733
+ ".env",
2734
+ "apps/web/.env",
2735
+ "apps/server/.env",
2736
+ "apps/native/.env",
2737
+ "packages/backend/.env.local",
2738
+ "packages/infra/.env"
2739
+ ];
3823
2740
  const HOOK_ADDONS = ["husky", "lefthook"];
3824
2741
  const HOOK_LINTER_ADDONS = [
3825
2742
  "biome",
3826
2743
  "oxlint",
3827
2744
  "vite-plus"
3828
2745
  ];
3829
- const TASK_RUNNER_ADDONS = [
3830
- "turborepo",
3831
- "nx",
3832
- "vite-plus"
3833
- ];
2746
+ const TASK_RUNNER_ADDONS = ["turborepo", "vite-plus"];
3834
2747
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3835
2748
  if (!existingAddonOptions && !nextAddonOptions) return;
3836
2749
  const mergedAddonOptions = { ...existingAddonOptions };
@@ -3868,11 +2781,8 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
3868
2781
  const packageJson = await fs.readJson(packageJsonPath);
3869
2782
  packageJson.devDependencies = packageJson.devDependencies ?? {};
3870
2783
  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") {
2784
+ if (removed === "biome") delete packageJson.devDependencies["@biomejs/biome"];
2785
+ if (removed === "oxlint") {
3876
2786
  delete packageJson.devDependencies.oxlint;
3877
2787
  delete packageJson.devDependencies.oxfmt;
3878
2788
  }
@@ -3880,7 +2790,6 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
3880
2790
  if (typeof packageJson.scripts?.check === "string" && removedLinters.some((linter) => {
3881
2791
  if (linter === "biome") return packageJson.scripts.check.includes("biome");
3882
2792
  if (linter === "oxlint") return packageJson.scripts.check.includes("oxlint");
3883
- if (linter === "ultracite") return packageJson.scripts.check.includes("ultracite");
3884
2793
  return false;
3885
2794
  })) delete packageJson.scripts.check;
3886
2795
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
@@ -3936,7 +2845,7 @@ async function addHandlerInternal(input) {
3936
2845
  }));
3937
2846
  if (!isSilent()) {
3938
2847
  renderTitle();
3939
- intro(pc.magenta("Add addons to your kubojs project"));
2848
+ intro(cliColors.signal("Add addons to your kubojs project"));
3940
2849
  }
3941
2850
  const existingConfig = await detectProjectConfig(projectDir);
3942
2851
  if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubojs.jsonrc exists.` }));
@@ -3969,7 +2878,7 @@ async function addHandlerInternal(input) {
3969
2878
  if (selectedAddons.length === 0) {
3970
2879
  if (!isSilent()) {
3971
2880
  log.info(pc.dim("No addons selected."));
3972
- outro(pc.magenta("Nothing to add."));
2881
+ outro(cliColors.signal("Nothing to add."));
3973
2882
  }
3974
2883
  return Result.ok({
3975
2884
  success: true,
@@ -3980,10 +2889,11 @@ async function addHandlerInternal(input) {
3980
2889
  addonsToAdd = selectedAddons;
3981
2890
  }
3982
2891
  const { updatedAddons, removedAddons } = mergeAddonsExclusive(existingConfig.addons, addonsToAdd);
2892
+ const updatedTesting = existingConfig.testing ?? [];
3983
2893
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3984
2894
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3985
2895
  if (!isSilent()) {
3986
- log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
2896
+ log.info(cliColors.bright(`Adding addons: ${addonsToAdd.join(", ")}`));
3987
2897
  if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
3988
2898
  }
3989
2899
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
@@ -4003,6 +2913,7 @@ async function addHandlerInternal(input) {
4003
2913
  payments: existingConfig.payments,
4004
2914
  observability: existingConfig.observability,
4005
2915
  communication: existingConfig.communication ?? "none",
2916
+ testing: updatedTesting,
4006
2917
  git: false,
4007
2918
  packageManager: input.packageManager || existingConfig.packageManager,
4008
2919
  install: input.install ?? false,
@@ -4031,10 +2942,17 @@ async function addHandlerInternal(input) {
4031
2942
  vfs.writeFile(filePath, content);
4032
2943
  }
4033
2944
  }
2945
+ for (const filePath of ADD_ENV_FILE_PATHS) {
2946
+ const fullPath = path.join(projectDir, filePath);
2947
+ if (await fs.pathExists(fullPath)) {
2948
+ const content = await fs.readFile(fullPath, "utf-8");
2949
+ vfs.writeFile(filePath, content);
2950
+ }
2951
+ }
4034
2952
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
4035
2953
  processAddonsDeps(vfs, config);
2954
+ processEnvVariables(vfs, updatedConfig);
4036
2955
  if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
4037
- if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
4038
2956
  if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
4039
2957
  if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
4040
2958
  if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
@@ -4049,7 +2967,7 @@ async function addHandlerInternal(input) {
4049
2967
  if (!isSilent()) {
4050
2968
  log.success(pc.green("Dry run validation passed. No addon files were written."));
4051
2969
  log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
4052
- outro(pc.magenta("Dry run complete."));
2970
+ outro(cliColors.signal("Dry run complete."));
4053
2971
  }
4054
2972
  return Result.ok({
4055
2973
  success: true,
@@ -4087,7 +3005,8 @@ async function addHandlerInternal(input) {
4087
3005
  if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
4088
3006
  await updateKubojsConfig(projectDir, {
4089
3007
  addons: updatedAddons,
4090
- addonOptions: updatedConfig.addonOptions
3008
+ addonOptions: updatedConfig.addonOptions,
3009
+ testing: updatedTesting
4091
3010
  });
4092
3011
  if (input.install) {
4093
3012
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
@@ -4099,7 +3018,7 @@ async function addHandlerInternal(input) {
4099
3018
  if (!isSilent()) {
4100
3019
  log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
4101
3020
  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!"));
3021
+ outro(cliColors.signal("Addons added successfully!"));
4103
3022
  }
4104
3023
  return Result.ok({
4105
3024
  success: true,
@@ -4112,7 +3031,7 @@ async function addHandlerInternal(input) {
4112
3031
  //#region src/prompts/api.ts
4113
3032
  async function getApiChoice(Api, frontend, backend, previousValue) {
4114
3033
  if (backend === "convex" || backend === "none") return "none";
4115
- const allowed = allowedApisForFrontends(frontend ?? []);
3034
+ const allowed = allowedApisForFrontends(frontend ?? [], backend);
4116
3035
  if (Api) {
4117
3036
  const compat = validateApiFrontendCompatibility(Api, frontend ?? []);
4118
3037
  if (compat.isErr()) throw compat.error;
@@ -4126,6 +3045,10 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4126
3045
  value: "orpc",
4127
3046
  label: "oRPC",
4128
3047
  hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
3048
+ } : a === "orval" ? {
3049
+ value: "orval",
3050
+ label: "Orval",
3051
+ hint: "OpenAPI REST APIs with generated Hono handlers and Fetch clients"
4129
3052
  } : {
4130
3053
  value: "none",
4131
3054
  label: "None",
@@ -4136,7 +3059,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4136
3059
  options: apiOptions,
4137
3060
  initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
4138
3061
  });
4139
- if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
3062
+ if (isCancel$2(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
4140
3063
  return apiType;
4141
3064
  }
4142
3065
  //#endregion
@@ -4188,7 +3111,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
4188
3111
  options,
4189
3112
  initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4190
3113
  });
4191
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3114
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4192
3115
  return response;
4193
3116
  }
4194
3117
  //#endregion
@@ -4242,7 +3165,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
4242
3165
  options: backendOptions,
4243
3166
  initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4244
3167
  });
4245
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3168
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4246
3169
  return response;
4247
3170
  }
4248
3171
  //#endregion
@@ -4265,6 +3188,11 @@ async function getCommunicationChoice(communication, backend, previousValue) {
4265
3188
  value: "notifique",
4266
3189
  label: "Notifique",
4267
3190
  hint: "Omnichannel BR messaging API"
3191
+ },
3192
+ {
3193
+ value: "arara",
3194
+ label: "AraraHQ",
3195
+ hint: "WhatsApp messaging via the official Node SDK"
4268
3196
  }
4269
3197
  ];
4270
3198
  const response = await navigableSelect({
@@ -4272,7 +3200,7 @@ async function getCommunicationChoice(communication, backend, previousValue) {
4272
3200
  options,
4273
3201
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.communication)
4274
3202
  });
4275
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3203
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4276
3204
  return response;
4277
3205
  }
4278
3206
  //#endregion
@@ -4312,7 +3240,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4312
3240
  options: databaseOptions,
4313
3241
  initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4314
3242
  });
4315
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3243
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4316
3244
  return response;
4317
3245
  }
4318
3246
  //#endregion
@@ -4411,7 +3339,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
4411
3339
  options,
4412
3340
  initialValue: preferValidInitial(options, previousValue, "none")
4413
3341
  });
4414
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3342
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4415
3343
  return response;
4416
3344
  }
4417
3345
  //#endregion
@@ -4438,7 +3366,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
4438
3366
  required: false,
4439
3367
  initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
4440
3368
  });
4441
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3369
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4442
3370
  return response;
4443
3371
  }
4444
3372
  //#endregion
@@ -4474,7 +3402,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4474
3402
  initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
4475
3403
  });
4476
3404
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
4477
- if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
3405
+ if (isCancel$2(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
4478
3406
  setIsFirstPrompt(false);
4479
3407
  const result = [];
4480
3408
  let shouldRestart = false;
@@ -4527,7 +3455,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4527
3455
  initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4528
3456
  });
4529
3457
  if (isGoBack(webFramework)) shouldRestart = true;
4530
- else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3458
+ else if (isCancel$2(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4531
3459
  else result.push(webFramework);
4532
3460
  }
4533
3461
  if (shouldRestart) {
@@ -4561,7 +3489,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4561
3489
  setIsFirstPrompt(wasFirstPrompt);
4562
3490
  continue;
4563
3491
  }
4564
- else if (isCancel$1(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3492
+ else if (isCancel$2(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4565
3493
  else result.push(nativeFramework);
4566
3494
  }
4567
3495
  if (shouldRestart) {
@@ -4579,7 +3507,7 @@ async function getGitChoice(git, previousValue) {
4579
3507
  message: "Initialize git repository?",
4580
3508
  initialValue: previousValue ?? DEFAULT_CONFIG.git
4581
3509
  });
4582
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3510
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4583
3511
  return response;
4584
3512
  }
4585
3513
  //#endregion
@@ -4590,7 +3518,7 @@ async function getinstallChoice(install, previousValue) {
4590
3518
  message: "Install dependencies?",
4591
3519
  initialValue: previousValue ?? DEFAULT_CONFIG.install
4592
3520
  });
4593
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3521
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4594
3522
  return response;
4595
3523
  }
4596
3524
  //#endregion
@@ -4612,7 +3540,7 @@ async function getObservabilityChoice(observability, previousValue) {
4612
3540
  required: false,
4613
3541
  initialValues: previousValue ?? DEFAULT_CONFIG.observability
4614
3542
  });
4615
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3543
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4616
3544
  return response;
4617
3545
  }
4618
3546
  //#endregion
@@ -4730,7 +3658,7 @@ function validateBackendNoneConstraints(config, providedFlags) {
4730
3658
  if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
4731
3659
  if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
4732
3660
  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'.");
3661
+ 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
3662
  if (has("communication") && config.communication !== "none") return validationErr("Backend 'none' requires '--communication none'. Please remove the --communication flag or set it to 'none'.");
4735
3663
  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
3664
  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 +3707,8 @@ function validateFrontendConstraints(config, providedFlags) {
4779
3707
  return Result.ok(void 0);
4780
3708
  }
4781
3709
  function validateApiConstraints(config, options) {
3710
+ 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.");
3711
+ 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
3712
  if (config.api === "none") {
4783
3713
  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
3714
  }
@@ -4811,6 +3741,7 @@ function validateFullConfig(config, providedFlags, options) {
4811
3741
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4812
3742
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? [], config.orm, config.database);
4813
3743
  yield* validateCommunicationCompatibility(config.communication, config.backend);
3744
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4814
3745
  return Result.ok(void 0);
4815
3746
  });
4816
3747
  }
@@ -4822,6 +3753,7 @@ function validateConfigForProgrammaticUse(config) {
4822
3753
  yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4823
3754
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend, config.orm, config.database);
4824
3755
  yield* validateCommunicationCompatibility(config.communication, config.backend);
3756
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4825
3757
  if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
4826
3758
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4827
3759
  return Result.ok(void 0);
@@ -4860,7 +3792,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
4860
3792
  options,
4861
3793
  initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4862
3794
  });
4863
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3795
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4864
3796
  return response;
4865
3797
  }
4866
3798
  //#endregion
@@ -4889,47 +3821,48 @@ async function getPackageManagerChoice(packageManager, previousValue) {
4889
3821
  ],
4890
3822
  initialValue: previousValue ?? detectedPackageManager
4891
3823
  });
4892
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3824
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4893
3825
  return response;
4894
3826
  }
4895
3827
  //#endregion
4896
3828
  //#region src/prompts/payments.ts
4897
3829
  async function getPaymentsChoice(payments, auth, backend, frontends = [], previousValue) {
4898
3830
  if (payments !== void 0) return payments;
4899
- if (backend === "none") return "none";
3831
+ if (backend === "none") return [];
4900
3832
  const options = [{
4901
- value: "none",
4902
- label: "None",
4903
- hint: "No payments integration"
4904
- }, {
4905
3833
  value: "abacatepay",
4906
3834
  label: "AbacatePay",
4907
3835
  hint: "Hosted checkout with webhook reconciliation"
3836
+ }, {
3837
+ value: "stripe",
3838
+ label: "Stripe",
3839
+ hint: "Embedded Checkout with a fulfillment webhook"
4908
3840
  }];
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)
3841
+ const getDisabledReason = (provider) => {
3842
+ const issue = getPaymentCompatibilityIssue({
3843
+ provider,
3844
+ backend,
3845
+ frontends
3846
+ });
3847
+ if (issue === "requires-web-frontend") return "Requires a web frontend";
3848
+ if (issue === "convex-unsupported") return "Not supported with Convex";
3849
+ if (issue === "native-only-unsupported") return "Not supported with native-only frontends";
3850
+ if (issue === "sql-database-required") return "Requires a SQL database with Prisma or Drizzle";
3851
+ };
3852
+ const response = await navigableMultiselect({
3853
+ message: "Select payments providers",
3854
+ options: options.map((option) => {
3855
+ const disabledReason = getDisabledReason(option.value);
3856
+ return {
3857
+ ...option,
3858
+ hint: disabledReason ?? option.hint,
3859
+ disabled: disabledReason !== void 0
3860
+ };
3861
+ }),
3862
+ required: false,
3863
+ initialValues: previousValue ?? DEFAULT_CONFIG.payments
4931
3864
  });
4932
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3865
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4933
3866
  return response;
4934
3867
  }
4935
3868
  //#endregion
@@ -4956,7 +3889,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
4956
3889
  options: runtimeOptions,
4957
3890
  initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4958
3891
  });
4959
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3892
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4960
3893
  return response;
4961
3894
  }
4962
3895
  //#endregion
@@ -5014,7 +3947,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
5014
3947
  options,
5015
3948
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
5016
3949
  });
5017
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3950
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5018
3951
  return response;
5019
3952
  }
5020
3953
  //#endregion
@@ -5037,7 +3970,7 @@ async function getTestingChoice(testing, frontends, previousValue) {
5037
3970
  required: false,
5038
3971
  initialValues: (previousValue ?? DEFAULT_CONFIG.testing)?.filter((t) => options.some((o) => o.value === t))
5039
3972
  });
5040
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3973
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5041
3974
  return response;
5042
3975
  }
5043
3976
  //#endregion
@@ -5090,7 +4023,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
5090
4023
  options,
5091
4024
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
5092
4025
  });
5093
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4026
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5094
4027
  return response;
5095
4028
  }
5096
4029
  //#endregion
@@ -5174,6 +4107,32 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5174
4107
  }
5175
4108
  //#endregion
5176
4109
  //#region src/prompts/project-name.ts
4110
+ const S_BAR = "│";
4111
+ const S_BAR_END = "└";
4112
+ const S_STEP_ACTIVE = "◆";
4113
+ function kuboText(options) {
4114
+ return new TextPrompt({
4115
+ validate: options.validate,
4116
+ placeholder: options.placeholder,
4117
+ defaultValue: options.defaultValue,
4118
+ initialValue: options.initialValue,
4119
+ render() {
4120
+ const title = `${cliColors.signal(S_BAR)}\n${cliColors.bright(S_STEP_ACTIVE)} ${options.message}\n`;
4121
+ const placeholder = options.placeholder ? pc.inverse(options.placeholder[0]) + pc.dim(options.placeholder.slice(1)) : pc.inverse(pc.hidden("_"));
4122
+ const input = this.userInput ? this.userInputWithCursor : placeholder;
4123
+ const value = this.value ?? "";
4124
+ switch (this.state) {
4125
+ case "error": {
4126
+ const error = this.error ? ` ${pc.yellow(this.error)}` : "";
4127
+ return `${title.trim()}\n${cliColors.signal(S_BAR)} ${input}\n${pc.yellow(S_BAR_END)}${error}\n`;
4128
+ }
4129
+ case "submit": return `${title}${pc.gray(S_BAR)}${value ? ` ${pc.dim(value)}` : ""}`;
4130
+ case "cancel": return `${title}${pc.gray(S_BAR)}${value ? ` ${pc.strikethrough(pc.dim(value))}` : ""}${value.trim() ? `\n${pc.gray(S_BAR)}` : ""}`;
4131
+ default: return `${title}${cliColors.signal(S_BAR)} ${input}\n${cliColors.signal(S_BAR_END)}\n`;
4132
+ }
4133
+ }
4134
+ }).prompt();
4135
+ }
5177
4136
  function isPathWithinCwd$1(targetPath) {
5178
4137
  const resolved = path.resolve(targetPath);
5179
4138
  const rel = path.relative(process.cwd(), resolved);
@@ -5201,7 +4160,7 @@ async function getProjectName(initialName) {
5201
4160
  counter++;
5202
4161
  }
5203
4162
  while (!isValid) {
5204
- const response = await text({
4163
+ const response = await kuboText({
5205
4164
  message: "Enter your project name or path (relative to current directory)",
5206
4165
  placeholder: defaultName,
5207
4166
  initialValue: initialName,
@@ -5215,7 +4174,7 @@ async function getProjectName(initialName) {
5215
4174
  }
5216
4175
  }
5217
4176
  });
5218
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
4177
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled." });
5219
4178
  projectPath = response || defaultName;
5220
4179
  isValid = true;
5221
4180
  }
@@ -5256,48 +4215,48 @@ async function trackProjectCreation(config, disableAnalytics = false) {
5256
4215
  //#region src/utils/display-config.ts
5257
4216
  function displayConfig(config) {
5258
4217
  const configDisplay = [];
5259
- if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
4218
+ if (config.projectName) configDisplay.push(`${cliColors.signal("Project Name:")} ${config.projectName}`);
5260
4219
  if (config.frontend !== void 0) {
5261
4220
  const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
5262
4221
  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"}`);
4222
+ configDisplay.push(`${cliColors.signal("Frontend:")} ${frontendText}`);
4223
+ }
4224
+ if (config.backend !== void 0) configDisplay.push(`${cliColors.signal("Backend:")} ${String(config.backend)}`);
4225
+ if (config.runtime !== void 0) configDisplay.push(`${cliColors.signal("Runtime:")} ${String(config.runtime)}`);
4226
+ if (config.api !== void 0) configDisplay.push(`${cliColors.signal("API:")} ${String(config.api)}`);
4227
+ if (config.database !== void 0) configDisplay.push(`${cliColors.signal("Database:")} ${String(config.database)}`);
4228
+ if (config.orm !== void 0) configDisplay.push(`${cliColors.signal("ORM:")} ${String(config.orm)}`);
4229
+ if (config.auth !== void 0) configDisplay.push(`${cliColors.signal("Auth:")} ${String(config.auth)}`);
4230
+ if (config.payments !== void 0) configDisplay.push(`${cliColors.signal("Payments:")} ${config.payments.join(", ") || "none"}`);
4231
+ if (config.communication !== void 0) configDisplay.push(`${cliColors.signal("Communication:")} ${String(config.communication)}`);
4232
+ if (config.observability !== void 0) configDisplay.push(`${cliColors.signal("Observability:")} ${config.observability.join(", ") || "none"}`);
5274
4233
  if (config.addons !== void 0) {
5275
4234
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
5276
4235
  const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
5277
- configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
4236
+ configDisplay.push(`${cliColors.signal("Addons:")} ${addonsText}`);
5278
4237
  }
5279
4238
  if (config.examples !== void 0) {
5280
4239
  const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
5281
4240
  const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
5282
- configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
4241
+ configDisplay.push(`${cliColors.signal("Examples:")} ${examplesText}`);
5283
4242
  }
5284
4243
  if (config.testing !== void 0) {
5285
4244
  const testing = Array.isArray(config.testing) ? config.testing : [config.testing];
5286
4245
  const testingText = testing.length > 0 && testing[0] !== void 0 ? testing.join(", ") : "none";
5287
- configDisplay.push(`${pc.blue("Testing:")} ${testingText}`);
4246
+ configDisplay.push(`${cliColors.signal("Testing:")} ${testingText}`);
5288
4247
  }
5289
4248
  if (config.git !== void 0) {
5290
4249
  const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
5291
- configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
4250
+ configDisplay.push(`${cliColors.signal("Git Init:")} ${gitText}`);
5292
4251
  }
5293
- if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
4252
+ if (config.packageManager !== void 0) configDisplay.push(`${cliColors.signal("Package Manager:")} ${String(config.packageManager)}`);
5294
4253
  if (config.install !== void 0) {
5295
4254
  const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
5296
- configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
4255
+ configDisplay.push(`${cliColors.signal("Install Dependencies:")} ${installText}`);
5297
4256
  }
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)}`);
4257
+ if (config.dbSetup !== void 0) configDisplay.push(`${cliColors.signal("Database Setup:")} ${String(config.dbSetup)}`);
4258
+ if (config.webDeploy !== void 0) configDisplay.push(`${cliColors.signal("Web Deployment:")} ${String(config.webDeploy)}`);
4259
+ if (config.serverDeploy !== void 0) configDisplay.push(`${cliColors.signal("Server Deployment:")} ${String(config.serverDeploy)}`);
5301
4260
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
5302
4261
  return configDisplay.join("\n");
5303
4262
  }
@@ -5427,7 +4386,7 @@ const TEMPLATE_PRESETS = {
5427
4386
  frontend: ["react-router"],
5428
4387
  api: "orpc",
5429
4388
  auth: "better-auth",
5430
- payments: "none",
4389
+ payments: [],
5431
4390
  observability: ["getmonitor"],
5432
4391
  communication: "none",
5433
4392
  addons: ["turborepo"],
@@ -5444,7 +4403,7 @@ const TEMPLATE_PRESETS = {
5444
4403
  frontend: ["tanstack-router"],
5445
4404
  api: "trpc",
5446
4405
  auth: "better-auth",
5447
- payments: "none",
4406
+ payments: [],
5448
4407
  observability: ["getmonitor"],
5449
4408
  communication: "none",
5450
4409
  addons: ["turborepo"],
@@ -5461,7 +4420,7 @@ const TEMPLATE_PRESETS = {
5461
4420
  frontend: ["next"],
5462
4421
  api: "trpc",
5463
4422
  auth: "better-auth",
5464
- payments: "none",
4423
+ payments: [],
5465
4424
  observability: ["getmonitor"],
5466
4425
  communication: "none",
5467
4426
  addons: ["biome", "turborepo"],
@@ -5478,7 +4437,7 @@ const TEMPLATE_PRESETS = {
5478
4437
  frontend: ["native-uniwind"],
5479
4438
  api: "none",
5480
4439
  auth: "none",
5481
- payments: "none",
4440
+ payments: [],
5482
4441
  observability: [],
5483
4442
  communication: "none",
5484
4443
  addons: ["none"],
@@ -5614,7 +4573,7 @@ function shouldSkipFile(fileName) {
5614
4573
  }
5615
4574
  function shouldUseBiomeFormatter(addons) {
5616
4575
  if (!addons) return false;
5617
- return addons.includes("biome") || addons.includes("ultracite");
4576
+ return addons.includes("biome");
5618
4577
  }
5619
4578
  async function formatCode(filePath, content) {
5620
4579
  const result = await Result.tryPromise({
@@ -5670,7 +4629,7 @@ async function formatProjectWithBiome(projectDir) {
5670
4629
  }
5671
4630
  /**
5672
4631
  * Post-scaffold format.
5673
- * - biome / ultracite → Biome `check --write` (not oxfmt)
4632
+ * - biome → Biome `check --write` (not oxfmt)
5674
4633
  * - oxlint / none → oxfmt
5675
4634
  * Always skips kubojs.jsonrc, lockfiles, and binary extensions (oxfmt path).
5676
4635
  */
@@ -6118,7 +5077,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
6118
5077
  }
6119
5078
  async function setupWithNeonDb(projectDir, packageManager, backend) {
6120
5079
  const s = createSpinner();
6121
- s.start("Creating Neon database using get-db...");
5080
+ s.start("Creating Neon database using neon-new...");
6122
5081
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
6123
5082
  const targetDir = path.join(projectDir, targetApp);
6124
5083
  const ensureDirResult = await Result.tryPromise({
@@ -6133,17 +5092,17 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
6133
5092
  s.stop(pc.red("Failed to create directory"));
6134
5093
  return ensureDirResult;
6135
5094
  }
6136
- const packageArgs = getPackageExecutionArgs(packageManager, `get-db@latest --yes --ref "sbA3tIe"`);
5095
+ const packageArgs = getPackageExecutionArgs(packageManager, `neon-new@latest --yes --ref "sbA3tIe"`);
6137
5096
  return Result.tryPromise({
6138
5097
  try: async () => {
6139
5098
  await $({ cwd: targetDir })`${packageArgs}`;
6140
5099
  s.stop(pc.green("Neon database created successfully!"));
6141
5100
  },
6142
5101
  catch: (e) => {
6143
- s.stop(pc.red("Failed to create database with get-db"));
5102
+ s.stop(pc.red("Failed to create database with neon-new"));
6144
5103
  return new DatabaseSetupError({
6145
5104
  provider: "neon",
6146
- message: `Failed to create database with get-db: ${e instanceof Error ? e.message : String(e)}`,
5105
+ message: `Failed to create database with neon-new: ${e instanceof Error ? e.message : String(e)}`,
6147
5106
  cause: e
6148
5107
  });
6149
5108
  }
@@ -6209,7 +5168,7 @@ async function setupNeonPostgres(config, cliInput) {
6209
5168
  const promptedSetupMethod = await select({
6210
5169
  message: "Choose your Neon setup method:",
6211
5170
  options: [{
6212
- label: "Quick setup with get-db",
5171
+ label: "Quick setup with neon-new",
6213
5172
  value: "neondb",
6214
5173
  hint: "fastest, no auth required"
6215
5174
  }, {
@@ -7141,6 +6100,10 @@ async function getDockerStatus(database) {
7141
6100
  }
7142
6101
  //#endregion
7143
6102
  //#region src/helpers/core/post-installation.ts
6103
+ const pc$1 = {
6104
+ ...pc,
6105
+ cyan: cliColors.signal
6106
+ };
7144
6107
  function getDesktopStaticBuildNote(frontend) {
7145
6108
  const staticBuildFrontends = new Map([
7146
6109
  ["tanstack-start", "TanStack Start"],
@@ -7151,7 +6114,7 @@ function getDesktopStaticBuildNote(frontend) {
7151
6114
  ]);
7152
6115
  const staticBuildFrontend = frontend.find((value) => staticBuildFrontends.has(value));
7153
6116
  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.`;
6117
+ 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
6118
  }
7156
6119
  async function displayPostInstallInstructions(config) {
7157
6120
  const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication } = config;
@@ -7173,50 +6136,52 @@ async function displayPostInstallInstructions(config) {
7173
6136
  const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
7174
6137
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
7175
6138
  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) : "";
6139
+ const playwrightInstructions = testing?.includes("playwright") ? getPlaywrightInstructions(runCmd, packageManager) : "";
7178
6140
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
7179
6141
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
7180
6142
  const getMonitorInstructions = observability.includes("getmonitor") ? getGetMonitorInstructions() : "";
7181
6143
  const resendInstructions = communication === "resend" ? getResendInstructions() : "";
7182
6144
  const notifiqueInstructions = communication === "notifique" ? getNotifiqueInstructions() : "";
6145
+ const araraInstructions = communication === "arara" ? getAraraInstructions(backend) : "";
7183
6146
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
7184
6147
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
7185
6148
  const webPort = (0, types_exports.getWebPort)(frontend ?? []);
7186
6149
  const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager, runCmd) : "";
7187
6150
  const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
7188
6151
  const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
7189
- let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
6152
+ let output = `${pc$1.bold("Next steps")}\n${pc$1.cyan("1.")} ${cdCmd}\n`;
7190
6153
  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`;
6154
+ if (!depsInstalled) output += `${pc$1.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
6155
+ 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
6156
  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`;
6157
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc$1.dim(" (this will guide you through Convex project setup)")}\n`;
6158
+ output += `${pc$1.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc$1.white(" packages/backend/.env.local")} to ${pc$1.white("apps/*/.env")}\n`;
6159
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
6160
+ } else if (isBackendSelf) output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7198
6161
  else {
7199
- if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
6162
+ if (runtime !== "workers") output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7200
6163
  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`;
6164
+ if (dbSetup === "d1") output += `${pc$1.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
6165
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7203
6166
  }
7204
6167
  }
7205
6168
  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`;
6169
+ if (hasWeb || hasStandaloneBackend) {
6170
+ output += `${pc$1.bold("Your project will be available at:")}\n`;
6171
+ if (hasWeb) output += `${pc$1.cyan("•")} Frontend: http://localhost:${webPort}\n`;
6172
+ else if (!hasNative) output += `${pc$1.yellow("NOTE:")} You are creating a backend-only app\n (no frontend selected)\n`;
7210
6173
  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`;
6174
+ output += `${pc$1.cyan("•")} Backend API: http://localhost:3000\n`;
6175
+ if (api === "orpc") output += `${pc$1.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
6176
+ if (api === "orval") {
6177
+ output += `${pc$1.cyan("•")} OpenAPI contract: apps/api/openapi.yaml\n`;
6178
+ output += `${pc$1.cyan("•")} Regenerate API code: bun run api:generate\n`;
6179
+ }
7213
6180
  }
7214
6181
  if (isBackendSelf && api === "orpc") {
7215
6182
  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`;
6183
+ output += `${pc$1.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
7217
6184
  }
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
6185
  }
7221
6186
  if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
7222
6187
  if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
@@ -7227,18 +6192,18 @@ async function displayPostInstallInstructions(config) {
7227
6192
  if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
7228
6193
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
7229
6194
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
7230
- if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
7231
6195
  if (playwrightInstructions) output += `\n${playwrightInstructions.trim()}\n`;
7232
6196
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
7233
6197
  if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
7234
6198
  if (getMonitorInstructions) output += `\n${getMonitorInstructions.trim()}\n`;
7235
6199
  if (resendInstructions) output += `\n${resendInstructions.trim()}\n`;
7236
6200
  if (notifiqueInstructions) output += `\n${notifiqueInstructions.trim()}\n`;
6201
+ if (araraInstructions) output += `\n${araraInstructions.trim()}\n`;
7237
6202
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
7238
6203
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
7239
6204
  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");
6205
+ output += `\n${pc$1.bold("Like kubojs?")} Please consider giving us a star\n on GitHub:\n`;
6206
+ output += pc$1.cyan("https://github.com/albuquerquesz/kubo");
7242
6207
  cliConsola.box(output);
7243
6208
  }
7244
6209
  function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
@@ -7247,26 +6212,27 @@ function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
7247
6212
  const exampleUrl = isConvex ? "https://example.convex.cloud" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
7248
6213
  const envFileName = ".env";
7249
6214
  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`;
6215
+ let instructions = `${pc$1.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
6216
+ 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`;
6217
+ 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
6218
  return instructions;
7254
6219
  }
7255
6220
  function getHuskyInstructions(runCmd) {
7256
- return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
6221
+ return `${pc$1.bold("Git hooks with Husky:")}\n${pc$1.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
7257
6222
  }
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`;
6223
+ function getPlaywrightInstructions(runCmd, packageManager) {
6224
+ const browserInstall = packageManager === "bun" ? "bunx playwright install" : packageManager === "pnpm" ? "pnpm exec playwright install" : "npx playwright install";
6225
+ 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
6226
  }
7261
6227
  function getLintingInstructions(runCmd) {
7262
- return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
6228
+ return `${pc$1.bold("Linting and formatting:")}\n${pc$1.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
7263
6229
  }
7264
6230
  function getLefthookInstructions(packageManager) {
7265
6231
  const cmd = packageManager === "npm" ? "npx" : packageManager;
7266
- return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
6232
+ return `${pc$1.bold("Git hooks with Lefthook:")}\n${pc$1.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
7267
6233
  }
7268
6234
  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`;
6235
+ 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
6236
  }
7271
6237
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
7272
6238
  const instructions = [];
@@ -7279,62 +6245,63 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7279
6245
  }
7280
6246
  }
7281
6247
  if (isD1Alchemy) {
7282
- if (orm === "drizzle") instructions.push(`${pc.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`);
6248
+ if (orm === "drizzle") instructions.push(`${pc$1.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`);
7283
6249
  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`}`);
6250
+ instructions.push(`${pc$1.cyan("•")} Generate Prisma client: ${`${runCmd} db:generate`}`);
6251
+ instructions.push(`${pc$1.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`);
7286
6252
  }
7287
6253
  }
7288
6254
  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`);
6255
+ if (database === "mysql" && orm === "drizzle") instructions.push(`${pc$1.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
6256
+ 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
6257
  }
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`);
6258
+ 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
6259
  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`}`);
6260
+ if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc$1.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
6261
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7296
6262
  if (!isD1Alchemy) {
7297
- instructions.push(`${pc.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
7298
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
6263
+ instructions.push(`${pc$1.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
6264
+ instructions.push(`${pc$1.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7299
6265
  }
7300
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
6266
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7301
6267
  } 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`}`);
6268
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
6269
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
6270
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7305
6271
  } 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")}` : "";
6272
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
6273
+ } else if (orm === "none") instructions.push(`${pc$1.yellow("NOTE:")} Manual database schema setup\n required.`);
6274
+ return instructions.length ? `${pc$1.bold("Database commands:")}\n${instructions.join("\n")}` : "";
7309
6275
  }
7310
6276
  function getTauriInstructions(runCmd, frontend) {
7311
6277
  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}` : ""}`;
6278
+ 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
6279
  }
7314
6280
  function getElectrobunInstructions(runCmd, frontend) {
7315
6281
  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}` : ""}`;
6282
+ 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
6283
  }
7318
6284
  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`}`;
6285
+ 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
6286
  }
7324
6287
  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.`;
6288
+ return `\n${pc$1.yellow("WARNING:")} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
7326
6289
  }
7327
6290
  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")}`;
6291
+ 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
6292
  }
7330
6293
  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")}`;
6294
+ 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
6295
  }
7333
6296
  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")}`;
6297
+ 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")}`;
6298
+ }
6299
+ function getAraraInstructions(backend) {
6300
+ const envPath = backend === "convex" ? "packages/backend/.env.local" : "the server .env";
6301
+ 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
6302
  }
7336
6303
  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.`;
6304
+ return `\n${pc$1.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
7338
6305
  }
7339
6306
  function getClerkQuickstartUrl(frontend) {
7340
6307
  if (frontend.includes("next")) return "https://clerk.com/docs/nextjs/getting-started/quickstart";
@@ -7390,28 +6357,28 @@ function getClerkInstructionLines(frontend, backend, api) {
7390
6357
  }
7391
6358
  function getClerkInstructions(frontend, backend, api) {
7392
6359
  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}`)
6360
+ `${pc$1.bold("Clerk Authentication Setup:")}`,
6361
+ `${pc$1.cyan("•")} Follow the guide: ${pc$1.underline(getClerkQuickstartUrl(frontend))}`,
6362
+ ...getClerkInstructionLines(frontend, backend, api).map((line) => `${pc$1.cyan("•")} ${line}`)
7396
6363
  ].join("\n");
7397
6364
  }
7398
6365
  function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager, runCmd) {
7399
6366
  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` : "");
6367
+ 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
6368
  }
7402
6369
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
7403
6370
  const instructions = [];
7404
6371
  const isBackendSelf = backend === "self";
7405
6372
  if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) {
7406
6373
  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`}`);
6374
+ 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
6375
  } else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) {
7409
6376
  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`}`);
6377
+ 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`}`);
6378
+ } 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
6379
  if (webDeploy === "docker" || serverDeploy === "docker") {
7413
6380
  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`);
6381
+ 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
6382
  }
7416
6383
  if (webDeploy === "vercel" || serverDeploy === "vercel") {
7417
6384
  const mixedWithCloudflare = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy;
@@ -7419,14 +6386,14 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
7419
6386
  const vercelEnvScript = "env:production";
7420
6387
  const vercelDeployScript = mixedWithCloudflare ? webDeploy === "vercel" ? "deploy:web:prod" : "deploy:server:prod" : "deploy:prod";
7421
6388
  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`);
6389
+ 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
6390
  }
7424
6391
  if (webDeploy === "guaracloud" || serverDeploy === "guaracloud") {
7425
6392
  const deployScript = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy || webDeploy === "guaracloud" && serverDeploy === "guaracloud" ? `deploy:${webDeploy === "guaracloud" ? "web" : "server"}` : "deploy";
7426
6393
  const linkScript = `${deployScript}:link`;
7427
6394
  const logsScript = `${deployScript}:logs`;
7428
6395
  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/`);
6396
+ 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
6397
  }
7431
6398
  return instructions.length ? `\n${instructions.join("\n")}` : "";
7432
6399
  }
@@ -7492,10 +6459,13 @@ async function createProject(options, cliInput = {}) {
7492
6459
  }));
7493
6460
  yield* Result.await(formatProject(projectDir, { addons: options.addons }));
7494
6461
  if (!isSilent()) log.success("Project template successfully scaffolded!");
7495
- if (options.install) yield* Result.await(installDependencies({
7496
- projectDir,
7497
- packageManager: options.packageManager
7498
- }));
6462
+ if (options.install) {
6463
+ yield* Result.await(installDependencies({
6464
+ projectDir,
6465
+ packageManager: options.packageManager
6466
+ }));
6467
+ if (options.testing.includes("playwright")) await installPlaywrightBrowsers(projectDir, options.packageManager);
6468
+ }
7499
6469
  yield* Result.await(initializeGit(projectDir, options.git));
7500
6470
  if (!isSilent()) await displayPostInstallInstructions({
7501
6471
  ...options,
@@ -7568,7 +6538,7 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
7568
6538
  examples: [],
7569
6539
  testing: [],
7570
6540
  auth: "none",
7571
- payments: "none",
6541
+ payments: [],
7572
6542
  observability: [],
7573
6543
  communication: "none",
7574
6544
  git: false,
@@ -7608,7 +6578,7 @@ async function createProjectHandler(input, options = {}) {
7608
6578
  async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7609
6579
  return Result.gen(async function* () {
7610
6580
  if (!isSilent() && input.renderTitle !== false) renderTitle();
7611
- if (!isSilent()) intro(pc.magenta("Creating a new kubojs project"));
6581
+ if (!isSilent()) intro(cliColors.bright("Creating a new kubojs project"));
7612
6582
  if (!isSilent() && input.yolo) cliConsola.fatal("YOLO mode enabled - skipping checks. Things may break!");
7613
6583
  let currentPathInput;
7614
6584
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
@@ -7670,7 +6640,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7670
6640
  const templateName = input.template.toUpperCase();
7671
6641
  const templateDescription = getTemplateDescription(input.template);
7672
6642
  if (!isSilent()) {
7673
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
6643
+ log.message(pc.bold(cliColors.bright(`Using template: ${pc.white(templateName)}`)));
7674
6644
  log.message(pc.dim(` ${templateDescription}`));
7675
6645
  }
7676
6646
  const userOverrides = {};
@@ -7753,7 +6723,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7753
6723
  log.success(pc.green("Dry run validation passed. No files were written."));
7754
6724
  log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
7755
6725
  log.message(pc.dim(`Run without --dry-run to create the project.`));
7756
- outro(pc.magenta("Dry run complete."));
6726
+ outro(cliColors.signal("Dry run complete."));
7757
6727
  }
7758
6728
  return Result.ok({
7759
6729
  success: true,
@@ -7769,14 +6739,14 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7769
6739
  manualDb: cliInput.manualDb ?? input.manualDb,
7770
6740
  dbSetupOptions: effectiveDbSetupOptions
7771
6741
  }));
7772
- if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
6742
+ if (!isSilent()) log.success(cliColors.bright(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
7773
6743
  await trackProjectCreation(config, input.disableAnalytics);
7774
6744
  const historyResult = await addToHistory(config, reproducibleCommand);
7775
6745
  if (historyResult.isErr() && !isSilent()) log.warn(pc.yellow(historyResult.error.message));
7776
6746
  const elapsedTimeMs = Date.now() - startTime;
7777
6747
  if (!isSilent()) {
7778
6748
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
7779
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
6749
+ outro(cliColors.signal(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
7780
6750
  }
7781
6751
  return Result.ok({
7782
6752
  success: true,
@@ -8085,6 +7055,8 @@ async function builder() {
8085
7055
  * ```
8086
7056
  */
8087
7057
  async function createVirtual(options) {
7058
+ const rawPayments = options.payments;
7059
+ const rawObservability = options.observability;
8088
7060
  const config = {
8089
7061
  projectName: options.projectName || "my-project",
8090
7062
  projectDir: "/virtual",
@@ -8100,8 +7072,8 @@ async function createVirtual(options) {
8100
7072
  examples: options.examples || [],
8101
7073
  testing: options.testing || [],
8102
7074
  auth: options.auth || "none",
8103
- payments: options.payments || "none",
8104
- observability: options.disableObservability ? [] : options.observability || [],
7075
+ payments: rawPayments === "none" ? [] : Array.isArray(rawPayments) ? rawPayments : rawPayments ? [rawPayments] : [],
7076
+ observability: options.disableObservability ? [] : rawObservability === "none" ? [] : Array.isArray(rawObservability) ? rawObservability : rawObservability ? [rawObservability] : [],
8105
7077
  communication: options.communication || "none",
8106
7078
  git: options.git ?? false,
8107
7079
  packageManager: options.packageManager || "bun",