create-kubojs 0.1.1 → 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,17 +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
+ 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
25
48
  //#region src/utils/get-package-manager.ts
26
49
  const getUserPkgManager = () => {
27
50
  const userAgent = process.env.npm_config_user_agent;
@@ -41,10 +64,12 @@ const DEFAULT_CONFIG_BASE = {
41
64
  database: "sqlite",
42
65
  orm: "drizzle",
43
66
  auth: "better-auth",
44
- payments: "none",
45
- observability: "none",
67
+ payments: [],
68
+ observability: ["getmonitor"],
69
+ communication: "none",
46
70
  addons: ["turborepo"],
47
71
  examples: [],
72
+ testing: [],
48
73
  git: true,
49
74
  install: true,
50
75
  dbSetup: "none",
@@ -60,8 +85,11 @@ function getDefaultConfig() {
60
85
  projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
61
86
  packageManager: getUserPkgManager(),
62
87
  frontend: [...DEFAULT_CONFIG_BASE.frontend],
88
+ payments: [...DEFAULT_CONFIG_BASE.payments],
89
+ observability: [...DEFAULT_CONFIG_BASE.observability],
63
90
  addons: [...DEFAULT_CONFIG_BASE.addons],
64
- examples: [...DEFAULT_CONFIG_BASE.examples]
91
+ examples: [...DEFAULT_CONFIG_BASE.examples],
92
+ testing: [...DEFAULT_CONFIG_BASE.testing]
65
93
  };
66
94
  }
67
95
  const DEFAULT_CONFIG = getDefaultConfig();
@@ -78,17 +106,12 @@ const ADDON_COMPATIBILITY = {
78
106
  husky: [],
79
107
  lefthook: [],
80
108
  turborepo: [],
81
- nx: [],
82
109
  "vite-plus": [],
83
- starlight: [],
84
- ultracite: [],
85
110
  mcp: [],
86
111
  oxlint: [],
87
- fumadocs: [],
88
112
  opentui: [],
89
- wxt: [],
90
113
  skills: [],
91
- evlog: [],
114
+ "s3-storage": [],
92
115
  none: []
93
116
  };
94
117
  //#endregion
@@ -383,9 +406,11 @@ async function addToHistory(config, reproducibleCommand) {
383
406
  auth: config.auth,
384
407
  payments: config.payments,
385
408
  observability: config.observability,
409
+ communication: config.communication,
386
410
  api: config.api,
387
411
  addons: config.addons,
388
412
  examples: config.examples,
413
+ testing: config.testing,
389
414
  dbSetup: config.dbSetup,
390
415
  packageManager: config.packageManager
391
416
  },
@@ -423,24 +448,26 @@ const TITLE_TEXT = `
423
448
  ██║ ██╗╚██████╔╝██████╔╝╚██████╔╝
424
449
  ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
425
450
  `;
426
- const catppuccinTheme = {
427
- pink: "#F5C2E7",
428
- mauve: "#CBA6F7",
429
- red: "#F38BA8",
430
- maroon: "#E78284",
431
- peach: "#FAB387",
432
- yellow: "#F9E2AF",
433
- green: "#A6E3A1",
434
- teal: "#94E2D5",
435
- sky: "#89DCEB",
436
- sapphire: "#74C7EC",
437
- lavender: "#B4BEFE"
438
- };
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
+ ];
439
464
  const renderTitle = () => {
440
465
  const terminalWidth = process.stdout.columns || 80;
441
466
  const titleLines = TITLE_TEXT.split("\n");
442
- if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Kubo`));
443
- 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));
444
471
  };
445
472
  //#endregion
446
473
  //#region src/commands/history.ts
@@ -487,10 +514,10 @@ async function historyHandler(input) {
487
514
  return;
488
515
  }
489
516
  renderTitle();
490
- intro(pc.magenta(`Project History (${entries.length} entries)`));
517
+ intro(cliColors.signal(`Project History (${entries.length} entries)`));
491
518
  for (const [index, entry] of entries.entries()) {
492
519
  const num = pc.dim(`${index + 1}.`);
493
- const name = pc.cyan(pc.bold(entry.projectName));
520
+ const name = cliColors.bright(pc.bold(entry.projectName));
494
521
  const stack = pc.dim(formatStackSummary(entry));
495
522
  log.message(`${num} ${name}`);
496
523
  log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
@@ -516,169 +543,6 @@ async function openUrl(url) {
516
543
  await $({ stdio: "ignore" })`xdg-open ${url}`;
517
544
  }
518
545
  //#endregion
519
- //#region src/utils/sponsors.ts
520
- const SPONSORS_JSON_URL = "https://sponsors.kubojs.dev/sponsors.json";
521
- const GITHUB_SPONSOR_URL = "https://github.com/sponsors/AmanVarshney01";
522
- const nullableString = z.string().nullish().transform((value) => value ?? void 0);
523
- const nullableNumber = z.number().nullish().transform((value) => value ?? void 0);
524
- const sponsorSchema = z.object({
525
- name: nullableString,
526
- githubId: z.string(),
527
- avatarUrl: z.string(),
528
- websiteUrl: nullableString,
529
- githubUrl: z.string(),
530
- tierName: nullableString,
531
- sinceWhen: z.string(),
532
- transactionCount: z.number(),
533
- totalProcessedAmount: nullableNumber,
534
- formattedAmount: nullableString
535
- });
536
- const sponsorSummarySchema = z.object({
537
- total_sponsors: z.number(),
538
- total_lifetime_amount: z.number(),
539
- total_current_monthly: z.number(),
540
- special_sponsors: z.number(),
541
- current_sponsors: z.number(),
542
- past_sponsors: z.number(),
543
- backers: z.number(),
544
- top_sponsor: z.object({
545
- name: z.string(),
546
- amount: z.number()
547
- }).nullish().transform((value) => value ?? void 0)
548
- });
549
- const sponsorEntrySchema = z.object({
550
- generated_at: z.string(),
551
- summary: sponsorSummarySchema,
552
- specialSponsors: z.array(sponsorSchema),
553
- sponsors: z.array(sponsorSchema),
554
- pastSponsors: z.array(sponsorSchema),
555
- backers: z.array(sponsorSchema)
556
- });
557
- async function fetchSponsors(url = SPONSORS_JSON_URL) {
558
- return fetchSponsorsData({
559
- url,
560
- withSpinner: true
561
- });
562
- }
563
- async function fetchSponsorsQuietly({ url = SPONSORS_JSON_URL, timeoutMs = 1500 } = {}) {
564
- return fetchSponsorsData({
565
- url,
566
- withSpinner: false,
567
- timeoutMs
568
- });
569
- }
570
- function displaySponsors(sponsors) {
571
- const { total_sponsors } = sponsors.summary;
572
- if (total_sponsors === 0) {
573
- log.info("No sponsors found. You can be the first one! ✨");
574
- outro(pc.cyan(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
575
- return;
576
- }
577
- displaySponsorsBox(sponsors);
578
- if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
579
- outro(pc.magenta(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
580
- }
581
- function displaySponsorsBox(sponsors) {
582
- if (sponsors.specialSponsors.length === 0) return;
583
- let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
584
- sponsors.specialSponsors.forEach((sponsor, idx) => {
585
- const displayName = sponsor.name ?? sponsor.githubId;
586
- const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
587
- output += `${pc.green(`• ${displayName}`)}${tier}\n`;
588
- output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
589
- const website = sponsor.websiteUrl ?? sponsor.githubUrl;
590
- if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
591
- if (idx < sponsors.specialSponsors.length - 1) output += "\n";
592
- });
593
- cliConsola.box(output);
594
- }
595
- function formatPostInstallSpecialSponsorsSection(sponsors) {
596
- if (sponsors.specialSponsors.length === 0) return "";
597
- const wrappedSponsorLines = wrapSponsorTokens(sponsors.specialSponsors.map((sponsor) => {
598
- return `• ${sponsor.name ?? sponsor.githubId}`;
599
- }), getPostInstallSponsorLineWidth());
600
- let output = `${pc.bold("Special sponsors")}\n`;
601
- wrappedSponsorLines.forEach((line) => {
602
- output += `${line}\n`;
603
- });
604
- return output.trimEnd();
605
- }
606
- function getPostInstallSponsorLineWidth() {
607
- const terminalWidth = process.stdout.columns;
608
- if (!terminalWidth || terminalWidth <= 0) return 72;
609
- const availableWidth = Math.max(8, terminalWidth - 24);
610
- return Math.min(72, availableWidth);
611
- }
612
- function wrapSponsorTokens(tokens, maxLineWidth) {
613
- const lines = [];
614
- const separator = " ";
615
- let currentLine = "";
616
- tokens.forEach((token) => {
617
- const candidateLine = currentLine ? `${currentLine}${separator}${token}` : token;
618
- if (candidateLine.length <= maxLineWidth) {
619
- currentLine = candidateLine;
620
- return;
621
- }
622
- if (currentLine) {
623
- lines.push(currentLine);
624
- currentLine = token;
625
- return;
626
- }
627
- lines.push(token);
628
- });
629
- if (currentLine) lines.push(currentLine);
630
- return lines;
631
- }
632
- async function fetchSponsorsData({ url = SPONSORS_JSON_URL, withSpinner = false, timeoutMs }) {
633
- const s = withSpinner ? spinner() : null;
634
- if (s) s.start("Fetching sponsors…");
635
- const controller = timeoutMs ? new AbortController() : null;
636
- const timeout = timeoutMs ? setTimeout(() => {
637
- controller?.abort();
638
- }, timeoutMs) : null;
639
- try {
640
- const response = await fetch(url, controller ? { signal: controller.signal } : void 0);
641
- if (!response.ok) {
642
- const message = `Failed to fetch sponsors: ${response.statusText || String(response.status)}`;
643
- if (s) s.stop(pc.red(message));
644
- return Result.err(new CLIError({ message }));
645
- }
646
- const rawSponsors = await response.json();
647
- const parseResult = sponsorEntrySchema.safeParse(rawSponsors);
648
- if (!parseResult.success) {
649
- const message = `Failed to fetch sponsors: invalid response format at "${parseResult.error.issues[0]?.path?.join(".") || "unknown"}"`;
650
- if (s) s.stop(pc.red(message));
651
- return Result.err(new CLIError({
652
- message,
653
- cause: parseResult.error
654
- }));
655
- }
656
- if (s) s.stop("Sponsors fetched successfully!");
657
- return Result.ok(parseResult.data);
658
- } catch (error) {
659
- const normalizedError = normalizeSponsorFetchError(error);
660
- if (s) s.stop(pc.red(normalizedError.message));
661
- return Result.err(normalizedError);
662
- } finally {
663
- if (timeout) clearTimeout(timeout);
664
- }
665
- }
666
- function normalizeSponsorFetchError(error) {
667
- if (error instanceof Error && error.name === "AbortError") return new CLIError({
668
- message: "Failed to fetch sponsors: request timed out",
669
- cause: error
670
- });
671
- if (CLIError.is(error)) return error;
672
- if (error instanceof Error) return new CLIError({
673
- message: error.message.startsWith("Failed to fetch sponsors:") ? error.message : `Failed to fetch sponsors: ${error.message}`,
674
- cause: error
675
- });
676
- return new CLIError({
677
- message: `Failed to fetch sponsors: ${String(error)}`,
678
- cause: error
679
- });
680
- }
681
- //#endregion
682
546
  //#region src/commands/meta.ts
683
547
  const DOCS_URL = "https://kubojs.dev/docs";
684
548
  const BUILDER_URL = "https://kubojs.dev/new";
@@ -686,20 +550,9 @@ async function openExternalUrl(url, successMessage) {
686
550
  if ((await Result.tryPromise({
687
551
  try: () => openUrl(url),
688
552
  catch: () => null
689
- })).isOk()) log.success(pc.blue(successMessage));
553
+ })).isOk()) log.success(cliColors.signal(successMessage));
690
554
  else log.message(`Please visit ${url}`);
691
555
  }
692
- async function showSponsorsCommand() {
693
- renderTitle();
694
- intro(pc.magenta("I dont know Sponsors"));
695
- const sponsorsResult = await fetchSponsors();
696
- if (sponsorsResult.isErr()) {
697
- displayError(sponsorsResult.error);
698
- process.exit(1);
699
- return;
700
- }
701
- displaySponsors(sponsorsResult.value);
702
- }
703
556
  async function openDocsCommand() {
704
557
  await openExternalUrl(DOCS_URL, "Opened docs in your default browser.");
705
558
  }
@@ -725,12 +578,32 @@ const WEB_FRAMEWORKS = [
725
578
  ];
726
579
  //#endregion
727
580
  //#region src/utils/compatibility-rules.ts
728
- const TASK_RUNNER_ADDONS$1 = [
729
- "turborepo",
730
- "nx",
731
- "vite-plus"
732
- ];
581
+ const TASK_RUNNER_ADDONS$1 = ["turborepo", "vite-plus"];
582
+ /** Mutually exclusive code-quality linters (one slot). */
583
+ const LINTER_ADDONS = ["biome", "oxlint"];
733
584
  const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
585
+ function isLinterAddon(addon) {
586
+ return LINTER_ADDONS.includes(addon);
587
+ }
588
+ /**
589
+ * When adding a code-quality linter, drop sibling linters so only one remains.
590
+ * Last-selected linter wins. Task runners stay validation-only (reject on dual).
591
+ */
592
+ function mergeAddonsExclusive(existingAddons, addonsToAdd) {
593
+ const removed = /* @__PURE__ */ new Set();
594
+ let result = [...existingAddons];
595
+ for (const addon of addonsToAdd) {
596
+ if (LINTER_ADDONS.includes(addon)) {
597
+ for (const current of result) if (LINTER_ADDONS.includes(current) && current !== addon) removed.add(current);
598
+ result = result.filter((current) => !LINTER_ADDONS.includes(current) || current === addon);
599
+ }
600
+ if (!result.includes(addon) && addon !== "none") result.push(addon);
601
+ }
602
+ return {
603
+ updatedAddons: result,
604
+ removedAddons: [...removed]
605
+ };
606
+ }
734
607
  const TAURI_STATIC_EXPORT_FRONTENDS = ["next", "tanstack-start"];
735
608
  const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
736
609
  "nuxt",
@@ -753,6 +626,13 @@ function validationErr$1(message) {
753
626
  function isWebFrontend(value) {
754
627
  return WEB_FRAMEWORKS.includes(value);
755
628
  }
629
+ function isPlaywrightAllowed(frontends = []) {
630
+ return frontends.some((frontend) => isWebFrontend(frontend));
631
+ }
632
+ function validateTestingAgainstFrontends(testing = [], frontends = []) {
633
+ if (testing.includes("playwright") && !isPlaywrightAllowed(frontends)) return validationErr$1("playwright testing requires a web frontend");
634
+ return Result.ok(void 0);
635
+ }
756
636
  function splitFrontends(values = []) {
757
637
  return {
758
638
  web: values.filter((f) => isWebFrontend(f)),
@@ -772,23 +652,6 @@ const FULLSTACK_FRONTENDS$1 = [
772
652
  "svelte",
773
653
  "astro"
774
654
  ];
775
- const EVLOG_SERVER_BACKENDS = [
776
- "hono",
777
- "express",
778
- "fastify",
779
- "elysia"
780
- ];
781
- const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
782
- 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.";
783
- function supportsEvlogAddon(frontend = [], backend, _runtime) {
784
- if (!backend) return true;
785
- if (EVLOG_SERVER_BACKENDS.includes(backend)) return true;
786
- if (backend === "self") {
787
- if (frontend.length === 0) return true;
788
- return frontend.some((f) => EVLOG_FULLSTACK_FRONTENDS.includes(f));
789
- }
790
- return false;
791
- }
792
655
  function validateSelfBackendCompatibility(providedFlags, options, config) {
793
656
  const backend = config.backend || options.backend;
794
657
  const frontends = config.frontend || options.frontend || [];
@@ -834,7 +697,7 @@ function isFrontendAllowedWithBackend(frontend, backend, auth) {
834
697
  function supportsConvexBetterAuth(frontends = []) {
835
698
  return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.includes(frontend));
836
699
  }
837
- function allowedApisForFrontends(frontends = []) {
700
+ function allowedApisForFrontends(frontends = [], backend) {
838
701
  const includesNuxt = frontends.includes("nuxt");
839
702
  const includesSvelte = frontends.includes("svelte");
840
703
  const includesSolid = frontends.includes("solid");
@@ -844,9 +707,17 @@ function allowedApisForFrontends(frontends = []) {
844
707
  "orpc",
845
708
  "none"
846
709
  ];
847
- 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"];
848
716
  return base;
849
717
  }
718
+ function isApiCompatibleWithBackend(api, backend) {
719
+ return api !== "orval" || backend === "hono";
720
+ }
850
721
  function isExampleTodoAllowed(backend, database, api) {
851
722
  if (backend === "convex") return true;
852
723
  if (database === "none" || api === "none") return false;
@@ -906,10 +777,6 @@ function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backe
906
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).`);
907
778
  }
908
779
  function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
909
- if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
910
- isCompatible: false,
911
- reason: evlogCompatibilityMessage
912
- };
913
780
  if (STATIC_DESKTOP_ADDONS.includes(addon) && auth === "clerk" && frontend.includes("react-router")) return {
914
781
  isCompatible: false,
915
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.`
@@ -922,9 +789,13 @@ function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
922
789
  isCompatible: false,
923
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."
924
791
  };
792
+ if (!(addon in ADDON_COMPATIBILITY)) return {
793
+ isCompatible: false,
794
+ reason: `${addon} is not a supported addon`
795
+ };
925
796
  const compatibleFrontends = ADDON_COMPATIBILITY[addon];
926
797
  if (compatibleFrontends.length > 0) {
927
- if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
798
+ if (!frontend.some((f) => compatibleFrontends.some((compatibleFrontend) => compatibleFrontend === f))) return {
928
799
  isCompatible: false,
929
800
  reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
930
801
  };
@@ -941,7 +812,8 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
941
812
  });
942
813
  }
943
814
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
944
- 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.");
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.");
945
817
  for (const addon of addons) {
946
818
  if (addon === "none") continue;
947
819
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -952,18 +824,31 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backe
952
824
  function validateAddonsAgainstConfig(addons = [], config) {
953
825
  return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
954
826
  }
827
+ function validateCommunicationCompatibility(communication, backend) {
828
+ if (!communication || communication === "none") return Result.ok(void 0);
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.");
835
+ return Result.ok(void 0);
836
+ }
955
837
  function validatePaymentsCompatibility(payments, auth, backend, frontends = [], orm, database) {
956
- if (!payments || payments === "none") return Result.ok(void 0);
957
- if (payments === "abacatepay") {
958
- if (backend === "convex") return validationErr$1("AbacatePay payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.");
959
- 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.");
960
- if (frontends.length > 0 && frontends.every((frontend) => [
961
- "native-bare",
962
- "native-uniwind",
963
- "native-unistyles"
964
- ].includes(frontend))) return validationErr$1("AbacatePay payments is not compatible with native-only frontends. Please add a supported web frontend or use '--payments none'.");
965
- 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'.");
966
- 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.");
967
852
  }
968
853
  return Result.ok(void 0);
969
854
  }
@@ -998,12 +883,12 @@ function isGoBack(value) {
998
883
  * These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
999
884
  */
1000
885
  const unicode = process.platform !== "win32";
1001
- const S_STEP_ACTIVE = unicode ? "◆" : "*";
886
+ const S_STEP_ACTIVE$1 = unicode ? "◆" : "*";
1002
887
  const S_STEP_CANCEL = unicode ? "■" : "x";
1003
888
  const S_STEP_ERROR = unicode ? "▲" : "x";
1004
889
  const S_STEP_SUBMIT = unicode ? "◇" : "o";
1005
- const S_BAR = unicode ? "│" : "|";
1006
- const S_BAR_END = unicode ? "└" : "—";
890
+ const S_BAR$1 = unicode ? "│" : "|";
891
+ const S_BAR_END$1 = unicode ? "└" : "—";
1007
892
  const S_RADIO_ACTIVE = unicode ? "●" : ">";
1008
893
  const S_RADIO_INACTIVE = unicode ? "○" : " ";
1009
894
  const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
@@ -1012,10 +897,10 @@ const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
1012
897
  function symbol(state) {
1013
898
  switch (state) {
1014
899
  case "initial":
1015
- case "active": return pc.cyan(S_STEP_ACTIVE);
900
+ case "active": return cliColors.orange(S_STEP_ACTIVE$1);
1016
901
  case "cancel": return pc.red(S_STEP_CANCEL);
1017
902
  case "error": return pc.yellow(S_STEP_ERROR);
1018
- case "submit": return pc.green(S_STEP_SUBMIT);
903
+ case "submit": return cliColors.signal(S_STEP_SUBMIT);
1019
904
  }
1020
905
  }
1021
906
  const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
@@ -1050,7 +935,7 @@ async function navigableSelect(opts) {
1050
935
  switch (state) {
1051
936
  case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
1052
937
  case "selected": return `${pc.dim(label)}`;
1053
- 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})`)}` : ""}`;
1054
939
  case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
1055
940
  default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
1056
941
  }
@@ -1059,14 +944,14 @@ async function navigableSelect(opts) {
1059
944
  options: opts.options,
1060
945
  initialValue: opts.initialValue,
1061
946
  render() {
1062
- 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`;
1063
948
  switch (this.state) {
1064
- case "submit": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
1065
- 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)}`;
1066
951
  default: {
1067
- const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
1068
- const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
1069
- 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`;
1070
955
  }
1071
956
  }
1072
957
  }
@@ -1077,10 +962,10 @@ async function navigableMultiselect(opts) {
1077
962
  const opt = (option, state) => {
1078
963
  const label = option.label ?? String(option.value);
1079
964
  if (state === "disabled") return `${pc.gray(S_CHECKBOX_INACTIVE)} ${pc.strikethrough(pc.gray(label))}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
1080
- if (state === "active") return `${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1081
- 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})`)}` : ""}`;
1082
967
  if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
1083
- 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})`)}` : ""}`;
1084
969
  if (state === "submitted") return `${pc.dim(label)}`;
1085
970
  return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
1086
971
  };
@@ -1093,7 +978,7 @@ async function navigableMultiselect(opts) {
1093
978
  return normalizeValidationMessage(opts.validate?.(selected));
1094
979
  },
1095
980
  render() {
1096
- 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`;
1097
982
  const value = this.value ?? [];
1098
983
  const styleOption = (option, active) => {
1099
984
  if (option.disabled) return opt(option, "disabled");
@@ -1105,21 +990,21 @@ async function navigableMultiselect(opts) {
1105
990
  switch (this.state) {
1106
991
  case "submit": {
1107
992
  const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
1108
- return `${title}${pc.gray(S_BAR)} ${submitText}`;
993
+ return `${title}${pc.gray(S_BAR$1)} ${submitText}`;
1109
994
  }
1110
995
  case "cancel": {
1111
996
  const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
1112
- 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)}`;
1113
998
  }
1114
999
  case "error": {
1115
- const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1116
- const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
1117
- 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`;
1118
1003
  }
1119
1004
  default: {
1120
- const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
1121
- const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
1122
- 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`;
1123
1008
  }
1124
1009
  }
1125
1010
  }
@@ -1133,14 +1018,14 @@ async function navigableConfirm(opts) {
1133
1018
  inactive,
1134
1019
  initialValue: opts.initialValue ?? true,
1135
1020
  render() {
1136
- 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`;
1137
1022
  const value = this.value ? active : inactive;
1138
1023
  switch (this.state) {
1139
- case "submit": return `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
1140
- 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)}`;
1141
1026
  default: {
1142
- const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
1143
- 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`;
1144
1029
  }
1145
1030
  }
1146
1031
  }
@@ -1153,16 +1038,16 @@ async function navigableGroupMultiselect(opts) {
1153
1038
  const isItem = typeof option.group === "string";
1154
1039
  const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
1155
1040
  const isLast = isItem && next && next.group === true;
1156
- const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
1157
- if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1158
- if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
1159
- 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)}`;
1160
1045
  if (state === "selected") {
1161
- const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
1046
+ const selectedCheckbox = isItem ? cliColors.bright(S_CHECKBOX_SELECTED) : "";
1162
1047
  return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1163
1048
  }
1164
1049
  if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
1165
- 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})`)}` : ""}`;
1166
1051
  if (state === "submitted") return `${pc.dim(label)}`;
1167
1052
  const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
1168
1053
  return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
@@ -1177,20 +1062,20 @@ async function navigableGroupMultiselect(opts) {
1177
1062
  return normalizeValidationMessage(opts.validate?.(selected));
1178
1063
  },
1179
1064
  render() {
1180
- 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`;
1181
1066
  const value = this.value ?? [];
1182
1067
  switch (this.state) {
1183
1068
  case "submit": {
1184
1069
  const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
1185
1070
  const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
1186
- return `${title}${pc.gray(S_BAR)}${optionsText}`;
1071
+ return `${title}${pc.gray(S_BAR$1)}${optionsText}`;
1187
1072
  }
1188
1073
  case "cancel": {
1189
1074
  const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
1190
- 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)}` : ""}`;
1191
1076
  }
1192
1077
  case "error": {
1193
- 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");
1194
1079
  const optionsText = this.options.map((option, i, options) => {
1195
1080
  const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1196
1081
  const active = i === this.cursor;
@@ -1198,8 +1083,8 @@ async function navigableGroupMultiselect(opts) {
1198
1083
  if (active && selected) return opt(option, "active-selected", options);
1199
1084
  if (selected) return opt(option, "selected", options);
1200
1085
  return opt(option, active ? "active" : "inactive", options);
1201
- }).join(`\n${pc.yellow(S_BAR)} `);
1202
- 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`;
1203
1088
  }
1204
1089
  default: {
1205
1090
  const optionsText = this.options.map((option, i, options) => {
@@ -1212,10 +1097,10 @@ async function navigableGroupMultiselect(opts) {
1212
1097
  else if (selected) optionText = opt(option, "selected", options);
1213
1098
  else optionText = opt(option, active ? "active" : "inactive", options);
1214
1099
  return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
1215
- }).join(`\n${pc.cyan(S_BAR)}`);
1100
+ }).join(`\n${cliColors.signal(S_BAR$1)}`);
1216
1101
  const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
1217
- const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
1218
- 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`;
1219
1104
  }
1220
1105
  }
1221
1106
  }
@@ -1235,10 +1120,6 @@ function getAddonDisplay(addon) {
1235
1120
  label = "Turborepo";
1236
1121
  hint = "High-performance build system";
1237
1122
  break;
1238
- case "nx":
1239
- label = "Nx";
1240
- hint = "Smart monorepo orchestration and task graph";
1241
- break;
1242
1123
  case "vite-plus":
1243
1124
  label = "Vite+";
1244
1125
  hint = "Unified Vite toolchain and workspace task runner";
@@ -1263,10 +1144,6 @@ function getAddonDisplay(addon) {
1263
1144
  label = "Oxlint";
1264
1145
  hint = "Oxlint + Oxfmt (linting & formatting)";
1265
1146
  break;
1266
- case "ultracite":
1267
- label = "Ultracite";
1268
- hint = "Zero-config preset for Biome or Oxlint with AI integration";
1269
- break;
1270
1147
  case "lefthook":
1271
1148
  label = "Lefthook";
1272
1149
  hint = "Fast and powerful Git hooks manager";
@@ -1275,33 +1152,21 @@ function getAddonDisplay(addon) {
1275
1152
  label = "Husky";
1276
1153
  hint = "Modern native Git hooks made easy";
1277
1154
  break;
1278
- case "starlight":
1279
- label = "Starlight";
1280
- hint = "Build stellar docs with astro";
1281
- break;
1282
- case "fumadocs":
1283
- label = "Fumadocs";
1284
- hint = "Build excellent documentation site";
1285
- break;
1286
1155
  case "opentui":
1287
1156
  label = "OpenTUI";
1288
1157
  hint = "Build terminal user interfaces";
1289
1158
  break;
1290
- case "wxt":
1291
- label = "WXT";
1292
- hint = "Build browser extensions";
1293
- break;
1294
1159
  case "skills":
1295
1160
  label = "Skills";
1296
1161
  hint = "AI coding agent skills for your stack";
1297
1162
  break;
1298
1163
  case "mcp":
1299
1164
  label = "MCP";
1300
- hint = "Install MCP servers, including I dont know, via add-mcp";
1165
+ hint = "Install MCP servers, including kubojs, via add-mcp";
1301
1166
  break;
1302
- case "evlog":
1303
- label = "evlog";
1304
- 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";
1305
1170
  break;
1306
1171
  default:
1307
1172
  label = addon;
@@ -1313,27 +1178,20 @@ function getAddonDisplay(addon) {
1313
1178
  };
1314
1179
  }
1315
1180
  const ADDON_GROUPS = {
1316
- "Monorepo & Tasks": [
1317
- "turborepo",
1318
- "nx",
1319
- "vite-plus"
1320
- ],
1181
+ "Monorepo & Tasks": ["turborepo", "vite-plus"],
1321
1182
  "Code Quality": [
1322
1183
  "biome",
1323
1184
  "oxlint",
1324
- "ultracite",
1325
1185
  "husky",
1326
1186
  "lefthook"
1327
1187
  ],
1328
- Documentation: ["starlight", "fumadocs"],
1329
1188
  "Platform Extensions": [
1330
1189
  "pwa",
1331
1190
  "tauri",
1332
1191
  "electrobun",
1333
1192
  "opentui",
1334
- "wxt"
1193
+ "s3-storage"
1335
1194
  ],
1336
- Observability: ["evlog"],
1337
1195
  "AI & Agent Tools": ["skills", "mcp"]
1338
1196
  };
1339
1197
  function createGroupedOptions() {
@@ -1358,11 +1216,8 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1358
1216
  });
1359
1217
  }
1360
1218
  function validateAddonSelection(selected) {
1361
- if ((selected?.filter((addon) => [
1362
- "turborepo",
1363
- "nx",
1364
- "vite-plus"
1365
- ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, 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.";
1366
1221
  }
1367
1222
  async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1368
1223
  if (addons !== void 0) return addons;
@@ -1387,7 +1242,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previo
1387
1242
  required: false,
1388
1243
  validate: validateAddonSelection
1389
1244
  });
1390
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1245
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1391
1246
  return response;
1392
1247
  }
1393
1248
  async function getAddonsToAdd(config) {
@@ -1410,18 +1265,38 @@ async function getAddonsToAdd(config) {
1410
1265
  required: false,
1411
1266
  validate: validateAddonSelection
1412
1267
  });
1413
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1268
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1414
1269
  return response;
1415
1270
  }
1416
1271
  //#endregion
1417
- //#region src/utils/bts-config.ts
1418
- const BTS_CONFIG_FILE = "bts.jsonc";
1272
+ //#region src/utils/input-hardening.ts
1273
+ function hasControlCharacters(value) {
1274
+ for (const char of value) {
1275
+ const charCode = char.charCodeAt(0);
1276
+ if (charCode < 32 || charCode === 127) return true;
1277
+ }
1278
+ return false;
1279
+ }
1280
+ function hardeningError(field, value, message) {
1281
+ return Result.err(new ValidationError({
1282
+ field,
1283
+ value,
1284
+ message
1285
+ }));
1286
+ }
1287
+ function validateAgentSafePathInput(value, field) {
1288
+ if (hasControlCharacters(value)) return hardeningError(field, value, `Invalid ${field}: control characters are not allowed.`);
1289
+ return Result.ok(void 0);
1290
+ }
1291
+ //#endregion
1292
+ //#region src/utils/kubojs-config.ts
1293
+ const KUBOJS_CONFIG_FILE = "kubojs.jsonrc";
1419
1294
  /**
1420
- * Reads the BTS configuration file from the project directory.
1295
+ * Reads the kubojs configuration file from the project directory.
1421
1296
  */
1422
- async function readBtsConfig(projectDir) {
1297
+ async function readKubojsConfig(projectDir) {
1423
1298
  try {
1424
- const configPath = path.join(projectDir, BTS_CONFIG_FILE);
1299
+ const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1425
1300
  if (!await fs.pathExists(configPath)) return null;
1426
1301
  return parse(await fs.readFile(configPath, "utf-8"));
1427
1302
  } catch {
@@ -1429,11 +1304,11 @@ async function readBtsConfig(projectDir) {
1429
1304
  }
1430
1305
  }
1431
1306
  /**
1432
- * Updates specific fields in the BTS configuration file.
1307
+ * Updates specific fields in the kubojs configuration file.
1433
1308
  */
1434
- async function updateBtsConfig(projectDir, updates) {
1309
+ async function updateKubojsConfig(projectDir, updates) {
1435
1310
  try {
1436
- const configPath = path.join(projectDir, BTS_CONFIG_FILE);
1311
+ const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1437
1312
  if (!await fs.pathExists(configPath)) return;
1438
1313
  let content = await fs.readFile(configPath, "utf-8");
1439
1314
  for (const [key, value] of Object.entries(updates)) {
@@ -1444,26 +1319,6 @@ async function updateBtsConfig(projectDir, updates) {
1444
1319
  } catch {}
1445
1320
  }
1446
1321
  //#endregion
1447
- //#region src/utils/input-hardening.ts
1448
- function hasControlCharacters(value) {
1449
- for (const char of value) {
1450
- const charCode = char.charCodeAt(0);
1451
- if (charCode < 32 || charCode === 127) return true;
1452
- }
1453
- return false;
1454
- }
1455
- function hardeningError(field, value, message) {
1456
- return Result.err(new ValidationError({
1457
- field,
1458
- value,
1459
- message
1460
- }));
1461
- }
1462
- function validateAgentSafePathInput(value, field) {
1463
- if (hasControlCharacters(value)) return hardeningError(field, value, `Invalid ${field}: control characters are not allowed.`);
1464
- return Result.ok(void 0);
1465
- }
1466
- //#endregion
1467
1322
  //#region src/utils/add-package-deps.ts
1468
1323
  const addPackageDependency = async (opts) => {
1469
1324
  const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
@@ -1486,515 +1341,6 @@ const addPackageDependency = async (opts) => {
1486
1341
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1487
1342
  };
1488
1343
  //#endregion
1489
- //#region src/helpers/addons/evlog-setup.ts
1490
- const evlogBackends = [
1491
- "hono",
1492
- "express",
1493
- "fastify",
1494
- "elysia"
1495
- ];
1496
- const evlogWebFrontends = [
1497
- "next",
1498
- "nuxt",
1499
- "svelte",
1500
- "tanstack-start",
1501
- "astro"
1502
- ];
1503
- function isEvlogBackend(backend) {
1504
- return evlogBackends.includes(backend);
1505
- }
1506
- function getEvlogWebFrontend(frontends) {
1507
- return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
1508
- }
1509
- function shouldIdentifyWebAuth(config) {
1510
- return config.auth === "better-auth" && config.backend === "self";
1511
- }
1512
- function prependMissingImports(content, imports) {
1513
- const missingImports = imports.filter((line) => !content.includes(line));
1514
- if (missingImports.length === 0) return content;
1515
- const importBlock = `${missingImports.join("\n")}\n`;
1516
- const referenceMatch = content.match(/^(?:\/\/\/ <reference[^\n]*>\n)+/);
1517
- if (referenceMatch) return `${referenceMatch[0]}${importBlock}${content.slice(referenceMatch[0].length)}`;
1518
- return `${importBlock}${content}`;
1519
- }
1520
- function addNamedImport(content, moduleName, names) {
1521
- const importRegex = new RegExp(`import \\{([^}]+)\\} from "${moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";`);
1522
- const match = content.match(importRegex);
1523
- if (!match) return prependMissingImports(content, [`import { ${names.join(", ")} } from "${moduleName}";`]);
1524
- const nextNames = [...match[1].split(",").map((name) => name.trim()).filter(Boolean)];
1525
- for (const name of names) if (!nextNames.includes(name)) nextNames.push(name);
1526
- return content.replace(match[0], `import { ${nextNames.join(", ")} } from "${moduleName}";`);
1527
- }
1528
- function insertBeforeOnce(content, marker, snippet, alreadyPresent) {
1529
- if (content.includes(alreadyPresent)) return content;
1530
- if (!content.includes(marker)) return content;
1531
- return content.replace(marker, `${snippet}${marker}`);
1532
- }
1533
- function insertAfterOnce(content, marker, snippet, alreadyPresent) {
1534
- if (content.includes(alreadyPresent)) return content;
1535
- if (!content.includes(marker)) return content;
1536
- return content.replace(marker, `${marker}${snippet}`);
1537
- }
1538
- async function writeFileIfChanged(filePath, content) {
1539
- if ((await fs.pathExists(filePath) ? await fs.readFile(filePath, "utf-8") : void 0) === content) return;
1540
- await fs.ensureDir(path.dirname(filePath));
1541
- await fs.writeFile(filePath, content);
1542
- }
1543
- async function updateFileIfExists(filePath, update) {
1544
- if (!await fs.pathExists(filePath)) return;
1545
- const content = await fs.readFile(filePath, "utf-8");
1546
- const nextContent = update(content);
1547
- if (nextContent !== content) await fs.writeFile(filePath, nextContent);
1548
- }
1549
- function usesCreateAuthFactory(config) {
1550
- return config.runtime === "workers" || config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare";
1551
- }
1552
- function getAuthImportLine(config) {
1553
- return usesCreateAuthFactory(config) ? `import { createAuth } from "@${config.projectName}/auth";` : `import { auth } from "@${config.projectName}/auth";`;
1554
- }
1555
- function getAuthExpression(config) {
1556
- return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
1557
- }
1558
- function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
1559
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1560
- const usesAuthFactory = authExpression.endsWith("()");
1561
- const evlogAuthExpression = `${authExpression} as BetterAuthInstance`;
1562
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1563
- const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
1564
- const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
1565
- if (backend === "hono") {
1566
- nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
1567
- 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\")");
1568
- }
1569
- if (backend === "express") {
1570
- nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
1571
- 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");
1572
- }
1573
- if (backend === "fastify") {
1574
- nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
1575
- nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
1576
- return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
1577
- }
1578
- nextContent = insertBeforeOnce(nextContent, "new Elysia", identifySnippet, "createAuthMiddleware(");
1579
- 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");
1580
- }
1581
- function addEvlogServerSetup(content, backend, serviceName) {
1582
- const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
1583
- if (backend === "hono") {
1584
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
1585
- nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
1586
- nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
1587
- nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
1588
- return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
1589
- }
1590
- if (backend === "express") {
1591
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
1592
- nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
1593
- return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
1594
- }
1595
- if (backend === "fastify") {
1596
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
1597
- nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
1598
- return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
1599
- }
1600
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
1601
- nextContent = insertBeforeOnce(nextContent, "new Elysia", initSnippet, "initLogger({");
1602
- for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
1603
- return nextContent;
1604
- }
1605
- function addNuxtEvlogSetup(content, serviceName) {
1606
- let nextContent = content;
1607
- if (!nextContent.includes("\"evlog/nuxt\"") && !nextContent.includes("'evlog/nuxt'")) nextContent = nextContent.replace(/modules:\s*\[/, (match) => `${match}\n "evlog/nuxt",`);
1608
- if (!nextContent.includes("evlog:")) nextContent = nextContent.replace(/\n\}\)\s*$/, (match) => {
1609
- const contentBeforeConfigClose = nextContent.slice(0, -match.length);
1610
- return `${!/[,{]\s*$/.test(contentBeforeConfigClose) ? "," : ""}\n evlog: {\n env: { service: "${serviceName}" },\n },\n})`;
1611
- });
1612
- return nextContent;
1613
- }
1614
- function addSvelteViteEvlogSetup(content, serviceName) {
1615
- let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
1616
- if (nextContent.includes("evlog({")) return nextContent;
1617
- return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
1618
- }
1619
- function addSvelteHooksEvlogSetup(content) {
1620
- let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
1621
- if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
1622
- if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
1623
- return nextContent;
1624
- }
1625
- nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
1626
- if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
1627
- nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
1628
- if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
1629
- return nextContent;
1630
- }
1631
- function addSvelteLocalsType(content) {
1632
- let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1633
- if (nextContent.includes("log: RequestLogger")) return nextContent;
1634
- if (nextContent.includes("// interface Locals {}")) return nextContent.replace("// interface Locals {}", "interface Locals {\n log: RequestLogger;\n }");
1635
- return nextContent.replace("namespace App {", "namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1636
- }
1637
- function addTanstackStartRootEvlogSetup(content) {
1638
- let nextContent = prependMissingImports(content, ["import { createMiddleware } from \"@tanstack/react-start\";", "import { evlogErrorHandler } from \"evlog/nitro/v3\";"]);
1639
- const middlewareEntry = "createMiddleware().server(evlogErrorHandler)";
1640
- if (nextContent.includes(`middleware: [${middlewareEntry}]`)) return nextContent;
1641
- if (nextContent.includes("middleware: [")) return nextContent.replace("middleware: [", `middleware: [${middlewareEntry}, `);
1642
- if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
1643
- return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
1644
- }
1645
- function addAstroMiddlewareEvlogSetup(content, serviceName) {
1646
- let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
1647
- const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
1648
- nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
1649
- if (nextContent.includes("createRequestLogger({")) return nextContent;
1650
- const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
1651
- if (nextContent.includes(contextMarker)) {
1652
- 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({");
1653
- return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1654
- }
1655
- const localsMarker = "export const onRequest = defineMiddleware(async ({ request, locals }, next) => {";
1656
- if (nextContent.includes(localsMarker)) {
1657
- 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({");
1658
- return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1659
- }
1660
- return nextContent;
1661
- }
1662
- function addAstroLocalsType(content) {
1663
- let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1664
- if (nextContent.includes("log: RequestLogger")) return nextContent;
1665
- if (nextContent.includes("interface Locals {")) return nextContent.replace("interface Locals {", "interface Locals {\n log: RequestLogger;");
1666
- if (nextContent.includes("declare namespace App {")) return nextContent.replace("declare namespace App {", "declare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1667
- return `${nextContent.trimEnd()}\n\ndeclare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n}\n`;
1668
- }
1669
- function addNextRouteWrappers(content) {
1670
- let nextContent = prependMissingImports(content, ["import { withEvlog } from \"@/lib/evlog\";"]);
1671
- if (nextContent.includes("withEvlog(handler)") || nextContent.includes("withEvlog(handleRequest)")) return nextContent;
1672
- nextContent = nextContent.replace("export { handler as GET, handler as POST };", "export const GET = withEvlog(handler);\nexport const POST = withEvlog(handler);");
1673
- for (const method of [
1674
- "GET",
1675
- "POST",
1676
- "PUT",
1677
- "PATCH",
1678
- "DELETE"
1679
- ]) nextContent = nextContent.replace(`export const ${method} = handleRequest;`, `export const ${method} = withEvlog(handleRequest);`);
1680
- return nextContent;
1681
- }
1682
- function addNextAiEvlogSetup(content) {
1683
- let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
1684
- if (!nextContent.includes("withEvlog(async (req: Request)")) {
1685
- nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
1686
- if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
1687
- }
1688
- return nextContent;
1689
- }
1690
- function addNextBetterAuthToRoute(content) {
1691
- let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
1692
- nextContent = nextContent.replace("function handler(req:", "async function handler(req:");
1693
- for (const marker of [
1694
- "async function handler(req: NextRequest) {",
1695
- "async function handleRequest(req: NextRequest) {",
1696
- "export const POST = withEvlog(async (req: Request) => {"
1697
- ]) nextContent = insertAfterOnce(nextContent, marker, "\n await identifyEvlogUser(req);", "identifyEvlogUser(req)");
1698
- return nextContent;
1699
- }
1700
- function addSvelteBetterAuthEvlogSetup(content, config) {
1701
- if (!content.includes("authHandle") || content.includes("evlogAuthHandle")) return content;
1702
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1703
- if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1704
- if (usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" && !nextContent.includes(`@${config.projectName}/env/server`)) nextContent = prependMissingImports(nextContent, [`import { env as localEnv } from "@${config.projectName}/env/server";`]);
1705
- const authExpression = getAuthExpression(config);
1706
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1707
- 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`;
1708
- nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
1709
- return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
1710
- }
1711
- function addAstroBetterAuthEvlogSetup(content, config) {
1712
- if (content.includes("createAuthMiddleware(")) return content;
1713
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1714
- if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1715
- const authExpression = getAuthExpression(config);
1716
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1717
- const usesFactory = usesCreateAuthFactory(config);
1718
- if (!usesFactory) nextContent = insertBeforeOnce(nextContent, "export const onRequest", `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\n`, "const identifyUser = createAuthMiddleware(");
1719
- for (const marker of ["context.locals.log = log;", "locals.log = log;"]) {
1720
- if (!nextContent.includes(marker)) continue;
1721
- const requestExpression = marker.startsWith("context") ? "context.request" : "request";
1722
- 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);`;
1723
- return insertAfterOnce(nextContent, marker, identifySnippet, "identifyUser(log");
1724
- }
1725
- return nextContent;
1726
- }
1727
- function getNextEvlogFile(serviceName) {
1728
- return `import { createEvlog } from "evlog/next";
1729
- import { createInstrumentation } from "evlog/next/instrumentation/create";
1730
-
1731
- export const { withEvlog, useLogger, log, createError } = createEvlog({
1732
- service: "${serviceName}",
1733
- });
1734
-
1735
- export const { register, onRequestError } = createInstrumentation({
1736
- service: "${serviceName}",
1737
- });
1738
- `;
1739
- }
1740
- function getNextInstrumentationFile() {
1741
- return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
1742
-
1743
- export const { register, onRequestError } = defineNodeInstrumentation(() => import("./src/lib/evlog"));
1744
- `;
1745
- }
1746
- function getNextProxyFile() {
1747
- return `import { evlogMiddleware } from "evlog/next";
1748
-
1749
- export const proxy = evlogMiddleware();
1750
-
1751
- export const config = {
1752
- matcher: ["/api/:path*"],
1753
- };
1754
- `;
1755
- }
1756
- function getNextEvlogAuthFile(config) {
1757
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1758
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1759
- import { useLogger } from "@/lib/evlog";
1760
-
1761
- export async function identifyEvlogUser(request: Request) {
1762
- const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1763
- exclude: ["/api/auth/**"],
1764
- maskEmail: true,
1765
- });
1766
- await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1767
- }
1768
- `;
1769
- return `${getAuthImportLine(config)}
1770
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1771
- import { useLogger } from "@/lib/evlog";
1772
-
1773
- const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1774
- exclude: ["/api/auth/**"],
1775
- maskEmail: true,
1776
- });
1777
-
1778
- export async function identifyEvlogUser(request: Request) {
1779
- await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1780
- }
1781
- `;
1782
- }
1783
- function getNitroEvlogAuthPluginFile(config) {
1784
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1785
- import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1786
-
1787
- export default defineNitroPlugin((nitroApp) => {
1788
- nitroApp.hooks.hook("request", async (event) => {
1789
- const identify = createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1790
- exclude: ["/api/auth/**"],
1791
- maskEmail: true,
1792
- });
1793
- await identify(event);
1794
- });
1795
- });
1796
- `;
1797
- return `${getAuthImportLine(config)}
1798
- import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1799
-
1800
- export default defineNitroPlugin((nitroApp) => {
1801
- nitroApp.hooks.hook(
1802
- "request",
1803
- createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1804
- exclude: ["/api/auth/**"],
1805
- maskEmail: true,
1806
- }),
1807
- );
1808
- });
1809
- `;
1810
- }
1811
- function getNuxtEvlogAuthMiddlewareFile(config) {
1812
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1813
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1814
-
1815
- export default defineEventHandler(async (event) => {
1816
- if (!event.context.log) return;
1817
- const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1818
- exclude: ["/api/auth/**"],
1819
- maskEmail: true,
1820
- });
1821
- await identify(event.context.log, event.headers, event.path);
1822
- });
1823
- `;
1824
- return `${getAuthImportLine(config)}
1825
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1826
-
1827
- const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1828
- exclude: ["/api/auth/**"],
1829
- maskEmail: true,
1830
- });
1831
-
1832
- export default defineEventHandler(async (event) => {
1833
- if (!event.context.log) return;
1834
- await identify(event.context.log, event.headers, event.path);
1835
- });
1836
- `;
1837
- }
1838
- function getTanstackNitroConfigFile(serviceName) {
1839
- return `import { defineConfig } from "nitro";
1840
- import evlog from "evlog/nitro/v3";
1841
-
1842
- export default defineConfig({
1843
- experimental: {
1844
- asyncContext: true,
1845
- },
1846
- modules: [
1847
- evlog({
1848
- env: { service: "${serviceName}" },
1849
- }),
1850
- ],
1851
- });
1852
- `;
1853
- }
1854
- function getAstroMiddlewareFile(serviceName) {
1855
- return `import { defineMiddleware } from "astro:middleware";
1856
- import { createRequestLogger, initLogger } from "evlog";
1857
-
1858
- initLogger({
1859
- env: { service: "${serviceName}" },
1860
- });
1861
-
1862
- export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
1863
- const url = new URL(request.url);
1864
- const log = createRequestLogger({
1865
- method: request.method,
1866
- path: url.pathname,
1867
- });
1868
-
1869
- locals.log = log;
1870
-
1871
- try {
1872
- const response = await next();
1873
- log.emit();
1874
- return response;
1875
- } catch (error) {
1876
- log.error(error instanceof Error ? error : new Error(String(error)));
1877
- log.emit();
1878
- throw error;
1879
- }
1880
- });
1881
- `;
1882
- }
1883
- function getAstroEnvFile() {
1884
- return `/// <reference types="astro/client" />
1885
-
1886
- import type { RequestLogger } from "evlog";
1887
-
1888
- declare namespace App {
1889
- interface Locals {
1890
- log: RequestLogger;
1891
- }
1892
- }
1893
- `;
1894
- }
1895
- async function setupNextEvlog(config, serviceName) {
1896
- const webDir = path.join(config.projectDir, "apps/web");
1897
- const evlogPath = path.join(webDir, "src/lib/evlog.ts");
1898
- if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
1899
- const identifyWebAuth = shouldIdentifyWebAuth(config);
1900
- if (identifyWebAuth) {
1901
- const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
1902
- if (!await fs.pathExists(evlogAuthPath)) await writeFileIfChanged(evlogAuthPath, getNextEvlogAuthFile(config));
1903
- }
1904
- const instrumentationPath = path.join(webDir, "instrumentation.ts");
1905
- if (!await fs.pathExists(instrumentationPath)) await writeFileIfChanged(instrumentationPath, getNextInstrumentationFile());
1906
- const proxyPath = path.join(webDir, "src/proxy.ts");
1907
- const rootProxyPath = path.join(webDir, "proxy.ts");
1908
- if (!await fs.pathExists(proxyPath) && !await fs.pathExists(rootProxyPath)) await writeFileIfChanged(proxyPath, getNextProxyFile());
1909
- const updateNextApiRoute = (content) => {
1910
- let nextContent = addNextRouteWrappers(content);
1911
- if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1912
- return nextContent;
1913
- };
1914
- await updateFileIfExists(path.join(webDir, "src/app/api/trpc/[trpc]/route.ts"), updateNextApiRoute);
1915
- await updateFileIfExists(path.join(webDir, "src/app/api/rpc/[[...rest]]/route.ts"), updateNextApiRoute);
1916
- if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/app/api/ai/route.ts"), (content) => {
1917
- let nextContent = addNextAiEvlogSetup(content);
1918
- if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1919
- return nextContent;
1920
- });
1921
- }
1922
- async function setupNuxtEvlog(config, serviceName) {
1923
- const webDir = path.join(config.projectDir, "apps/web");
1924
- await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
1925
- if (shouldIdentifyWebAuth(config)) {
1926
- const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1927
- if (await fs.pathExists(oldAuthPluginPath)) {
1928
- if ((await fs.readFile(oldAuthPluginPath, "utf-8")).includes("evlog/better-auth")) await fs.remove(oldAuthPluginPath);
1929
- }
1930
- const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
1931
- if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
1932
- }
1933
- }
1934
- async function setupSvelteEvlog(config, serviceName) {
1935
- const webDir = path.join(config.projectDir, "apps/web");
1936
- await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
1937
- const hooksPath = path.join(webDir, "src/hooks.server.ts");
1938
- if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
1939
- else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
1940
-
1941
- export const { handle, handleError } = createEvlogHooks();
1942
- `);
1943
- await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
1944
- if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
1945
- }
1946
- async function setupTanstackStartEvlog(config, serviceName) {
1947
- const webDir = path.join(config.projectDir, "apps/web");
1948
- const nitroConfigPath = path.join(webDir, "nitro.config.ts");
1949
- if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
1950
- await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
1951
- if (shouldIdentifyWebAuth(config)) {
1952
- const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1953
- if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
1954
- }
1955
- }
1956
- async function setupAstroEvlog(config, serviceName) {
1957
- const webDir = path.join(config.projectDir, "apps/web");
1958
- const middlewarePath = path.join(webDir, "src/middleware.ts");
1959
- if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
1960
- else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
1961
- const envPath = path.join(webDir, "src/env.d.ts");
1962
- if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
1963
- else await updateFileIfExists(envPath, addAstroLocalsType);
1964
- if (shouldIdentifyWebAuth(config)) await updateFileIfExists(middlewarePath, (content) => addAstroBetterAuthEvlogSetup(content, config));
1965
- }
1966
- async function setupEvlogWeb(config) {
1967
- const frontend = getEvlogWebFrontend(config.frontend);
1968
- if (!frontend) return;
1969
- const serviceName = `${config.projectName}-web`;
1970
- if (frontend === "next") await setupNextEvlog(config, serviceName);
1971
- else if (frontend === "nuxt") await setupNuxtEvlog(config, serviceName);
1972
- else if (frontend === "svelte") await setupSvelteEvlog(config, serviceName);
1973
- else if (frontend === "tanstack-start") await setupTanstackStartEvlog(config, serviceName);
1974
- else if (frontend === "astro") await setupAstroEvlog(config, serviceName);
1975
- }
1976
- async function setupEvlog(config) {
1977
- return Result.tryPromise({
1978
- try: async () => {
1979
- if (isEvlogBackend(config.backend)) {
1980
- const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
1981
- if (await fs.pathExists(serverIndexPath)) {
1982
- const content = await fs.readFile(serverIndexPath, "utf-8");
1983
- let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
1984
- if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
1985
- if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
1986
- }
1987
- }
1988
- await setupEvlogWeb(config);
1989
- },
1990
- catch: (error) => new AddonSetupError({
1991
- addon: "evlog",
1992
- message: `Failed to set up evlog: ${error instanceof Error ? error.message : String(error)}`,
1993
- cause: error
1994
- })
1995
- });
1996
- }
1997
- //#endregion
1998
1344
  //#region src/prompts/navigable-group.ts
1999
1345
  /**
2000
1346
  * Navigable group - a group of prompts that allows going back
@@ -2037,7 +1383,7 @@ async function navigableGroup(prompts, opts) {
2037
1383
  goingBack = false;
2038
1384
  continue;
2039
1385
  }
2040
- if (isCancel$1(result)) {
1386
+ if (isCancel$2(result)) {
2041
1387
  if (typeof opts?.onCancel === "function") {
2042
1388
  results[name] = "canceled";
2043
1389
  opts.onCancel({ results });
@@ -2158,206 +1504,6 @@ function getPackageRunnerPrefix(packageManager) {
2158
1504
  }
2159
1505
  }
2160
1506
  //#endregion
2161
- //#region src/helpers/addons/fumadocs-setup.ts
2162
- const TEMPLATES$2 = {
2163
- "next-mdx": {
2164
- label: "Next.js: Fumadocs MDX",
2165
- hint: "recommended",
2166
- value: "+next+fuma-docs-mdx"
2167
- },
2168
- "next-mdx-static": {
2169
- label: "Next.js Static: Fumadocs MDX",
2170
- value: "+next+fuma-docs-mdx+static"
2171
- },
2172
- waku: {
2173
- label: "Waku: Fumadocs MDX",
2174
- value: "waku"
2175
- },
2176
- "react-router": {
2177
- label: "React Router: Fumadocs MDX (not RSC)",
2178
- value: "react-router"
2179
- },
2180
- "react-router-spa": {
2181
- label: "React Router SPA: Fumadocs MDX (not RSC)",
2182
- hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
2183
- value: "react-router-spa"
2184
- },
2185
- "tanstack-start": {
2186
- label: "Tanstack Start: Fumadocs MDX (not RSC)",
2187
- value: "tanstack-start"
2188
- },
2189
- "tanstack-start-spa": {
2190
- label: "Tanstack Start SPA: Fumadocs MDX (not RSC)",
2191
- hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
2192
- value: "tanstack-start-spa"
2193
- }
2194
- };
2195
- const DEFAULT_TEMPLATE$2 = "next-mdx";
2196
- const DEFAULT_DEV_PORT$1 = 4e3;
2197
- function aiChatDisabledForTemplate(template) {
2198
- return template === "next-mdx-static" || template.endsWith("-spa");
2199
- }
2200
- function getFumadocsLinter(addons) {
2201
- if (addons.includes("oxlint")) return "oxlint";
2202
- if (addons.includes("biome") || addons.includes("ultracite")) return "biome";
2203
- if (addons.includes("vite-plus")) return "oxlint";
2204
- }
2205
- function getFumadocsAddonContext(currentAddons, persistedAddons) {
2206
- return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
2207
- }
2208
- async function setupFumadocs(config) {
2209
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2210
- const { packageManager, projectDir } = config;
2211
- cliLog.info("Setting up Fumadocs...");
2212
- const configuredOptions = config.addonOptions?.fumadocs;
2213
- let template = configuredOptions?.template;
2214
- let search = configuredOptions?.search;
2215
- let ogImage = configuredOptions?.ogImage;
2216
- let aiChat = configuredOptions?.aiChat;
2217
- if (isSilent()) template = template ?? DEFAULT_TEMPLATE$2;
2218
- else {
2219
- const promptResult = await Result.tryPromise({
2220
- try: () => navigableGroup({
2221
- template: async () => {
2222
- if (template !== void 0) return template;
2223
- return navigableSelect({
2224
- message: "Choose a template",
2225
- options: Object.entries(TEMPLATES$2).map(([key, t]) => ({
2226
- value: key,
2227
- label: t.label,
2228
- hint: "hint" in t ? t.hint : void 0
2229
- })),
2230
- initialValue: DEFAULT_TEMPLATE$2
2231
- });
2232
- },
2233
- search: async () => {
2234
- if (search !== void 0) return search;
2235
- return navigableSelect({
2236
- message: "Choose a search solution?",
2237
- options: [{
2238
- value: "orama",
2239
- label: "Default",
2240
- hint: "local search powered by Orama, recommended"
2241
- }, {
2242
- value: "orama-cloud",
2243
- label: "Orama Cloud",
2244
- hint: "3rd party search solution, signup needed"
2245
- }],
2246
- initialValue: "orama"
2247
- });
2248
- },
2249
- ogImage: async ({ results }) => {
2250
- if (ogImage !== void 0) return ogImage;
2251
- if (!(results.template ?? template ?? DEFAULT_TEMPLATE$2).startsWith("next-")) return "skip";
2252
- return navigableSelect({
2253
- message: "Configure Open Graph Image generation?",
2254
- options: [{
2255
- value: "next-og",
2256
- label: "next/og",
2257
- hint: "Next.js built-in solution"
2258
- }, {
2259
- value: "takumi",
2260
- label: "Takumi",
2261
- hint: "Output WebP format, framework-agnostic"
2262
- }],
2263
- initialValue: "next-og"
2264
- });
2265
- },
2266
- aiChat: async ({ results }) => {
2267
- if (aiChat !== void 0) return aiChat;
2268
- if (aiChatDisabledForTemplate(results.template ?? template ?? DEFAULT_TEMPLATE$2)) return "none";
2269
- return navigableSelect({
2270
- message: "Configure AI Chat?",
2271
- options: [
2272
- {
2273
- value: "none",
2274
- label: "No"
2275
- },
2276
- {
2277
- value: "openrouter",
2278
- label: "AI SDK",
2279
- hint: "default to OpenRouter"
2280
- },
2281
- {
2282
- value: "llmgateway",
2283
- label: "LLM Gateway"
2284
- },
2285
- {
2286
- value: "inkeep",
2287
- label: "Inkeep AI",
2288
- hint: "API key required"
2289
- }
2290
- ],
2291
- initialValue: "none"
2292
- });
2293
- }
2294
- }),
2295
- catch: (e) => new AddonSetupError({
2296
- addon: "fumadocs",
2297
- message: `Failed to run Fumadocs prompts: ${e instanceof Error ? e.message : String(e)}`,
2298
- cause: e
2299
- })
2300
- });
2301
- if (promptResult.isErr()) return Result.err(promptResult.error);
2302
- const results = promptResult.value;
2303
- if (results.template === void 0 || results.search === void 0 || results.ogImage === void 0 || results.aiChat === void 0) return userCancelled("Operation cancelled");
2304
- template = results.template;
2305
- search = results.search;
2306
- ogImage = results.ogImage === "skip" ? void 0 : results.ogImage;
2307
- aiChat = results.aiChat === "none" ? void 0 : results.aiChat;
2308
- }
2309
- if (!template) return userCancelled("Operation cancelled");
2310
- const isNextTemplate = template.startsWith("next-");
2311
- if (!isNextTemplate) ogImage = void 0;
2312
- if (aiChatDisabledForTemplate(template)) aiChat = void 0;
2313
- const templateArg = TEMPLATES$2[template].value;
2314
- const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT$1;
2315
- const options = [
2316
- `--template ${templateArg}`,
2317
- `--pm ${packageManager}`,
2318
- "--no-git"
2319
- ];
2320
- if (isNextTemplate) options.push("--src");
2321
- const persistedConfig = await readBtsConfig(projectDir);
2322
- const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
2323
- if (linter) options.push(`--linter ${linter}`);
2324
- if (search) options.push(`--search ${search}`);
2325
- if (ogImage) options.push(`--og-image ${ogImage}`);
2326
- if (aiChat) options.push(`--ai-chat ${aiChat}`);
2327
- const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
2328
- const appsDir = path.join(projectDir, "apps");
2329
- await fs.ensureDir(appsDir);
2330
- const s = createSpinner();
2331
- s.start("Running Fumadocs create command...");
2332
- const result = await Result.tryPromise({
2333
- try: async () => {
2334
- await $({
2335
- cwd: appsDir,
2336
- env: { CI: "true" }
2337
- })`${args}`;
2338
- const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
2339
- const packageJsonPath = path.join(fumadocsDir, "package.json");
2340
- if (await fs.pathExists(packageJsonPath)) {
2341
- const packageJson = await fs.readJson(packageJsonPath);
2342
- packageJson.name = "fumadocs";
2343
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=${devPort}`;
2344
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2345
- }
2346
- },
2347
- catch: (e) => new AddonSetupError({
2348
- addon: "fumadocs",
2349
- message: `Failed to set up Fumadocs: ${e instanceof Error ? e.message : String(e)}`,
2350
- cause: e
2351
- })
2352
- });
2353
- if (result.isErr()) {
2354
- s.stop("Failed to set up Fumadocs");
2355
- return result;
2356
- }
2357
- s.stop("Fumadocs setup complete!");
2358
- return Result.ok(void 0);
2359
- }
2360
- //#endregion
2361
1507
  //#region src/helpers/addons/mcp-setup.ts
2362
1508
  const MCP_AGENTS = [
2363
1509
  {
@@ -2432,7 +1578,7 @@ const MCP_AGENTS = [
2432
1578
  }
2433
1579
  ];
2434
1580
  const DEFAULT_SCOPE$1 = "project";
2435
- const DEFAULT_AGENTS$2 = [
1581
+ const DEFAULT_AGENTS$1 = [
2436
1582
  "cursor",
2437
1583
  "claude-code",
2438
1584
  "vscode"
@@ -2450,7 +1596,7 @@ function getAllMcpServers(config) {
2450
1596
  return [
2451
1597
  {
2452
1598
  key: "kubojs",
2453
- label: "I dont know",
1599
+ label: "kubojs",
2454
1600
  name: "kubojs",
2455
1601
  target: getPackageExecutionCommand(config.packageManager, "kubojs@latest mcp")
2456
1602
  },
@@ -2556,7 +1702,6 @@ function getAllMcpServers(config) {
2556
1702
  function getRecommendedMcpServers(config, scope) {
2557
1703
  const serversByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
2558
1704
  const recommendedServerKeys = ["kubojs", "context7"];
2559
- if (scope === "project" && config.addons.includes("nx")) recommendedServerKeys.push("nx");
2560
1705
  if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") recommendedServerKeys.push("cloudflare-docs");
2561
1706
  if (config.backend === "convex") recommendedServerKeys.push("convex");
2562
1707
  if (hasReactBasedFrontend$1(config.frontend)) recommendedServerKeys.push("shadcn");
@@ -2595,7 +1740,7 @@ async function setupMcp(config) {
2595
1740
  selectedServerKeys = configuredServerKeys?.filter((k) => availableServerKeys.has(k)) ?? serverOptions;
2596
1741
  if (selectedServerKeys.length === 0) return Result.ok(void 0);
2597
1742
  const agentOptions = filterAgentsForScope(scope);
2598
- 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)));
2599
1744
  selectedAgents = configuredAgents?.filter((a) => agentOptions.some((o) => o.value === a)) ?? defaultAgents;
2600
1745
  if (selectedAgents.length === 0) return Result.ok(void 0);
2601
1746
  } else {
@@ -2638,7 +1783,7 @@ async function setupMcp(config) {
2638
1783
  if (currentServers !== void 0 && currentServers.length === 0) return [];
2639
1784
  const agentOpts = filterAgentsForScope(currentScope);
2640
1785
  if (agentOpts.length === 0) return [];
2641
- 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)));
2642
1787
  if (configuredAgents !== void 0) return configuredAgents.filter((a) => agentOpts.some((o) => o.value === a));
2643
1788
  return navigableMultiselect({
2644
1789
  message: "Select agents to install MCP servers to",
@@ -2884,7 +2029,7 @@ const AVAILABLE_AGENTS = [
2884
2029
  }
2885
2030
  ];
2886
2031
  const DEFAULT_SCOPE = "project";
2887
- const DEFAULT_AGENTS$1 = [
2032
+ const DEFAULT_AGENTS = [
2888
2033
  "cursor",
2889
2034
  "claude-code",
2890
2035
  "github-copilot"
@@ -2919,8 +2064,6 @@ function getRecommendedSourceKeys(config) {
2919
2064
  if (backend === "elysia") sources.push("elysiajs/skills");
2920
2065
  if (backend === "convex") sources.push("waynesutton/convexskills");
2921
2066
  if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
2922
- if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
2923
- if (addons.includes("evlog")) sources.push("https://www.evlog.dev");
2924
2067
  return sources;
2925
2068
  }
2926
2069
  const CURATED_SKILLS_BY_SOURCE = {
@@ -3001,11 +2144,11 @@ function uniqueValues(values) {
3001
2144
  async function setupSkills(config) {
3002
2145
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
3003
2146
  const { packageManager, projectDir } = config;
3004
- const btsConfig = await readBtsConfig(projectDir);
3005
- const fullConfig = btsConfig ? {
2147
+ const kubojsConfig = await readKubojsConfig(projectDir);
2148
+ const fullConfig = kubojsConfig ? {
3006
2149
  ...config,
3007
- addons: btsConfig.addons ?? config.addons,
3008
- addonOptions: btsConfig.addonOptions ?? config.addonOptions
2150
+ addons: kubojsConfig.addons ?? config.addons,
2151
+ addonOptions: kubojsConfig.addonOptions ?? config.addonOptions
3009
2152
  } : config;
3010
2153
  const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
3011
2154
  const skillsOptions = fullConfig.addonOptions?.skills;
@@ -3032,7 +2175,7 @@ async function setupSkills(config) {
3032
2175
  scope = configuredScope ?? DEFAULT_SCOPE;
3033
2176
  selectedSkills = configuredSelections !== void 0 ? configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`)) : allSkillValues;
3034
2177
  if (selectedSkills.length === 0) return Result.ok(void 0);
3035
- selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS$1];
2178
+ selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS];
3036
2179
  if (selectedAgents.length === 0) return Result.ok(void 0);
3037
2180
  } else {
3038
2181
  const results = await navigableGroup({
@@ -3069,7 +2212,7 @@ async function setupSkills(config) {
3069
2212
  message: "Select agents to install skills to",
3070
2213
  options: AVAILABLE_AGENTS,
3071
2214
  required: false,
3072
- initialValues: [...DEFAULT_AGENTS$1]
2215
+ initialValues: [...DEFAULT_AGENTS]
3073
2216
  });
3074
2217
  }
3075
2218
  });
@@ -3118,44 +2261,6 @@ async function setupSkills(config) {
3118
2261
  return Result.ok(void 0);
3119
2262
  }
3120
2263
  //#endregion
3121
- //#region src/helpers/addons/starlight-setup.ts
3122
- async function setupStarlight(config) {
3123
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3124
- const { packageManager, projectDir } = config;
3125
- const s = createSpinner();
3126
- s.start("Setting up Starlight docs...");
3127
- const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
3128
- "docs",
3129
- "--template",
3130
- "starlight",
3131
- "--yes",
3132
- "--no-install",
3133
- "--no-git",
3134
- "--skip-houston"
3135
- ].join(" ")}`);
3136
- const appsDir = path.join(projectDir, "apps");
3137
- await fs.ensureDir(appsDir);
3138
- const result = await Result.tryPromise({
3139
- try: async () => {
3140
- await $({
3141
- cwd: appsDir,
3142
- env: { CI: "true" }
3143
- })`${args}`;
3144
- },
3145
- catch: (e) => new AddonSetupError({
3146
- addon: "starlight",
3147
- message: `Failed to set up Starlight docs: ${e instanceof Error ? e.message : String(e)}`,
3148
- cause: e
3149
- })
3150
- });
3151
- if (result.isErr()) {
3152
- s.stop("Failed to set up Starlight docs");
3153
- return result;
3154
- }
3155
- s.stop("Starlight docs setup successfully!");
3156
- return Result.ok(void 0);
3157
- }
3158
- //#endregion
3159
2264
  //#region src/helpers/addons/tauri-setup.ts
3160
2265
  function getWebFrontend(frontend) {
3161
2266
  return frontend.find((value) => types_exports.desktopWebFrontends.includes(value));
@@ -3236,7 +2341,7 @@ async function setupTauri(config) {
3236
2341
  }
3237
2342
  //#endregion
3238
2343
  //#region src/helpers/addons/tui-setup.ts
3239
- const TEMPLATES$1 = {
2344
+ const TEMPLATES = {
3240
2345
  core: {
3241
2346
  label: "Core",
3242
2347
  hint: "Basic OpenTUI template"
@@ -3250,7 +2355,7 @@ const TEMPLATES$1 = {
3250
2355
  hint: "SolidJS-based OpenTUI template"
3251
2356
  }
3252
2357
  };
3253
- const DEFAULT_TEMPLATE$1 = "core";
2358
+ const DEFAULT_TEMPLATE = "core";
3254
2359
  const TUI_LOCKFILES = [
3255
2360
  "bun.lock",
3256
2361
  "package-lock.json",
@@ -3260,7 +2365,7 @@ const TUI_LOCKFILES = [
3260
2365
  function resolveTuiTemplate(config) {
3261
2366
  const configuredTemplate = config.addonOptions?.opentui?.template;
3262
2367
  if (configuredTemplate) return configuredTemplate;
3263
- if (isSilent()) return DEFAULT_TEMPLATE$1;
2368
+ if (isSilent()) return DEFAULT_TEMPLATE;
3264
2369
  }
3265
2370
  async function setupTui(config) {
3266
2371
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
@@ -3271,14 +2376,14 @@ async function setupTui(config) {
3271
2376
  setIsFirstPrompt(true);
3272
2377
  const selectedTemplate = await navigableSelect({
3273
2378
  message: "Choose a template",
3274
- options: Object.entries(TEMPLATES$1).map(([key, templateOption]) => ({
2379
+ options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
3275
2380
  value: key,
3276
2381
  label: templateOption.label,
3277
2382
  hint: templateOption.hint
3278
2383
  })),
3279
- initialValue: DEFAULT_TEMPLATE$1
2384
+ initialValue: DEFAULT_TEMPLATE
3280
2385
  });
3281
- if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
2386
+ if (isCancel$2(selectedTemplate)) return userCancelled("Operation cancelled");
3282
2387
  template = selectedTemplate;
3283
2388
  }
3284
2389
  const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
@@ -3356,324 +2461,6 @@ async function postProcessTuiWorkspace(tuiDir) {
3356
2461
  return Result.ok(void 0);
3357
2462
  }
3358
2463
  //#endregion
3359
- //#region src/helpers/addons/ultracite-setup.ts
3360
- const LINTERS = {
3361
- biome: { label: "Biome (Recommended)" },
3362
- oxlint: { label: "Oxlint + Oxfmt" }
3363
- };
3364
- const AGENTS = {
3365
- universal: { label: "Universal (AGENTS.md — covers all agents)" },
3366
- claude: { label: "Claude Code" },
3367
- codex: { label: "Codex" },
3368
- jules: { label: "Jules" },
3369
- replit: { label: "Replit Agent" },
3370
- devin: { label: "Devin" },
3371
- lovable: { label: "Lovable" },
3372
- zencoder: { label: "Zencoder" },
3373
- ona: { label: "Ona" },
3374
- openclaw: { label: "OpenClaw" },
3375
- continue: { label: "Continue" },
3376
- "snowflake-cortex": { label: "Snowflake Cortex" },
3377
- deepagents: { label: "Deepagents" },
3378
- qoder: { label: "Qoder" },
3379
- "kimi-cli": { label: "Kimi CLI" },
3380
- mcpjam: { label: "MCPJam" },
3381
- mux: { label: "Mux" },
3382
- pi: { label: "Pi" },
3383
- adal: { label: "AdaL" },
3384
- copilot: { label: "GitHub Copilot" },
3385
- cline: { label: "Cline" },
3386
- amp: { label: "AMP" },
3387
- aider: { label: "Aider" },
3388
- "firebase-studio": { label: "Firebase Studio" },
3389
- "open-hands": { label: "OpenHands" },
3390
- gemini: { label: "Gemini" },
3391
- junie: { label: "Junie" },
3392
- augmentcode: { label: "Augment Code" },
3393
- bob: { label: "IBM Bob" },
3394
- "kilo-code": { label: "Kilo Code" },
3395
- goose: { label: "Goose" },
3396
- "roo-code": { label: "Roo Code" },
3397
- warp: { label: "Warp" },
3398
- droid: { label: "Droid" },
3399
- opencode: { label: "OpenCode" },
3400
- crush: { label: "Crush" },
3401
- qwen: { label: "Qwen Code" },
3402
- "amazon-q-cli": { label: "Amazon Q CLI" },
3403
- firebender: { label: "Firebender" },
3404
- "cursor-cli": { label: "Cursor CLI" },
3405
- "mistral-vibe": { label: "Mistral Vibe" },
3406
- vercel: { label: "Vercel Agent" }
3407
- };
3408
- const HOOKS = {
3409
- cursor: { label: "Cursor" },
3410
- windsurf: { label: "Windsurf" },
3411
- codebuddy: { label: "CodeBuddy" },
3412
- claude: { label: "Claude Code" },
3413
- copilot: { label: "GitHub Copilot" }
3414
- };
3415
- const ULTRACITE_VERSION = "7.9.3";
3416
- const DEFAULT_LINTER = "biome";
3417
- const DEFAULT_EDITORS = ["vscode"];
3418
- const DEFAULT_AGENTS = ["universal"];
3419
- const DEFAULT_HOOKS = [];
3420
- function getFrameworksFromFrontend(frontend) {
3421
- const frameworkMap = {
3422
- "tanstack-router": ["react", "tanstack"],
3423
- "react-router": ["react", "remix"],
3424
- "tanstack-start": ["react", "tanstack"],
3425
- next: ["react", "next"],
3426
- nuxt: ["vue"],
3427
- "native-bare": ["react"],
3428
- "native-uniwind": ["react"],
3429
- "native-unistyles": ["react"],
3430
- svelte: ["svelte"],
3431
- solid: ["solid"],
3432
- astro: ["astro"]
3433
- };
3434
- const frameworks = /* @__PURE__ */ new Set();
3435
- for (const f of frontend) for (const framework of frameworkMap[f] ?? []) frameworks.add(framework);
3436
- return Array.from(frameworks);
3437
- }
3438
- function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
3439
- const ultraciteArgs = [
3440
- "init",
3441
- "--pm",
3442
- packageManager,
3443
- "--linter",
3444
- linter
3445
- ];
3446
- if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
3447
- if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
3448
- if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
3449
- if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
3450
- if (gitHooks.length > 0) ultraciteArgs.push("--integrations", ...gitHooks);
3451
- return [
3452
- ...getPackageRunnerPrefix(packageManager),
3453
- `ultracite@${ULTRACITE_VERSION}`,
3454
- ...ultraciteArgs,
3455
- "--skip-install",
3456
- "--quiet"
3457
- ];
3458
- }
3459
- async function removeHuskySampleHook(projectDir) {
3460
- const hookPath = path.join(projectDir, ".husky", "pre-commit");
3461
- if (!await fs.pathExists(hookPath)) return;
3462
- const content = await fs.readFile(hookPath, "utf-8");
3463
- const cleaned = content.split("\n").filter((line) => !/^(?:npm|pnpm|yarn|bun|deno)(?: run)? test$/.test(line.trim())).join("\n");
3464
- if (cleaned !== content) await fs.writeFile(hookPath, cleaned);
3465
- }
3466
- async function setupUltracite(config, gitHooks) {
3467
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3468
- const { packageManager, projectDir, frontend } = config;
3469
- cliLog.info("Setting up Ultracite...");
3470
- const configuredOptions = config.addonOptions?.ultracite;
3471
- let linter = configuredOptions?.linter;
3472
- let editors = configuredOptions?.editors;
3473
- let agents = configuredOptions?.agents;
3474
- let hooks = configuredOptions?.hooks;
3475
- if (!linter || !editors || !agents || !hooks) if (isSilent()) {
3476
- linter = linter ?? DEFAULT_LINTER;
3477
- editors = editors ?? [...DEFAULT_EDITORS];
3478
- agents = agents ?? [...DEFAULT_AGENTS];
3479
- hooks = hooks ?? [...DEFAULT_HOOKS];
3480
- } else {
3481
- const results = await navigableGroup({
3482
- linter: async () => {
3483
- if (linter !== void 0) return linter;
3484
- return navigableSelect({
3485
- message: "Which linter do you want to use?",
3486
- options: Object.entries(LINTERS).map(([key, linterOption]) => ({
3487
- value: key,
3488
- label: linterOption.label
3489
- })),
3490
- initialValue: linter ?? DEFAULT_LINTER
3491
- });
3492
- },
3493
- editors: async () => {
3494
- if (editors !== void 0) return editors;
3495
- return navigableMultiselect({
3496
- message: "Which editors do you want to configure (recommended)?",
3497
- required: false,
3498
- options: [{
3499
- value: "vscode",
3500
- label: "VSCode / Cursor / Windsurf"
3501
- }, {
3502
- value: "zed",
3503
- label: "Zed"
3504
- }]
3505
- });
3506
- },
3507
- agents: async () => {
3508
- if (agents !== void 0) return agents;
3509
- return navigableMultiselect({
3510
- message: "Which agent files do you want to add (optional)?",
3511
- required: false,
3512
- options: Object.entries(AGENTS).map(([key, agent]) => ({
3513
- value: key,
3514
- label: agent.label
3515
- }))
3516
- });
3517
- },
3518
- hooks: async () => {
3519
- if (hooks !== void 0) return hooks;
3520
- return navigableMultiselect({
3521
- message: "Which agent hooks do you want to enable (optional)?",
3522
- required: false,
3523
- options: Object.entries(HOOKS).map(([key, hook]) => ({
3524
- value: key,
3525
- label: hook.label
3526
- }))
3527
- });
3528
- }
3529
- });
3530
- if (results.linter === void 0 || results.editors === void 0 || results.agents === void 0 || results.hooks === void 0) return userCancelled("Operation cancelled");
3531
- linter = results.linter;
3532
- editors = results.editors;
3533
- agents = results.agents;
3534
- hooks = results.hooks;
3535
- }
3536
- const frameworks = getFrameworksFromFrontend(frontend);
3537
- const args = buildUltraciteInitArgs({
3538
- packageManager,
3539
- linter,
3540
- frameworks,
3541
- editors,
3542
- agents,
3543
- hooks,
3544
- gitHooks
3545
- });
3546
- const s = createSpinner();
3547
- s.start("Running Ultracite init command...");
3548
- const initResult = await Result.tryPromise({
3549
- try: async () => {
3550
- await $({
3551
- cwd: projectDir,
3552
- env: { CI: "true" }
3553
- })`${args}`;
3554
- },
3555
- catch: (e) => {
3556
- s.stop(pc.red("Failed to run Ultracite init command"));
3557
- return new AddonSetupError({
3558
- addon: "ultracite",
3559
- message: `Failed to set up Ultracite: ${e instanceof Error ? e.message : String(e)}`,
3560
- cause: e
3561
- });
3562
- }
3563
- });
3564
- if (initResult.isErr()) {
3565
- cliLog.error(pc.red("Failed to set up Ultracite"));
3566
- return initResult;
3567
- }
3568
- if (gitHooks.includes("husky")) await Result.tryPromise({
3569
- try: () => removeHuskySampleHook(projectDir),
3570
- catch: (e) => e
3571
- });
3572
- s.stop("Ultracite setup successfully!");
3573
- return Result.ok(void 0);
3574
- }
3575
- //#endregion
3576
- //#region src/helpers/addons/wxt-setup.ts
3577
- const TEMPLATES = {
3578
- vanilla: {
3579
- label: "Vanilla",
3580
- hint: "Vanilla JavaScript template"
3581
- },
3582
- vue: {
3583
- label: "Vue",
3584
- hint: "Vue.js template"
3585
- },
3586
- react: {
3587
- label: "React",
3588
- hint: "React template"
3589
- },
3590
- solid: {
3591
- label: "Solid",
3592
- hint: "SolidJS template"
3593
- },
3594
- svelte: {
3595
- label: "Svelte",
3596
- hint: "Svelte template"
3597
- }
3598
- };
3599
- const DEFAULT_TEMPLATE = "react";
3600
- const DEFAULT_DEV_PORT = 5555;
3601
- async function setupWxt(config) {
3602
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3603
- const { packageManager, projectDir } = config;
3604
- cliLog.info("Setting up WXT...");
3605
- const configuredOptions = config.addonOptions?.wxt;
3606
- let template = configuredOptions?.template;
3607
- if (!template) if (isSilent()) template = DEFAULT_TEMPLATE;
3608
- else {
3609
- setIsFirstPrompt(true);
3610
- const selectedTemplate = await navigableSelect({
3611
- message: "Choose a template",
3612
- options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
3613
- value: key,
3614
- label: templateOption.label,
3615
- hint: templateOption.hint
3616
- })),
3617
- initialValue: DEFAULT_TEMPLATE
3618
- });
3619
- if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
3620
- template = selectedTemplate;
3621
- }
3622
- const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT;
3623
- const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
3624
- const appsDir = path.join(projectDir, "apps");
3625
- const ensureDirResult = await Result.tryPromise({
3626
- try: () => fs.ensureDir(appsDir),
3627
- catch: (e) => new AddonSetupError({
3628
- addon: "wxt",
3629
- message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3630
- cause: e
3631
- })
3632
- });
3633
- if (ensureDirResult.isErr()) return ensureDirResult;
3634
- const s = createSpinner();
3635
- s.start("Running WXT init command...");
3636
- const initResult = await Result.tryPromise({
3637
- try: async () => {
3638
- await $({
3639
- cwd: appsDir,
3640
- env: { CI: "true" }
3641
- })`${args}`;
3642
- },
3643
- catch: (e) => {
3644
- s.stop(pc.red("Failed to run WXT init command"));
3645
- return new AddonSetupError({
3646
- addon: "wxt",
3647
- message: `Failed to set up WXT: ${e instanceof Error ? e.message : String(e)}`,
3648
- cause: e
3649
- });
3650
- }
3651
- });
3652
- if (initResult.isErr()) {
3653
- cliLog.error(pc.red("Failed to set up WXT"));
3654
- return initResult;
3655
- }
3656
- const extensionDir = path.join(projectDir, "apps", "extension");
3657
- const packageJsonPath = path.join(extensionDir, "package.json");
3658
- if ((await Result.tryPromise({
3659
- try: async () => {
3660
- if (await fs.pathExists(packageJsonPath)) {
3661
- const packageJson = await fs.readJson(packageJsonPath);
3662
- packageJson.name = "extension";
3663
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port ${devPort}`;
3664
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3665
- }
3666
- },
3667
- catch: (e) => new AddonSetupError({
3668
- addon: "wxt",
3669
- message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
3670
- cause: e
3671
- })
3672
- })).isErr()) cliLog.warn(pc.yellow("WXT setup completed but failed to update package.json"));
3673
- s.stop("WXT setup complete!");
3674
- return Result.ok(void 0);
3675
- }
3676
- //#endregion
3677
2464
  //#region src/helpers/addons/addons-setup.ts
3678
2465
  async function runSetup(setupFn) {
3679
2466
  const result = await setupFn();
@@ -3697,36 +2484,24 @@ async function setupAddons(config) {
3697
2484
  const { addons, frontend, projectDir } = config;
3698
2485
  const hasWebFrontend = frontend.some((value) => types_exports.desktopWebFrontends.includes(value));
3699
2486
  if (addons.includes("tauri") && hasWebFrontend) await runSetup(() => setupTauri(config));
3700
- const hasUltracite = addons.includes("ultracite");
3701
- const hasBiome = addons.includes("biome");
2487
+ const hasOxlint = addons.includes("oxlint");
2488
+ const hasBiome = !hasOxlint && addons.includes("biome");
3702
2489
  const hasHusky = addons.includes("husky");
3703
2490
  const hasLefthook = addons.includes("lefthook");
3704
- const hasOxlint = addons.includes("oxlint");
3705
2491
  const hasVitePlus = addons.includes("vite-plus");
3706
- if (hasUltracite) {
3707
- const gitHooks = [];
3708
- if (hasHusky) gitHooks.push("husky");
3709
- if (hasLefthook) gitHooks.push("lefthook");
3710
- await runSetup(() => setupUltracite(config, gitHooks));
3711
- } else {
3712
- if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
3713
- if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
3714
- if (hasHusky || hasLefthook) {
3715
- let linter;
3716
- if (hasOxlint) linter = "oxlint";
3717
- else if (hasBiome) linter = "biome";
3718
- else if (hasVitePlus) linter = "vite-plus";
3719
- if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
3720
- if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3721
- }
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));
3722
2501
  }
3723
- if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
3724
- if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config));
3725
2502
  if (addons.includes("opentui")) await runSetup(() => setupTui(config));
3726
- if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
3727
2503
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
3728
2504
  if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
3729
- if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
3730
2505
  }
3731
2506
  async function setupBiome(projectDir) {
3732
2507
  await addPackageDependency({
@@ -3743,57 +2518,168 @@ async function setupBiome(projectDir) {
3743
2518
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3744
2519
  }
3745
2520
  }
3746
- async function setupHusky(projectDir, linter) {
3747
- await addPackageDependency({
3748
- devDependencies: ["husky", "lint-staged"],
3749
- projectDir
3750
- });
3751
- const packageJsonPath = path.join(projectDir, "package.json");
3752
- if (await fs.pathExists(packageJsonPath)) {
3753
- const packageJson = await fs.readJson(packageJsonPath);
3754
- packageJson.scripts = {
3755
- ...packageJson.scripts,
3756
- prepare: "husky"
3757
- };
3758
- if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
3759
- else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
3760
- else if (linter === "vite-plus") packageJson["lint-staged"] = { "*.{js,ts,jsx,tsx,vue,svelte,json,jsonc,css,md}": ["vp check --fix"] };
3761
- else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
3762
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3763
- }
2521
+ async function setupHusky(projectDir, linter) {
2522
+ await addPackageDependency({
2523
+ devDependencies: ["husky", "lint-staged"],
2524
+ projectDir
2525
+ });
2526
+ const packageJsonPath = path.join(projectDir, "package.json");
2527
+ if (await fs.pathExists(packageJsonPath)) {
2528
+ const packageJson = await fs.readJson(packageJsonPath);
2529
+ packageJson.scripts = {
2530
+ ...packageJson.scripts,
2531
+ prepare: "husky"
2532
+ };
2533
+ if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
2534
+ else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2535
+ else if (linter === "vite-plus") packageJson["lint-staged"] = { "*.{js,ts,jsx,tsx,vue,svelte,json,jsonc,css,md}": ["vp check --fix"] };
2536
+ else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2537
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2538
+ }
2539
+ }
2540
+ async function setupLefthook(projectDir) {
2541
+ await addPackageDependency({
2542
+ devDependencies: ["lefthook"],
2543
+ projectDir
2544
+ });
2545
+ }
2546
+ //#endregion
2547
+ //#region src/helpers/testing/testing-setup.ts
2548
+ async function updateScripts(projectDir, scripts) {
2549
+ const packageJsonPath = path.join(projectDir, "package.json");
2550
+ if (!await fs.pathExists(packageJsonPath)) return;
2551
+ const packageJson = await fs.readJson(packageJsonPath);
2552
+ packageJson.scripts = {
2553
+ ...packageJson.scripts,
2554
+ ...scripts
2555
+ };
2556
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2557
+ }
2558
+ async function setupTesting(projectDir, testing) {
2559
+ if (testing.includes("vitest")) {
2560
+ await addPackageDependency({
2561
+ devDependencies: ["vitest"],
2562
+ projectDir
2563
+ });
2564
+ await updateScripts(projectDir, {
2565
+ test: "vitest run",
2566
+ "test:watch": "vitest"
2567
+ });
2568
+ }
2569
+ if (testing.includes("playwright")) {
2570
+ await addPackageDependency({
2571
+ devDependencies: ["@playwright/test"],
2572
+ projectDir
2573
+ });
2574
+ await updateScripts(projectDir, { "test:e2e": "playwright test" });
2575
+ }
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
+ }
2591
+ //#endregion
2592
+ //#region src/utils/config-processing.ts
2593
+ function processArrayOption(options) {
2594
+ if (!options || options.length === 0) return [];
2595
+ if (options.includes("none")) return [];
2596
+ return options.filter((item) => item !== "none");
2597
+ }
2598
+ function deriveProjectName(projectName, projectDirectory) {
2599
+ if (projectName) return projectName;
2600
+ if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
2601
+ return "";
2602
+ }
2603
+ function processFlags(options, projectName) {
2604
+ const config = {};
2605
+ if (options.api) config.api = options.api;
2606
+ if (options.addonOptions) config.addonOptions = options.addonOptions;
2607
+ if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
2608
+ if (options.backend) config.backend = options.backend;
2609
+ if (options.database) config.database = options.database;
2610
+ if (options.orm) config.orm = options.orm;
2611
+ if (options.auth !== void 0) config.auth = options.auth;
2612
+ if (options.payments !== void 0) config.payments = normalizePayments(options.payments);
2613
+ if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
2614
+ if (options.disableObservability) config.observability = [];
2615
+ if (options.communication !== void 0) config.communication = options.communication;
2616
+ if (options.git !== void 0) config.git = options.git;
2617
+ if (options.install !== void 0) config.install = options.install;
2618
+ if (options.runtime) config.runtime = options.runtime;
2619
+ if (options.dbSetup) config.dbSetup = options.dbSetup;
2620
+ if (options.packageManager) config.packageManager = options.packageManager;
2621
+ if (options.webDeploy) config.webDeploy = options.webDeploy;
2622
+ if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
2623
+ const derivedName = deriveProjectName(projectName, options.projectDirectory);
2624
+ if (derivedName) config.projectName = projectName || derivedName;
2625
+ if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
2626
+ if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
2627
+ if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
2628
+ if (options.testing && options.testing.length > 0) config.testing = processArrayOption(options.testing);
2629
+ return config;
2630
+ }
2631
+ function normalizeObservability(value) {
2632
+ if (value === "none" || value === void 0) return [];
2633
+ return [...new Set((Array.isArray(value) ? value : [value]).filter((item) => item === "getmonitor" || item === "himetrica"))];
3764
2634
  }
3765
- async function setupLefthook(projectDir) {
3766
- await addPackageDependency({
3767
- devDependencies: ["lefthook"],
3768
- projectDir
3769
- });
2635
+ function getProvidedFlags(options) {
2636
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
2637
+ }
2638
+ function validateNoneExclusivity(options, optionName) {
2639
+ if (!options || options.length === 0) return Result.ok(void 0);
2640
+ if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
2641
+ return Result.ok(void 0);
2642
+ }
2643
+ function validateArrayOptions(options) {
2644
+ if (options.disableObservability && options.observability !== void 0) return Result.err(new ValidationError({ message: "Cannot combine '--disable-observability' with '--observability'." }));
2645
+ const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
2646
+ if (frontendResult.isErr()) return frontendResult;
2647
+ const addonsResult = validateNoneExclusivity(options.addons, "addons");
2648
+ if (addonsResult.isErr()) return addonsResult;
2649
+ const examplesResult = validateNoneExclusivity(options.examples, "examples");
2650
+ if (examplesResult.isErr()) return examplesResult;
2651
+ const testingResult = validateNoneExclusivity(options.testing, "testing");
2652
+ if (testingResult.isErr()) return testingResult;
2653
+ return Result.ok(void 0);
3770
2654
  }
3771
2655
  //#endregion
3772
2656
  //#region src/helpers/core/detect-project-config.ts
3773
2657
  async function detectProjectConfig(projectDir) {
3774
2658
  const result = await Result.tryPromise({
3775
2659
  try: async () => {
3776
- const btsConfig = await readBtsConfig(projectDir);
3777
- if (btsConfig) return {
2660
+ const kubojsConfig = await readKubojsConfig(projectDir);
2661
+ if (kubojsConfig) return {
3778
2662
  projectDir,
3779
2663
  projectName: path.basename(projectDir),
3780
- addonOptions: btsConfig.addonOptions,
3781
- dbSetupOptions: btsConfig.dbSetupOptions,
3782
- database: btsConfig.database,
3783
- orm: btsConfig.orm,
3784
- backend: btsConfig.backend,
3785
- runtime: btsConfig.runtime,
3786
- frontend: btsConfig.frontend,
3787
- addons: btsConfig.addons,
3788
- examples: btsConfig.examples,
3789
- auth: btsConfig.auth,
3790
- payments: btsConfig.payments,
3791
- observability: btsConfig.observability,
3792
- packageManager: btsConfig.packageManager,
3793
- dbSetup: btsConfig.dbSetup,
3794
- api: btsConfig.api,
3795
- webDeploy: btsConfig.webDeploy,
3796
- serverDeploy: btsConfig.serverDeploy
2664
+ addonOptions: kubojsConfig.addonOptions,
2665
+ dbSetupOptions: kubojsConfig.dbSetupOptions,
2666
+ database: kubojsConfig.database,
2667
+ orm: kubojsConfig.orm,
2668
+ backend: kubojsConfig.backend,
2669
+ runtime: kubojsConfig.runtime,
2670
+ frontend: kubojsConfig.frontend,
2671
+ addons: kubojsConfig.addons,
2672
+ examples: kubojsConfig.examples,
2673
+ testing: kubojsConfig.testing,
2674
+ auth: kubojsConfig.auth,
2675
+ payments: kubojsConfig.payments,
2676
+ observability: normalizeObservability(kubojsConfig.observability),
2677
+ communication: kubojsConfig.communication ?? "none",
2678
+ packageManager: kubojsConfig.packageManager,
2679
+ dbSetup: kubojsConfig.dbSetup,
2680
+ api: kubojsConfig.api,
2681
+ webDeploy: kubojsConfig.webDeploy,
2682
+ serverDeploy: kubojsConfig.serverDeploy
3797
2683
  };
3798
2684
  return null;
3799
2685
  },
@@ -3832,9 +2718,8 @@ const ADD_PACKAGE_JSON_PATHS = [
3832
2718
  "apps/web/package.json",
3833
2719
  "apps/native/package.json",
3834
2720
  "apps/desktop/package.json",
3835
- "apps/fumadocs/package.json",
3836
2721
  "apps/docs/package.json",
3837
- "packages/api/package.json",
2722
+ "apps/api/package.json",
3838
2723
  "packages/db/package.json",
3839
2724
  "packages/auth/package.json",
3840
2725
  "packages/backend/package.json",
@@ -3844,17 +2729,21 @@ const ADD_PACKAGE_JSON_PATHS = [
3844
2729
  "packages/ui/package.json"
3845
2730
  ];
3846
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
+ ];
3847
2740
  const HOOK_ADDONS = ["husky", "lefthook"];
3848
2741
  const HOOK_LINTER_ADDONS = [
3849
2742
  "biome",
3850
2743
  "oxlint",
3851
2744
  "vite-plus"
3852
2745
  ];
3853
- const TASK_RUNNER_ADDONS = [
3854
- "turborepo",
3855
- "nx",
3856
- "vite-plus"
3857
- ];
2746
+ const TASK_RUNNER_ADDONS = ["turborepo", "vite-plus"];
3858
2747
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3859
2748
  if (!existingAddonOptions && !nextAddonOptions) return;
3860
2749
  const mergedAddonOptions = { ...existingAddonOptions };
@@ -3883,6 +2772,37 @@ function refreshLefthookTemplate(vfs, config) {
3883
2772
  if (!template) return;
3884
2773
  vfs.writeFile("lefthook.yml", processTemplateString(template, config));
3885
2774
  }
2775
+ /** Remove files/deps left by a replaced exclusive linter. */
2776
+ async function cleanupRemovedLinters(projectDir, removedAddons) {
2777
+ const removedLinters = removedAddons.filter(isLinterAddon);
2778
+ if (removedLinters.length === 0) return;
2779
+ const packageJsonPath = path.join(projectDir, "package.json");
2780
+ if (await fs.pathExists(packageJsonPath)) {
2781
+ const packageJson = await fs.readJson(packageJsonPath);
2782
+ packageJson.devDependencies = packageJson.devDependencies ?? {};
2783
+ for (const removed of removedLinters) {
2784
+ if (removed === "biome") delete packageJson.devDependencies["@biomejs/biome"];
2785
+ if (removed === "oxlint") {
2786
+ delete packageJson.devDependencies.oxlint;
2787
+ delete packageJson.devDependencies.oxfmt;
2788
+ }
2789
+ }
2790
+ if (typeof packageJson.scripts?.check === "string" && removedLinters.some((linter) => {
2791
+ if (linter === "biome") return packageJson.scripts.check.includes("biome");
2792
+ if (linter === "oxlint") return packageJson.scripts.check.includes("oxlint");
2793
+ return false;
2794
+ })) delete packageJson.scripts.check;
2795
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2796
+ }
2797
+ for (const removed of removedLinters) {
2798
+ if (removed === "biome") await fs.remove(path.join(projectDir, "biome.json"));
2799
+ if (removed === "oxlint") {
2800
+ await fs.remove(path.join(projectDir, ".oxlintrc.json"));
2801
+ await fs.remove(path.join(projectDir, ".oxfmtrc.json"));
2802
+ await fs.remove(path.join(projectDir, ".oxfmtrc.jsonc"));
2803
+ }
2804
+ }
2805
+ }
3886
2806
  function updateViteConfigImportsForVitePlus(vfs) {
3887
2807
  for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
3888
2808
  const content = vfs.readFile(viteConfigPath);
@@ -3925,10 +2845,10 @@ async function addHandlerInternal(input) {
3925
2845
  }));
3926
2846
  if (!isSilent()) {
3927
2847
  renderTitle();
3928
- intro(pc.magenta("Add addons to your I dont know project"));
2848
+ intro(cliColors.signal("Add addons to your kubojs project"));
3929
2849
  }
3930
2850
  const existingConfig = await detectProjectConfig(projectDir);
3931
- if (!existingConfig) return Result.err(new CLIError({ message: `No I dont know project found in ${projectDir}. Make sure bts.jsonc exists.` }));
2851
+ if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubojs.jsonrc exists.` }));
3932
2852
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
3933
2853
  let addonsToAdd;
3934
2854
  if (input.addons && input.addons.length > 0) {
@@ -3958,7 +2878,7 @@ async function addHandlerInternal(input) {
3958
2878
  if (selectedAddons.length === 0) {
3959
2879
  if (!isSilent()) {
3960
2880
  log.info(pc.dim("No addons selected."));
3961
- outro(pc.magenta("Nothing to add."));
2881
+ outro(cliColors.signal("Nothing to add."));
3962
2882
  }
3963
2883
  return Result.ok({
3964
2884
  success: true,
@@ -3968,10 +2888,14 @@ async function addHandlerInternal(input) {
3968
2888
  }
3969
2889
  addonsToAdd = selectedAddons;
3970
2890
  }
3971
- const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
2891
+ const { updatedAddons, removedAddons } = mergeAddonsExclusive(existingConfig.addons, addonsToAdd);
2892
+ const updatedTesting = existingConfig.testing ?? [];
3972
2893
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3973
2894
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3974
- if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
2895
+ if (!isSilent()) {
2896
+ log.info(cliColors.bright(`Adding addons: ${addonsToAdd.join(", ")}`));
2897
+ if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
2898
+ }
3975
2899
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
3976
2900
  const config = {
3977
2901
  projectName: existingConfig.projectName,
@@ -3988,6 +2912,8 @@ async function addHandlerInternal(input) {
3988
2912
  auth: existingConfig.auth,
3989
2913
  payments: existingConfig.payments,
3990
2914
  observability: existingConfig.observability,
2915
+ communication: existingConfig.communication ?? "none",
2916
+ testing: updatedTesting,
3991
2917
  git: false,
3992
2918
  packageManager: input.packageManager || existingConfig.packageManager,
3993
2919
  install: input.install ?? false,
@@ -4016,10 +2942,17 @@ async function addHandlerInternal(input) {
4016
2942
  vfs.writeFile(filePath, content);
4017
2943
  }
4018
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
+ }
4019
2952
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
4020
2953
  processAddonsDeps(vfs, config);
2954
+ processEnvVariables(vfs, updatedConfig);
4021
2955
  if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
4022
- if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
4023
2956
  if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
4024
2957
  if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
4025
2958
  if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
@@ -4034,7 +2967,7 @@ async function addHandlerInternal(input) {
4034
2967
  if (!isSilent()) {
4035
2968
  log.success(pc.green("Dry run validation passed. No addon files were written."));
4036
2969
  log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
4037
- outro(pc.magenta("Dry run complete."));
2970
+ outro(cliColors.signal("Dry run complete."));
4038
2971
  }
4039
2972
  return Result.ok({
4040
2973
  success: true,
@@ -4047,6 +2980,7 @@ async function addHandlerInternal(input) {
4047
2980
  const writeResult = await writeTree(tree, projectDir);
4048
2981
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
4049
2982
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
2983
+ await cleanupRemovedLinters(projectDir, removedAddons);
4050
2984
  const setupResult = await Result.tryPromise({
4051
2985
  try: () => setupAddons({
4052
2986
  ...config,
@@ -4061,9 +2995,18 @@ async function addHandlerInternal(input) {
4061
2995
  }
4062
2996
  });
4063
2997
  if (setupResult.isErr()) return Result.err(setupResult.error);
4064
- await updateBtsConfig(projectDir, {
2998
+ const testingSetupResult = await Result.tryPromise({
2999
+ try: () => setupTesting(projectDir, updatedTesting),
3000
+ catch: (e) => new CLIError({
3001
+ message: e instanceof Error ? e.message : String(e),
3002
+ cause: e
3003
+ })
3004
+ });
3005
+ if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
3006
+ await updateKubojsConfig(projectDir, {
4065
3007
  addons: updatedAddons,
4066
- addonOptions: updatedConfig.addonOptions
3008
+ addonOptions: updatedConfig.addonOptions,
3009
+ testing: updatedTesting
4067
3010
  });
4068
3011
  if (input.install) {
4069
3012
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
@@ -4075,7 +3018,7 @@ async function addHandlerInternal(input) {
4075
3018
  if (!isSilent()) {
4076
3019
  log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
4077
3020
  if (!input.install) log.info(pc.yellow(`Run '${config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`}' to install new dependencies.`));
4078
- outro(pc.magenta("Addons added successfully!"));
3021
+ outro(cliColors.signal("Addons added successfully!"));
4079
3022
  }
4080
3023
  return Result.ok({
4081
3024
  success: true,
@@ -4088,7 +3031,7 @@ async function addHandlerInternal(input) {
4088
3031
  //#region src/prompts/api.ts
4089
3032
  async function getApiChoice(Api, frontend, backend, previousValue) {
4090
3033
  if (backend === "convex" || backend === "none") return "none";
4091
- const allowed = allowedApisForFrontends(frontend ?? []);
3034
+ const allowed = allowedApisForFrontends(frontend ?? [], backend);
4092
3035
  if (Api) {
4093
3036
  const compat = validateApiFrontendCompatibility(Api, frontend ?? []);
4094
3037
  if (compat.isErr()) throw compat.error;
@@ -4102,6 +3045,10 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4102
3045
  value: "orpc",
4103
3046
  label: "oRPC",
4104
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"
4105
3052
  } : {
4106
3053
  value: "none",
4107
3054
  label: "None",
@@ -4112,7 +3059,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4112
3059
  options: apiOptions,
4113
3060
  initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
4114
3061
  });
4115
- if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
3062
+ if (isCancel$2(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
4116
3063
  return apiType;
4117
3064
  }
4118
3065
  //#endregion
@@ -4164,7 +3111,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
4164
3111
  options,
4165
3112
  initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4166
3113
  });
4167
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3114
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4168
3115
  return response;
4169
3116
  }
4170
3117
  //#endregion
@@ -4218,7 +3165,42 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
4218
3165
  options: backendOptions,
4219
3166
  initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4220
3167
  });
4221
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3168
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3169
+ return response;
3170
+ }
3171
+ //#endregion
3172
+ //#region src/prompts/communication.ts
3173
+ async function getCommunicationChoice(communication, backend, previousValue) {
3174
+ if (communication !== void 0) return communication;
3175
+ if (backend === "none") return "none";
3176
+ const options = [
3177
+ {
3178
+ value: "none",
3179
+ label: "None",
3180
+ hint: "No email/communication provider"
3181
+ },
3182
+ {
3183
+ value: "resend",
3184
+ label: "Resend",
3185
+ hint: "Transactional email for developers"
3186
+ },
3187
+ {
3188
+ value: "notifique",
3189
+ label: "Notifique",
3190
+ hint: "Omnichannel BR messaging API"
3191
+ },
3192
+ {
3193
+ value: "arara",
3194
+ label: "AraraHQ",
3195
+ hint: "WhatsApp messaging via the official Node SDK"
3196
+ }
3197
+ ];
3198
+ const response = await navigableSelect({
3199
+ message: "Select communication provider",
3200
+ options,
3201
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.communication)
3202
+ });
3203
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4222
3204
  return response;
4223
3205
  }
4224
3206
  //#endregion
@@ -4258,7 +3240,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4258
3240
  options: databaseOptions,
4259
3241
  initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4260
3242
  });
4261
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3243
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4262
3244
  return response;
4263
3245
  }
4264
3246
  //#endregion
@@ -4357,7 +3339,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
4357
3339
  options,
4358
3340
  initialValue: preferValidInitial(options, previousValue, "none")
4359
3341
  });
4360
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3342
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4361
3343
  return response;
4362
3344
  }
4363
3345
  //#endregion
@@ -4384,7 +3366,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
4384
3366
  required: false,
4385
3367
  initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
4386
3368
  });
4387
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3369
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4388
3370
  return response;
4389
3371
  }
4390
3372
  //#endregion
@@ -4420,7 +3402,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4420
3402
  initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
4421
3403
  });
4422
3404
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
4423
- if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
3405
+ if (isCancel$2(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
4424
3406
  setIsFirstPrompt(false);
4425
3407
  const result = [];
4426
3408
  let shouldRestart = false;
@@ -4473,7 +3455,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4473
3455
  initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4474
3456
  });
4475
3457
  if (isGoBack(webFramework)) shouldRestart = true;
4476
- else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3458
+ else if (isCancel$2(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4477
3459
  else result.push(webFramework);
4478
3460
  }
4479
3461
  if (shouldRestart) {
@@ -4507,7 +3489,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4507
3489
  setIsFirstPrompt(wasFirstPrompt);
4508
3490
  continue;
4509
3491
  }
4510
- else if (isCancel$1(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3492
+ else if (isCancel$2(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4511
3493
  else result.push(nativeFramework);
4512
3494
  }
4513
3495
  if (shouldRestart) {
@@ -4525,7 +3507,7 @@ async function getGitChoice(git, previousValue) {
4525
3507
  message: "Initialize git repository?",
4526
3508
  initialValue: previousValue ?? DEFAULT_CONFIG.git
4527
3509
  });
4528
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3510
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4529
3511
  return response;
4530
3512
  }
4531
3513
  //#endregion
@@ -4536,7 +3518,7 @@ async function getinstallChoice(install, previousValue) {
4536
3518
  message: "Install dependencies?",
4537
3519
  initialValue: previousValue ?? DEFAULT_CONFIG.install
4538
3520
  });
4539
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3521
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4540
3522
  return response;
4541
3523
  }
4542
3524
  //#endregion
@@ -4544,20 +3526,21 @@ async function getinstallChoice(install, previousValue) {
4544
3526
  const options = [{
4545
3527
  value: "getmonitor",
4546
3528
  label: "GetMonitor",
4547
- hint: "Uptime monitoring, alerts, and hosted status pages"
3529
+ hint: "Recommended — JS/TS error tracking for browser and server"
4548
3530
  }, {
4549
- value: "none",
4550
- label: "None",
4551
- hint: "Skip observability setup guidance"
3531
+ value: "himetrica",
3532
+ label: "Himetrica",
3533
+ hint: "Product analytics, errors, and Web Vitals for web apps"
4552
3534
  }];
4553
3535
  async function getObservabilityChoice(observability, previousValue) {
4554
3536
  if (observability !== void 0) return observability;
4555
- const response = await navigableSelect({
4556
- message: "Select observability provider",
3537
+ const response = await navigableMultiselect({
3538
+ message: "Select observability providers",
4557
3539
  options,
4558
- initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.observability)
3540
+ required: false,
3541
+ initialValues: previousValue ?? DEFAULT_CONFIG.observability
4559
3542
  });
4560
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3543
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4561
3544
  return response;
4562
3545
  }
4563
3546
  //#endregion
@@ -4675,7 +3658,8 @@ function validateBackendNoneConstraints(config, providedFlags) {
4675
3658
  if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
4676
3659
  if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
4677
3660
  if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
4678
- 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'.");
3662
+ if (has("communication") && config.communication !== "none") return validationErr("Backend 'none' requires '--communication none'. Please remove the --communication flag or set it to 'none'.");
4679
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'.");
4680
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'.");
4681
3665
  return Result.ok(void 0);
@@ -4723,6 +3707,8 @@ function validateFrontendConstraints(config, providedFlags) {
4723
3707
  return Result.ok(void 0);
4724
3708
  }
4725
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.");
4726
3712
  if (config.api === "none") {
4727
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.");
4728
3714
  }
@@ -4737,6 +3723,7 @@ function validateFullConfig(config, providedFlags, options) {
4737
3723
  yield* validateSelfBackendConstraints(config, providedFlags);
4738
3724
  yield* validateBackendConstraints(config, providedFlags, options);
4739
3725
  yield* validateFrontendConstraints(config, providedFlags);
3726
+ yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4740
3727
  yield* validateApiConstraints(config, options);
4741
3728
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
4742
3729
  yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
@@ -4753,6 +3740,8 @@ function validateFullConfig(config, providedFlags, options) {
4753
3740
  }
4754
3741
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4755
3742
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? [], config.orm, config.database);
3743
+ yield* validateCommunicationCompatibility(config.communication, config.backend);
3744
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4756
3745
  return Result.ok(void 0);
4757
3746
  });
4758
3747
  }
@@ -4761,7 +3750,10 @@ function validateConfigForProgrammaticUse(config) {
4761
3750
  yield* validateDatabaseOrmAuth(config);
4762
3751
  if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
4763
3752
  yield* validateApiFrontendCompatibility(config.api, config.frontend);
3753
+ yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4764
3754
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend, config.orm, config.database);
3755
+ yield* validateCommunicationCompatibility(config.communication, config.backend);
3756
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4765
3757
  if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
4766
3758
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4767
3759
  return Result.ok(void 0);
@@ -4800,7 +3792,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
4800
3792
  options,
4801
3793
  initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4802
3794
  });
4803
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3795
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4804
3796
  return response;
4805
3797
  }
4806
3798
  //#endregion
@@ -4829,47 +3821,48 @@ async function getPackageManagerChoice(packageManager, previousValue) {
4829
3821
  ],
4830
3822
  initialValue: previousValue ?? detectedPackageManager
4831
3823
  });
4832
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3824
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4833
3825
  return response;
4834
3826
  }
4835
3827
  //#endregion
4836
3828
  //#region src/prompts/payments.ts
4837
3829
  async function getPaymentsChoice(payments, auth, backend, frontends = [], previousValue) {
4838
3830
  if (payments !== void 0) return payments;
4839
- if (backend === "none") return "none";
3831
+ if (backend === "none") return [];
4840
3832
  const options = [{
4841
- value: "none",
4842
- label: "None",
4843
- hint: "No payments integration"
4844
- }, {
4845
3833
  value: "abacatepay",
4846
3834
  label: "AbacatePay",
4847
3835
  hint: "Hosted checkout with webhook reconciliation"
3836
+ }, {
3837
+ value: "stripe",
3838
+ label: "Stripe",
3839
+ hint: "Embedded Checkout with a fulfillment webhook"
4848
3840
  }];
4849
- const abacatePayDisabledReason = !frontends.some((frontend) => [
4850
- "tanstack-router",
4851
- "react-router",
4852
- "tanstack-start",
4853
- "next",
4854
- "nuxt",
4855
- "svelte",
4856
- "solid",
4857
- "astro"
4858
- ].includes(frontend)) ? "Requires a web frontend" : backend === "convex" ? "Not supported with Convex" : void 0;
4859
- const selectableOptions = options.map((option) => {
4860
- if (option.value !== "abacatepay") return option;
4861
- return {
4862
- ...option,
4863
- hint: abacatePayDisabledReason ?? option.hint,
4864
- disabled: abacatePayDisabledReason !== void 0
4865
- };
4866
- });
4867
- const response = await navigableSelect({
4868
- message: "Select payments provider",
4869
- options: selectableOptions,
4870
- 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
4871
3864
  });
4872
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3865
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4873
3866
  return response;
4874
3867
  }
4875
3868
  //#endregion
@@ -4896,7 +3889,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
4896
3889
  options: runtimeOptions,
4897
3890
  initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4898
3891
  });
4899
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3892
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4900
3893
  return response;
4901
3894
  }
4902
3895
  //#endregion
@@ -4954,7 +3947,30 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
4954
3947
  options,
4955
3948
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
4956
3949
  });
4957
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3950
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3951
+ return response;
3952
+ }
3953
+ //#endregion
3954
+ //#region src/prompts/testing.ts
3955
+ async function getTestingChoice(testing, frontends, previousValue) {
3956
+ if (testing !== void 0) return testing;
3957
+ const options = [{
3958
+ value: "vitest",
3959
+ label: "Vitest",
3960
+ hint: "Fast unit test runner"
3961
+ }];
3962
+ if (isPlaywrightAllowed(frontends ?? [])) options.push({
3963
+ value: "playwright",
3964
+ label: "Playwright",
3965
+ hint: "End-to-end browser testing"
3966
+ });
3967
+ const response = await navigableMultiselect({
3968
+ message: "Select testing tools",
3969
+ options,
3970
+ required: false,
3971
+ initialValues: (previousValue ?? DEFAULT_CONFIG.testing)?.filter((t) => options.some((o) => o.value === t))
3972
+ });
3973
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4958
3974
  return response;
4959
3975
  }
4960
3976
  //#endregion
@@ -5007,7 +4023,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
5007
4023
  options,
5008
4024
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
5009
4025
  });
5010
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4026
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5011
4027
  return response;
5012
4028
  }
5013
4029
  //#endregion
@@ -5027,8 +4043,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5027
4043
  auth: flags.auth ?? DEFAULT_CONFIG.auth,
5028
4044
  payments: flags.payments ?? DEFAULT_CONFIG.payments,
5029
4045
  observability: flags.observability ?? DEFAULT_CONFIG.observability,
4046
+ communication: flags.communication ?? DEFAULT_CONFIG.communication,
5030
4047
  addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
5031
4048
  examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
4049
+ testing: flags.testing ?? [...DEFAULT_CONFIG.testing],
5032
4050
  git: flags.git ?? DEFAULT_CONFIG.git,
5033
4051
  packageManager: flags.packageManager ?? DEFAULT_CONFIG.packageManager,
5034
4052
  install: flags.install ?? DEFAULT_CONFIG.install,
@@ -5047,6 +4065,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5047
4065
  auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
5048
4066
  payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
5049
4067
  observability: ({ previousAnswer }) => getObservabilityChoice(flags.observability, previousAnswer),
4068
+ communication: ({ results, previousAnswer }) => getCommunicationChoice(flags.communication, results.backend, previousAnswer),
4069
+ testing: ({ results, previousAnswer }) => getTestingChoice(flags.testing, results.frontend, previousAnswer),
5050
4070
  addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
5051
4071
  examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
5052
4072
  dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
@@ -5072,8 +4092,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5072
4092
  auth: result.auth,
5073
4093
  payments: result.payments,
5074
4094
  observability: result.observability,
4095
+ communication: result.communication,
5075
4096
  addons: result.addons,
5076
4097
  examples: result.examples,
4098
+ testing: result.testing,
5077
4099
  git: result.git,
5078
4100
  packageManager: result.packageManager,
5079
4101
  install: result.install,
@@ -5085,6 +4107,32 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5085
4107
  }
5086
4108
  //#endregion
5087
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
+ }
5088
4136
  function isPathWithinCwd$1(targetPath) {
5089
4137
  const resolved = path.resolve(targetPath);
5090
4138
  const rel = path.relative(process.cwd(), resolved);
@@ -5112,7 +4160,7 @@ async function getProjectName(initialName) {
5112
4160
  counter++;
5113
4161
  }
5114
4162
  while (!isValid) {
5115
- const response = await text({
4163
+ const response = await kuboText({
5116
4164
  message: "Enter your project name or path (relative to current directory)",
5117
4165
  placeholder: defaultName,
5118
4166
  initialValue: initialName,
@@ -5126,7 +4174,7 @@ async function getProjectName(initialName) {
5126
4174
  }
5127
4175
  }
5128
4176
  });
5129
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
4177
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled." });
5130
4178
  projectPath = response || defaultName;
5131
4179
  isValid = true;
5132
4180
  }
@@ -5167,42 +4215,48 @@ async function trackProjectCreation(config, disableAnalytics = false) {
5167
4215
  //#region src/utils/display-config.ts
5168
4216
  function displayConfig(config) {
5169
4217
  const configDisplay = [];
5170
- if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
4218
+ if (config.projectName) configDisplay.push(`${cliColors.signal("Project Name:")} ${config.projectName}`);
5171
4219
  if (config.frontend !== void 0) {
5172
4220
  const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
5173
4221
  const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
5174
- configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
5175
- }
5176
- if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
5177
- if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
5178
- if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
5179
- if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
5180
- if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
5181
- if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
5182
- if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
5183
- if (config.observability !== void 0) configDisplay.push(`${pc.blue("Observability:")} ${String(config.observability)}`);
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"}`);
5184
4233
  if (config.addons !== void 0) {
5185
4234
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
5186
4235
  const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
5187
- configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
4236
+ configDisplay.push(`${cliColors.signal("Addons:")} ${addonsText}`);
5188
4237
  }
5189
4238
  if (config.examples !== void 0) {
5190
4239
  const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
5191
4240
  const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
5192
- configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
4241
+ configDisplay.push(`${cliColors.signal("Examples:")} ${examplesText}`);
4242
+ }
4243
+ if (config.testing !== void 0) {
4244
+ const testing = Array.isArray(config.testing) ? config.testing : [config.testing];
4245
+ const testingText = testing.length > 0 && testing[0] !== void 0 ? testing.join(", ") : "none";
4246
+ configDisplay.push(`${cliColors.signal("Testing:")} ${testingText}`);
5193
4247
  }
5194
4248
  if (config.git !== void 0) {
5195
4249
  const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
5196
- configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
4250
+ configDisplay.push(`${cliColors.signal("Git Init:")} ${gitText}`);
5197
4251
  }
5198
- 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)}`);
5199
4253
  if (config.install !== void 0) {
5200
4254
  const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
5201
- configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
4255
+ configDisplay.push(`${cliColors.signal("Install Dependencies:")} ${installText}`);
5202
4256
  }
5203
- if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
5204
- if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
5205
- 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)}`);
5206
4260
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
5207
4261
  return configDisplay.join("\n");
5208
4262
  }
@@ -5332,8 +4386,9 @@ const TEMPLATE_PRESETS = {
5332
4386
  frontend: ["react-router"],
5333
4387
  api: "orpc",
5334
4388
  auth: "better-auth",
5335
- payments: "none",
5336
- observability: "none",
4389
+ payments: [],
4390
+ observability: ["getmonitor"],
4391
+ communication: "none",
5337
4392
  addons: ["turborepo"],
5338
4393
  examples: ["todo"],
5339
4394
  dbSetup: "mongodb-atlas",
@@ -5348,8 +4403,9 @@ const TEMPLATE_PRESETS = {
5348
4403
  frontend: ["tanstack-router"],
5349
4404
  api: "trpc",
5350
4405
  auth: "better-auth",
5351
- payments: "none",
5352
- observability: "none",
4406
+ payments: [],
4407
+ observability: ["getmonitor"],
4408
+ communication: "none",
5353
4409
  addons: ["turborepo"],
5354
4410
  examples: ["todo"],
5355
4411
  dbSetup: "none",
@@ -5364,8 +4420,9 @@ const TEMPLATE_PRESETS = {
5364
4420
  frontend: ["next"],
5365
4421
  api: "trpc",
5366
4422
  auth: "better-auth",
5367
- payments: "none",
5368
- observability: "none",
4423
+ payments: [],
4424
+ observability: ["getmonitor"],
4425
+ communication: "none",
5369
4426
  addons: ["biome", "turborepo"],
5370
4427
  examples: ["none"],
5371
4428
  dbSetup: "none",
@@ -5380,8 +4437,9 @@ const TEMPLATE_PRESETS = {
5380
4437
  frontend: ["native-uniwind"],
5381
4438
  api: "none",
5382
4439
  auth: "none",
5383
- payments: "none",
5384
- observability: "none",
4440
+ payments: [],
4441
+ observability: [],
4442
+ communication: "none",
5385
4443
  addons: ["none"],
5386
4444
  examples: ["none"],
5387
4445
  dbSetup: "none",
@@ -5406,60 +4464,6 @@ function getTemplateDescription(template) {
5406
4464
  }[template] || "";
5407
4465
  }
5408
4466
  //#endregion
5409
- //#region src/utils/config-processing.ts
5410
- function processArrayOption(options) {
5411
- if (!options || options.length === 0) return [];
5412
- if (options.includes("none")) return [];
5413
- return options.filter((item) => item !== "none");
5414
- }
5415
- function deriveProjectName(projectName, projectDirectory) {
5416
- if (projectName) return projectName;
5417
- if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
5418
- return "";
5419
- }
5420
- function processFlags(options, projectName) {
5421
- const config = {};
5422
- if (options.api) config.api = options.api;
5423
- if (options.addonOptions) config.addonOptions = options.addonOptions;
5424
- if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
5425
- if (options.backend) config.backend = options.backend;
5426
- if (options.database) config.database = options.database;
5427
- if (options.orm) config.orm = options.orm;
5428
- if (options.auth !== void 0) config.auth = options.auth;
5429
- if (options.payments !== void 0) config.payments = options.payments;
5430
- if (options.observability !== void 0) config.observability = options.observability;
5431
- if (options.git !== void 0) config.git = options.git;
5432
- if (options.install !== void 0) config.install = options.install;
5433
- if (options.runtime) config.runtime = options.runtime;
5434
- if (options.dbSetup) config.dbSetup = options.dbSetup;
5435
- if (options.packageManager) config.packageManager = options.packageManager;
5436
- if (options.webDeploy) config.webDeploy = options.webDeploy;
5437
- if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
5438
- const derivedName = deriveProjectName(projectName, options.projectDirectory);
5439
- if (derivedName) config.projectName = projectName || derivedName;
5440
- if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
5441
- if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
5442
- if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
5443
- return config;
5444
- }
5445
- function getProvidedFlags(options) {
5446
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
5447
- }
5448
- function validateNoneExclusivity(options, optionName) {
5449
- if (!options || options.length === 0) return Result.ok(void 0);
5450
- if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
5451
- return Result.ok(void 0);
5452
- }
5453
- function validateArrayOptions(options) {
5454
- const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
5455
- if (frontendResult.isErr()) return frontendResult;
5456
- const addonsResult = validateNoneExclusivity(options.addons, "addons");
5457
- if (addonsResult.isErr()) return addonsResult;
5458
- const examplesResult = validateNoneExclusivity(options.examples, "examples");
5459
- if (examplesResult.isErr()) return examplesResult;
5460
- return Result.ok(void 0);
5461
- }
5462
- //#endregion
5463
4467
  //#region src/validation.ts
5464
4468
  const CORE_STACK_FLAGS = new Set([
5465
4469
  "database",
@@ -5469,10 +4473,13 @@ const CORE_STACK_FLAGS = new Set([
5469
4473
  "frontend",
5470
4474
  "addons",
5471
4475
  "examples",
4476
+ "testing",
5472
4477
  "auth",
5473
4478
  "dbSetup",
5474
4479
  "payments",
5475
4480
  "observability",
4481
+ "disableObservability",
4482
+ "communication",
5476
4483
  "api",
5477
4484
  "webDeploy",
5478
4485
  "serverDeploy"
@@ -5525,6 +4532,49 @@ const formatOptions = {
5525
4532
  experimentalSortPackageJson: true,
5526
4533
  experimentalSortImports: { order: "asc" }
5527
4534
  };
4535
+ /** Files that must not be rewritten by post-scaffold formatters. */
4536
+ const SKIP_FILE_NAMES = new Set([
4537
+ "kubojs.jsonrc",
4538
+ "package-lock.json",
4539
+ "pnpm-lock.yaml",
4540
+ "yarn.lock",
4541
+ "bun.lock",
4542
+ "bun.lockb",
4543
+ "Cargo.lock",
4544
+ "composer.lock"
4545
+ ]);
4546
+ const SKIP_EXTENSIONS = new Set([
4547
+ ".png",
4548
+ ".jpg",
4549
+ ".jpeg",
4550
+ ".gif",
4551
+ ".webp",
4552
+ ".ico",
4553
+ ".svg",
4554
+ ".woff",
4555
+ ".woff2",
4556
+ ".ttf",
4557
+ ".eot",
4558
+ ".mp4",
4559
+ ".mp3",
4560
+ ".wasm",
4561
+ ".zip",
4562
+ ".gz",
4563
+ ".tgz",
4564
+ ".br",
4565
+ ".pdf",
4566
+ ".db",
4567
+ ".sqlite"
4568
+ ]);
4569
+ function shouldSkipFile(fileName) {
4570
+ if (SKIP_FILE_NAMES.has(fileName)) return true;
4571
+ const ext = path.extname(fileName).toLowerCase();
4572
+ return SKIP_EXTENSIONS.has(ext);
4573
+ }
4574
+ function shouldUseBiomeFormatter(addons) {
4575
+ if (!addons) return false;
4576
+ return addons.includes("biome");
4577
+ }
5528
4578
  async function formatCode(filePath, content) {
5529
4579
  const result = await Result.tryPromise({
5530
4580
  try: async () => {
@@ -5536,25 +4586,61 @@ async function formatCode(filePath, content) {
5536
4586
  });
5537
4587
  return result.isOk() ? result.value : null;
5538
4588
  }
5539
- async function formatProject(projectDir) {
4589
+ async function formatProjectWithOxfmt(projectDir) {
4590
+ async function formatDirectory(dir) {
4591
+ const entries = await fs.readdir(dir, { withFileTypes: true });
4592
+ await Promise.all(entries.map(async (entry) => {
4593
+ const fullPath = path.join(dir, entry.name);
4594
+ if (entry.isDirectory()) {
4595
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === ".turbo") return;
4596
+ await formatDirectory(fullPath);
4597
+ } else if (entry.isFile()) {
4598
+ if (shouldSkipFile(entry.name)) return;
4599
+ await Result.tryPromise({
4600
+ try: async () => {
4601
+ const content = await fs.readFile(fullPath, "utf-8");
4602
+ const formatted = await formatCode(fullPath, content);
4603
+ if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
4604
+ },
4605
+ catch: () => void 0
4606
+ });
4607
+ }
4608
+ }));
4609
+ }
4610
+ await formatDirectory(projectDir);
4611
+ }
4612
+ /**
4613
+ * Format with Biome so scaffold matches `biome check` (source of truth).
4614
+ * Uses bunx so it works before local install. Best-effort: failures are ignored
4615
+ * so offline CI/tests still scaffold.
4616
+ */
4617
+ async function formatProjectWithBiome(projectDir) {
4618
+ if (shouldSkipExternalCommands()) return;
4619
+ await Result.tryPromise({
4620
+ try: async () => {
4621
+ await $({
4622
+ cwd: projectDir,
4623
+ reject: false,
4624
+ stdio: "pipe"
4625
+ })`bunx --bun @biomejs/biome@2.5.8 check --write .`;
4626
+ },
4627
+ catch: () => void 0
4628
+ });
4629
+ }
4630
+ /**
4631
+ * Post-scaffold format.
4632
+ * - biome → Biome `check --write` (not oxfmt)
4633
+ * - oxlint / none → oxfmt
4634
+ * Always skips kubojs.jsonrc, lockfiles, and binary extensions (oxfmt path).
4635
+ */
4636
+ async function formatProject(projectDir, options = {}) {
5540
4637
  return Result.tryPromise({
5541
4638
  try: async () => {
5542
- async function formatDirectory(dir) {
5543
- const entries = await fs.readdir(dir, { withFileTypes: true });
5544
- await Promise.all(entries.map(async (entry) => {
5545
- const fullPath = path.join(dir, entry.name);
5546
- if (entry.isDirectory()) await formatDirectory(fullPath);
5547
- else if (entry.isFile()) await Result.tryPromise({
5548
- try: async () => {
5549
- const content = await fs.readFile(fullPath, "utf-8");
5550
- const formatted = await formatCode(fullPath, content);
5551
- if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
5552
- },
5553
- catch: () => void 0
5554
- });
5555
- }));
4639
+ if (shouldUseBiomeFormatter(options.addons)) {
4640
+ await formatProjectWithBiome(projectDir);
4641
+ return;
5556
4642
  }
5557
- await formatDirectory(projectDir);
4643
+ await formatProjectWithOxfmt(projectDir);
5558
4644
  },
5559
4645
  catch: (e) => new ProjectCreationError({
5560
4646
  phase: "formatting",
@@ -5564,6 +4650,57 @@ async function formatProject(projectDir) {
5564
4650
  });
5565
4651
  }
5566
4652
  //#endregion
4653
+ //#region src/utils/generate-route-tree.ts
4654
+ const TANSTACK_ROUTE_TREE_FRONTENDS = [
4655
+ "tanstack-router",
4656
+ "tanstack-start",
4657
+ "solid"
4658
+ ];
4659
+ function resolveRouteTreeTarget(frontend) {
4660
+ if (frontend.includes("solid")) return "solid";
4661
+ if (frontend.includes("tanstack-router") || frontend.includes("tanstack-start")) return "react";
4662
+ return null;
4663
+ }
4664
+ function needsRouteTree(frontend) {
4665
+ return frontend.some((value) => TANSTACK_ROUTE_TREE_FRONTENDS.includes(value));
4666
+ }
4667
+ /**
4668
+ * Generate apps/web/src/routeTree.gen.ts from the scaffolded file routes.
4669
+ * TanStack recommends committing this file (runtime source, not a build artifact).
4670
+ * Running at create-time makes `check-types` pass without a prior vite build.
4671
+ */
4672
+ async function generateRouteTreeIfNeeded(projectDir, config) {
4673
+ if (!needsRouteTree(config.frontend)) return Result.ok(void 0);
4674
+ const target = resolveRouteTreeTarget(config.frontend);
4675
+ if (!target) return Result.ok(void 0);
4676
+ const webAppDir = path.join(projectDir, "apps/web");
4677
+ const routesDir = path.join(webAppDir, "src/routes");
4678
+ if (!await fs.pathExists(routesDir)) return Result.ok(void 0);
4679
+ return Result.tryPromise({
4680
+ try: async () => {
4681
+ await new Generator({
4682
+ config: configSchema.parse({
4683
+ target,
4684
+ routesDirectory: path.join(webAppDir, "src/routes"),
4685
+ generatedRouteTree: path.join(webAppDir, "src/routeTree.gen.ts"),
4686
+ tmpDir: path.join(webAppDir, ".tanstack/tmp"),
4687
+ autoCodeSplitting: true,
4688
+ disableLogging: true,
4689
+ enableRouteTreeFormatting: true,
4690
+ quoteStyle: "single",
4691
+ semicolons: false
4692
+ }),
4693
+ root: webAppDir
4694
+ }).run();
4695
+ },
4696
+ catch: (e) => new ProjectCreationError({
4697
+ phase: "route-tree-generation",
4698
+ message: `Failed to generate TanStack route tree: ${e instanceof Error ? e.message : String(e)}`,
4699
+ cause: e
4700
+ })
4701
+ });
4702
+ }
4703
+ //#endregion
5567
4704
  //#region src/utils/env-utils.ts
5568
4705
  async function addEnvVariablesToFile(envPath, variables) {
5569
4706
  let content = "";
@@ -5940,7 +5077,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
5940
5077
  }
5941
5078
  async function setupWithNeonDb(projectDir, packageManager, backend) {
5942
5079
  const s = createSpinner();
5943
- s.start("Creating Neon database using get-db...");
5080
+ s.start("Creating Neon database using neon-new...");
5944
5081
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
5945
5082
  const targetDir = path.join(projectDir, targetApp);
5946
5083
  const ensureDirResult = await Result.tryPromise({
@@ -5955,17 +5092,17 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
5955
5092
  s.stop(pc.red("Failed to create directory"));
5956
5093
  return ensureDirResult;
5957
5094
  }
5958
- const packageArgs = getPackageExecutionArgs(packageManager, `get-db@latest --yes --ref "sbA3tIe"`);
5095
+ const packageArgs = getPackageExecutionArgs(packageManager, `neon-new@latest --yes --ref "sbA3tIe"`);
5959
5096
  return Result.tryPromise({
5960
5097
  try: async () => {
5961
5098
  await $({ cwd: targetDir })`${packageArgs}`;
5962
5099
  s.stop(pc.green("Neon database created successfully!"));
5963
5100
  },
5964
5101
  catch: (e) => {
5965
- s.stop(pc.red("Failed to create database with get-db"));
5102
+ s.stop(pc.red("Failed to create database with neon-new"));
5966
5103
  return new DatabaseSetupError({
5967
5104
  provider: "neon",
5968
- 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)}`,
5969
5106
  cause: e
5970
5107
  });
5971
5108
  }
@@ -5974,7 +5111,7 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
5974
5111
  function displayManualSetupInstructions$2(target) {
5975
5112
  cliLog.info(`Manual Neon PostgreSQL Setup Instructions:
5976
5113
 
5977
- 1. Get Neon with I dont know referral: https://get.neon.com/sbA3tIe
5114
+ 1. Get Neon with kubojs referral: https://get.neon.com/sbA3tIe
5978
5115
  2. Create a new project from the dashboard
5979
5116
  3. Get your connection string
5980
5117
  4. Add the database URL to the .env file in ${target}/.env
@@ -6031,7 +5168,7 @@ async function setupNeonPostgres(config, cliInput) {
6031
5168
  const promptedSetupMethod = await select({
6032
5169
  message: "Choose your Neon setup method:",
6033
5170
  options: [{
6034
- label: "Quick setup with get-db",
5171
+ label: "Quick setup with neon-new",
6035
5172
  value: "neondb",
6036
5173
  hint: "fastest, no auth required"
6037
5174
  }, {
@@ -6053,7 +5190,7 @@ async function setupNeonPostgres(config, cliInput) {
6053
5190
  displayManualSetupInstructions$2(target);
6054
5191
  return Result.ok(void 0);
6055
5192
  }
6056
- cliLog.info(`Get Neon with I dont know referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
5193
+ cliLog.info(`Get Neon with kubojs referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6057
5194
  return Result.ok(void 0);
6058
5195
  }
6059
5196
  const suggestedProjectName = path.basename(projectDir);
@@ -6095,7 +5232,7 @@ async function setupNeonPostgres(config, cliInput) {
6095
5232
  return envResult;
6096
5233
  }
6097
5234
  finalSpinner.stop("Neon database configured!");
6098
- cliLog.info(`Get Neon with I dont know referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
5235
+ cliLog.info(`Get Neon with kubojs referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6099
5236
  return Result.ok(void 0);
6100
5237
  }
6101
5238
  //#endregion
@@ -6196,7 +5333,7 @@ const AVAILABLE_REGIONS = [
6196
5333
  label: "US West (N. California)"
6197
5334
  }
6198
5335
  ];
6199
- const CREATE_DB_USER_AGENT = "aman/kubojs";
5336
+ const CREATE_DB_USER_AGENT = "kubojs/kubojs";
6200
5337
  async function setupWithCreateDb(serverDir, packageManager, regionId) {
6201
5338
  cliLog.info("Starting Prisma Postgres setup with create-db.");
6202
5339
  let selectedRegion = regionId;
@@ -6963,6 +6100,10 @@ async function getDockerStatus(database) {
6963
6100
  }
6964
6101
  //#endregion
6965
6102
  //#region src/helpers/core/post-installation.ts
6103
+ const pc$1 = {
6104
+ ...pc,
6105
+ cyan: cliColors.signal
6106
+ };
6966
6107
  function getDesktopStaticBuildNote(frontend) {
6967
6108
  const staticBuildFrontends = new Map([
6968
6109
  ["tanstack-start", "TanStack Start"],
@@ -6973,10 +6114,10 @@ function getDesktopStaticBuildNote(frontend) {
6973
6114
  ]);
6974
6115
  const staticBuildFrontend = frontend.find((value) => staticBuildFrontends.has(value));
6975
6116
  if (!staticBuildFrontend) return "";
6976
- 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.`;
6977
6118
  }
6978
6119
  async function displayPostInstallInstructions(config) {
6979
- const { api, database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability } = config;
6120
+ const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication } = config;
6980
6121
  const isConvex = backend === "convex";
6981
6122
  const isBackendSelf = backend === "self";
6982
6123
  const runCmd = packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
@@ -6995,50 +6136,52 @@ async function displayPostInstallInstructions(config) {
6995
6136
  const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
6996
6137
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
6997
6138
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
6998
- const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
6139
+ const playwrightInstructions = testing?.includes("playwright") ? getPlaywrightInstructions(runCmd, packageManager) : "";
6999
6140
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
7000
6141
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
7001
- const getMonitorInstructions = observability === "getmonitor" ? getGetMonitorInstructions() : "";
6142
+ const getMonitorInstructions = observability.includes("getmonitor") ? getGetMonitorInstructions() : "";
6143
+ const resendInstructions = communication === "resend" ? getResendInstructions() : "";
6144
+ const notifiqueInstructions = communication === "notifique" ? getNotifiqueInstructions() : "";
6145
+ const araraInstructions = communication === "arara" ? getAraraInstructions(backend) : "";
7002
6146
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
7003
6147
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
7004
- const hasReactRouter = frontend?.includes("react-router");
7005
- const hasSvelte = frontend?.includes("svelte");
7006
- const hasAstro = frontend?.includes("astro");
7007
- const webPort = hasReactRouter || hasSvelte ? "5173" : hasAstro ? "4321" : "3001";
6148
+ const webPort = (0, types_exports.getWebPort)(frontend ?? []);
7008
6149
  const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager, runCmd) : "";
7009
6150
  const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
7010
6151
  const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
7011
- 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`;
7012
6153
  let stepCounter = 2;
7013
- if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
7014
- if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\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`;
7015
6156
  if (isConvex) {
7016
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(" (this will guide you through Convex project setup)")}\n`;
7017
- output += `${pc.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc.white(" packages/backend/.env.local")} to ${pc.white("apps/*/.env")}\n`;
7018
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
7019
- } 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`;
7020
6161
  else {
7021
- if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
6162
+ if (runtime !== "workers") output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7022
6163
  if (runtime === "workers") {
7023
- if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
7024
- 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`;
7025
6166
  }
7026
6167
  }
7027
6168
  const hasStandaloneBackend = backend !== "none";
7028
- if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
7029
- output += `${pc.bold("Your project will be available at:")}\n`;
7030
- if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
7031
- 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`;
7032
6173
  if (!isConvex && !isBackendSelf && hasStandaloneBackend) {
7033
- output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
7034
- 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
+ }
7035
6180
  }
7036
6181
  if (isBackendSelf && api === "orpc") {
7037
6182
  const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
7038
- 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`;
7039
6184
  }
7040
- if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
7041
- if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
7042
6185
  }
7043
6186
  if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
7044
6187
  if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
@@ -7049,18 +6192,18 @@ async function displayPostInstallInstructions(config) {
7049
6192
  if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
7050
6193
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
7051
6194
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
7052
- if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
6195
+ if (playwrightInstructions) output += `\n${playwrightInstructions.trim()}\n`;
7053
6196
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
7054
6197
  if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
7055
6198
  if (getMonitorInstructions) output += `\n${getMonitorInstructions.trim()}\n`;
6199
+ if (resendInstructions) output += `\n${resendInstructions.trim()}\n`;
6200
+ if (notifiqueInstructions) output += `\n${notifiqueInstructions.trim()}\n`;
6201
+ if (araraInstructions) output += `\n${araraInstructions.trim()}\n`;
7056
6202
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
7057
6203
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
7058
6204
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
7059
- const sponsorsResult = await fetchSponsorsQuietly();
7060
- const specialSponsorsSection = sponsorsResult.isOk() ? formatPostInstallSpecialSponsorsSection(sponsorsResult.value) : "";
7061
- if (specialSponsorsSection) output += `\n${specialSponsorsSection.trim()}\n`;
7062
- output += `\n${pc.bold("Like I dont know?")} Please consider giving us a star\n on GitHub:\n`;
7063
- 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");
7064
6207
  cliConsola.box(output);
7065
6208
  }
7066
6209
  function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
@@ -7069,23 +6212,27 @@ function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
7069
6212
  const exampleUrl = isConvex ? "https://example.convex.cloud" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
7070
6213
  const envFileName = ".env";
7071
6214
  const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
7072
- let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
7073
- 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`;
7074
- 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`;
7075
6218
  return instructions;
7076
6219
  }
7077
6220
  function getHuskyInstructions(runCmd) {
7078
- 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`;
6222
+ }
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`;
7079
6226
  }
7080
6227
  function getLintingInstructions(runCmd) {
7081
- 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`;
7082
6229
  }
7083
6230
  function getLefthookInstructions(packageManager) {
7084
6231
  const cmd = packageManager === "npm" ? "npx" : packageManager;
7085
- 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`;
7086
6233
  }
7087
6234
  function getVitePlusNativeHooksInstructions(runCmd) {
7088
- 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`;
7089
6236
  }
7090
6237
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
7091
6238
  const instructions = [];
@@ -7098,56 +6245,63 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7098
6245
  }
7099
6246
  }
7100
6247
  if (isD1Alchemy) {
7101
- 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`}`);
7102
6249
  else if (orm === "prisma") {
7103
- instructions.push(`${pc.cyan("•")} Generate Prisma client: ${`${runCmd} db:generate`}`);
7104
- 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`}`);
7105
6252
  }
7106
6253
  }
7107
6254
  if (dbSetup === "planetscale") {
7108
- if (database === "mysql" && orm === "drizzle") instructions.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
7109
- 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`);
7110
6257
  }
7111
- 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`);
7112
6259
  if (orm === "prisma") {
7113
- if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
7114
- 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`}`);
7115
6262
  if (!isD1Alchemy) {
7116
- instructions.push(`${pc.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
7117
- 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`}`);
7118
6265
  }
7119
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
6266
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7120
6267
  } else if (orm === "drizzle") {
7121
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7122
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7123
- 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`}`);
7124
6271
  } else if (orm === "mongoose") {
7125
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7126
- } else if (orm === "none") instructions.push(`${pc.yellow("NOTE:")} Manual database schema setup\n required.`);
7127
- 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")}` : "";
7128
6275
  }
7129
6276
  function getTauriInstructions(runCmd, frontend) {
7130
6277
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
7131
- 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}` : ""}`;
7132
6279
  }
7133
6280
  function getElectrobunInstructions(runCmd, frontend) {
7134
6281
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
7135
- 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}` : ""}`;
7136
6283
  }
7137
6284
  function getPwaInstructions() {
7138
- 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`;
7139
- }
7140
- function getStarlightInstructions(runCmd) {
7141
- 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`;
7142
6286
  }
7143
6287
  function getNoOrmWarning() {
7144
- 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.`;
7145
6289
  }
7146
6290
  function getGetMonitorInstructions() {
7147
- return `${pc.bold("GetMonitor observability:")}\n${pc.cyan("•")} Deploy first, then create an HTTP monitor for a stable public health endpoint\n${pc.cyan("•")} Add an alert integration and test delivery\n${pc.cyan("•")} Create a status page and add the monitor as a component\n${pc.dim(" https://getmonitor.io/docs/getting-started/introduction/")}`;
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")}`;
6292
+ }
6293
+ function getResendInstructions() {
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")}`;
6295
+ }
6296
+ function getNotifiqueInstructions() {
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")}`;
7148
6302
  }
7149
6303
  function getBunWebNativeWarning() {
7150
- 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.`;
7151
6305
  }
7152
6306
  function getClerkQuickstartUrl(frontend) {
7153
6307
  if (frontend.includes("next")) return "https://clerk.com/docs/nextjs/getting-started/quickstart";
@@ -7203,28 +6357,28 @@ function getClerkInstructionLines(frontend, backend, api) {
7203
6357
  }
7204
6358
  function getClerkInstructions(frontend, backend, api) {
7205
6359
  return [
7206
- `${pc.bold("Clerk Authentication Setup:")}`,
7207
- `${pc.cyan("•")} Follow the guide: ${pc.underline(getClerkQuickstartUrl(frontend))}`,
7208
- ...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}`)
7209
6363
  ].join("\n");
7210
6364
  }
7211
6365
  function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager, runCmd) {
7212
6366
  const cmd = packageManager === "npm" ? "npx" : packageManager;
7213
- 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` : "");
7214
6368
  }
7215
6369
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
7216
6370
  const instructions = [];
7217
6371
  const isBackendSelf = backend === "self";
7218
6372
  if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) {
7219
6373
  const cfDeploy = serverDeploy === "vercel" ? "deploy:web" : "deploy";
7220
- 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`}`);
7221
6375
  } else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) {
7222
6376
  const cfDeploy = webDeploy === "vercel" ? "deploy:server" : "deploy";
7223
- 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`}`);
7224
- } 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`}`);
7225
6379
  if (webDeploy === "docker" || serverDeploy === "docker") {
7226
6380
  const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
7227
- 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`);
7228
6382
  }
7229
6383
  if (webDeploy === "vercel" || serverDeploy === "vercel") {
7230
6384
  const mixedWithCloudflare = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy;
@@ -7232,14 +6386,14 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
7232
6386
  const vercelEnvScript = "env:production";
7233
6387
  const vercelDeployScript = mixedWithCloudflare ? webDeploy === "vercel" ? "deploy:web:prod" : "deploy:server:prod" : "deploy:prod";
7234
6388
  const vercelTargets = webDeploy === "vercel" && (serverDeploy === "vercel" || isBackendSelf) ? "web + server" : webDeploy === "vercel" ? "web" : "server";
7235
- 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`);
7236
6390
  }
7237
6391
  if (webDeploy === "guaracloud" || serverDeploy === "guaracloud") {
7238
6392
  const deployScript = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy || webDeploy === "guaracloud" && serverDeploy === "guaracloud" ? `deploy:${webDeploy === "guaracloud" ? "web" : "server"}` : "deploy";
7239
6393
  const linkScript = `${deployScript}:link`;
7240
6394
  const logsScript = `${deployScript}:logs`;
7241
6395
  const guaraTargets = webDeploy === "guaracloud" && (serverDeploy === "guaracloud" || isBackendSelf) ? "web + server" : webDeploy === "guaracloud" ? "web" : "server";
7242
- 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/`);
7243
6397
  }
7244
6398
  return instructions.length ? `\n${instructions.join("\n")}` : "";
7245
6399
  }
@@ -7275,6 +6429,7 @@ async function createProject(options, cliInput = {}) {
7275
6429
  message: e.message,
7276
6430
  cause: e
7277
6431
  }))));
6432
+ yield* Result.await(generateRouteTreeIfNeeded(projectDir, options));
7278
6433
  yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager));
7279
6434
  if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
7280
6435
  try: () => setupDatabase(options, cliInput),
@@ -7284,7 +6439,9 @@ async function createProject(options, cliInput = {}) {
7284
6439
  cause: e
7285
6440
  })
7286
6441
  }));
7287
- if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
6442
+ const hasAddons = options.addons.some((addon) => addon !== "none");
6443
+ const hasTesting = options.testing.some((tool) => tool !== "none");
6444
+ if (hasAddons) yield* Result.await(Result.tryPromise({
7288
6445
  try: () => setupAddons(options),
7289
6446
  catch: (e) => new ProjectCreationError({
7290
6447
  phase: "addons-setup",
@@ -7292,12 +6449,23 @@ async function createProject(options, cliInput = {}) {
7292
6449
  cause: e
7293
6450
  })
7294
6451
  }));
7295
- yield* Result.await(formatProject(projectDir));
7296
- if (!isSilent()) log.success("Project template successfully scaffolded!");
7297
- if (options.install) yield* Result.await(installDependencies({
7298
- projectDir,
7299
- packageManager: options.packageManager
6452
+ if (hasTesting) yield* Result.await(Result.tryPromise({
6453
+ try: () => setupTesting(projectDir, options.testing),
6454
+ catch: (e) => new ProjectCreationError({
6455
+ phase: "testing-setup",
6456
+ message: String(e),
6457
+ cause: e
6458
+ })
7300
6459
  }));
6460
+ yield* Result.await(formatProject(projectDir, { addons: options.addons }));
6461
+ if (!isSilent()) log.success("Project template successfully scaffolded!");
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
+ }
7301
6469
  yield* Result.await(initializeGit(projectDir, options.git));
7302
6470
  if (!isSilent()) await displayPostInstallInstructions({
7303
6471
  ...options,
@@ -7306,21 +6474,40 @@ async function createProject(options, cliInput = {}) {
7306
6474
  return Result.ok(projectDir);
7307
6475
  });
7308
6476
  }
7309
- async function setPackageManagerVersion(projectDir, packageManager) {
7310
- const pkgJsonPath = path.join(projectDir, "package.json");
7311
- if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
6477
+ /**
6478
+ * Corepack/Turbo require `name@x.y.z`. Placeholders like `@latest` are invalid.
6479
+ */
6480
+ function isValidPackageManagerField(value) {
6481
+ return /^(bun|npm|pnpm|yarn)@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
6482
+ }
6483
+ function normalizePackageManagerVersion(version) {
6484
+ const cleaned = version.replace(/^v/i, "").trim();
6485
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(cleaned)) return null;
6486
+ return cleaned;
6487
+ }
6488
+ async function resolveInstalledPackageManagerVersion(packageManager) {
6489
+ if (packageManager === "bun" && typeof process.versions.bun === "string" && process.versions.bun.length > 0) return normalizePackageManagerVersion(process.versions.bun);
7312
6490
  const versionResult = await Result.tryPromise({
7313
6491
  try: async () => {
7314
6492
  const { stdout } = await $({ cwd: os.tmpdir() })`${packageManager} -v`;
7315
- return stdout.trim();
6493
+ return normalizePackageManagerVersion(stdout);
7316
6494
  },
7317
6495
  catch: () => null
7318
6496
  });
6497
+ return versionResult.isOk() ? versionResult.value : null;
6498
+ }
6499
+ async function setPackageManagerVersion(projectDir, packageManager) {
6500
+ const pkgJsonPath = path.join(projectDir, "package.json");
6501
+ if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
6502
+ const version = await resolveInstalledPackageManagerVersion(packageManager);
7319
6503
  return Result.tryPromise({
7320
6504
  try: async () => {
7321
6505
  const pkgJson = await fs.readJson(pkgJsonPath);
7322
- if (versionResult.isOk() && versionResult.value) pkgJson.packageManager = `${packageManager}@${versionResult.value}`;
7323
- else delete pkgJson.packageManager;
6506
+ if (version) {
6507
+ const packageManagerField = `${packageManager}@${version}`;
6508
+ if (isValidPackageManagerField(packageManagerField)) pkgJson.packageManager = packageManagerField;
6509
+ else delete pkgJson.packageManager;
6510
+ } else delete pkgJson.packageManager;
7324
6511
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
7325
6512
  },
7326
6513
  catch: (e) => new ProjectCreationError({
@@ -7349,9 +6536,11 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
7349
6536
  frontend: [],
7350
6537
  addons: [],
7351
6538
  examples: [],
6539
+ testing: [],
7352
6540
  auth: "none",
7353
- payments: "none",
7354
- observability: "none",
6541
+ payments: [],
6542
+ observability: [],
6543
+ communication: "none",
7355
6544
  git: false,
7356
6545
  packageManager: "npm",
7357
6546
  install: false,
@@ -7389,7 +6578,7 @@ async function createProjectHandler(input, options = {}) {
7389
6578
  async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7390
6579
  return Result.gen(async function* () {
7391
6580
  if (!isSilent() && input.renderTitle !== false) renderTitle();
7392
- if (!isSilent()) intro(pc.magenta("Creating a new I dont know project"));
6581
+ if (!isSilent()) intro(cliColors.bright("Creating a new kubojs project"));
7393
6582
  if (!isSilent() && input.yolo) cliConsola.fatal("YOLO mode enabled - skipping checks. Things may break!");
7394
6583
  let currentPathInput;
7395
6584
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
@@ -7451,7 +6640,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7451
6640
  const templateName = input.template.toUpperCase();
7452
6641
  const templateDescription = getTemplateDescription(input.template);
7453
6642
  if (!isSilent()) {
7454
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
6643
+ log.message(pc.bold(cliColors.bright(`Using template: ${pc.white(templateName)}`)));
7455
6644
  log.message(pc.dim(` ${templateDescription}`));
7456
6645
  }
7457
6646
  const userOverrides = {};
@@ -7523,6 +6712,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7523
6712
  if (!input.yolo) {
7524
6713
  const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
7525
6714
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
6715
+ const testingValidationResult = validateTestingAgainstFrontends(config.testing, config.frontend);
6716
+ if (testingValidationResult.isErr()) return Result.err(new CLIError({ message: testingValidationResult.error.message }));
7526
6717
  }
7527
6718
  const reproducibleCommand = generateReproducibleCommand(config);
7528
6719
  if (input.dryRun) {
@@ -7532,7 +6723,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7532
6723
  log.success(pc.green("Dry run validation passed. No files were written."));
7533
6724
  log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
7534
6725
  log.message(pc.dim(`Run without --dry-run to create the project.`));
7535
- outro(pc.magenta("Dry run complete."));
6726
+ outro(cliColors.signal("Dry run complete."));
7536
6727
  }
7537
6728
  return Result.ok({
7538
6729
  success: true,
@@ -7548,14 +6739,14 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7548
6739
  manualDb: cliInput.manualDb ?? input.manualDb,
7549
6740
  dbSetupOptions: effectiveDbSetupOptions
7550
6741
  }));
7551
- 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}`));
7552
6743
  await trackProjectCreation(config, input.disableAnalytics);
7553
6744
  const historyResult = await addToHistory(config, reproducibleCommand);
7554
6745
  if (historyResult.isErr() && !isSilent()) log.warn(pc.yellow(historyResult.error.message));
7555
6746
  const elapsedTimeMs = Date.now() - startTime;
7556
6747
  if (!isSilent()) {
7557
6748
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
7558
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
6749
+ outro(cliColors.signal(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
7559
6750
  }
7560
6751
  return Result.ok({
7561
6752
  success: true,
@@ -7655,12 +6846,14 @@ const SchemaNameSchema = z.enum([
7655
6846
  "frontend",
7656
6847
  "addons",
7657
6848
  "examples",
6849
+ "testing",
7658
6850
  "packageManager",
7659
6851
  "databaseSetup",
7660
6852
  "api",
7661
6853
  "auth",
7662
6854
  "payments",
7663
6855
  "observability",
6856
+ "communication",
7664
6857
  "webDeploy",
7665
6858
  "serverDeploy",
7666
6859
  "directoryConflict",
@@ -7670,8 +6863,8 @@ const SchemaNameSchema = z.enum([
7670
6863
  "createInput",
7671
6864
  "addInput",
7672
6865
  "projectConfig",
7673
- "betterTStackConfig",
7674
- "betterTStackConfigFile",
6866
+ "kubojsConfig",
6867
+ "kubojsConfigFile",
7675
6868
  "initResult"
7676
6869
  ]).default("all");
7677
6870
  const t = initTRPC.meta().create();
@@ -7693,7 +6886,7 @@ function getSchemaResult(name) {
7693
6886
  }
7694
6887
  const router = t.router({
7695
6888
  create: t.procedure.meta({
7696
- description: "Create a new I dont know project",
6889
+ description: "Create a new kubojs project",
7697
6890
  default: true,
7698
6891
  negateBooleans: true
7699
6892
  }).input(z.tuple([types_exports.ProjectNameSchema.optional(), z.object({
@@ -7707,9 +6900,12 @@ const router = t.router({
7707
6900
  auth: types_exports.AuthSchema.optional(),
7708
6901
  payments: types_exports.PaymentsSchema.optional(),
7709
6902
  observability: types_exports.ObservabilitySchema.optional(),
6903
+ disableObservability: z.boolean().optional().describe("Disable all observability providers"),
6904
+ communication: types_exports.CommunicationSchema.optional(),
7710
6905
  frontend: z.array(types_exports.FrontendSchema).optional(),
7711
6906
  addons: z.array(types_exports.AddonsSchema).optional(),
7712
6907
  examples: z.array(types_exports.ExamplesSchema).optional(),
6908
+ testing: z.array(types_exports.TestingSchema).optional(),
7713
6909
  git: z.boolean().optional(),
7714
6910
  packageManager: types_exports.PackageManagerSchema.optional(),
7715
6911
  install: z.boolean().optional(),
@@ -7742,11 +6938,11 @@ const router = t.router({
7742
6938
  return result;
7743
6939
  }),
7744
6940
  schema: t.procedure.meta({ description: "Show runtime CLI and input schemas as JSON" }).input(z.object({ name: SchemaNameSchema.describe("Schema name to inspect") })).query(({ input }) => getSchemaResult(input.name)),
7745
- sponsors: t.procedure.meta({ description: "Show I dont know sponsors" }).mutation(() => showSponsorsCommand()),
7746
- docs: t.procedure.meta({ description: "Open I dont know documentation" }).mutation(() => openDocsCommand()),
6941
+ docs: t.procedure.meta({ description: "Open kubojs documentation" }).mutation(() => openDocsCommand()),
7747
6942
  builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(() => openBuilderCommand()),
7748
- add: t.procedure.meta({ description: "Add addons to an existing I dont know project" }).input(z.object({
6943
+ add: t.procedure.meta({ description: "Add addons to an existing kubojs project" }).input(z.object({
7749
6944
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
6945
+ testing: z.array(types_exports.TestingSchema).optional().describe("Testing tools to add"),
7750
6946
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
7751
6947
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
7752
6948
  projectDir: z.string().optional().describe("Project directory (defaults to current)")
@@ -7778,7 +6974,7 @@ function createBtsCli() {
7778
6974
  });
7779
6975
  }
7780
6976
  /**
7781
- * Programmatic API to create a new I dont know project.
6977
+ * Programmatic API to create a new kubojs project.
7782
6978
  * Returns a Result type - no console output, no interactive prompts.
7783
6979
  *
7784
6980
  * @example
@@ -7829,9 +7025,6 @@ async function create(projectName, options) {
7829
7025
  }
7830
7026
  });
7831
7027
  }
7832
- async function sponsors() {
7833
- return showSponsorsCommand();
7834
- }
7835
7028
  async function docs() {
7836
7029
  return openDocsCommand();
7837
7030
  }
@@ -7862,6 +7055,8 @@ async function builder() {
7862
7055
  * ```
7863
7056
  */
7864
7057
  async function createVirtual(options) {
7058
+ const rawPayments = options.payments;
7059
+ const rawObservability = options.observability;
7865
7060
  const config = {
7866
7061
  projectName: options.projectName || "my-project",
7867
7062
  projectDir: "/virtual",
@@ -7875,9 +7070,11 @@ async function createVirtual(options) {
7875
7070
  frontend: options.frontend || ["tanstack-router"],
7876
7071
  addons: options.addons || [],
7877
7072
  examples: options.examples || [],
7073
+ testing: options.testing || [],
7878
7074
  auth: options.auth || "none",
7879
- payments: options.payments || "none",
7880
- observability: options.observability || "none",
7075
+ payments: rawPayments === "none" ? [] : Array.isArray(rawPayments) ? rawPayments : rawPayments ? [rawPayments] : [],
7076
+ observability: options.disableObservability ? [] : rawObservability === "none" ? [] : Array.isArray(rawObservability) ? rawObservability : rawObservability ? [rawObservability] : [],
7077
+ communication: options.communication || "none",
7881
7078
  git: options.git ?? false,
7882
7079
  packageManager: options.packageManager || "bun",
7883
7080
  install: false,
@@ -7894,10 +7091,13 @@ async function createVirtual(options) {
7894
7091
  "frontend",
7895
7092
  "addons",
7896
7093
  "examples",
7094
+ "testing",
7897
7095
  "auth",
7898
7096
  "dbSetup",
7899
7097
  "payments",
7900
7098
  "observability",
7099
+ "disableObservability",
7100
+ "communication",
7901
7101
  "api",
7902
7102
  "webDeploy",
7903
7103
  "serverDeploy"
@@ -7913,7 +7113,7 @@ async function createVirtual(options) {
7913
7113
  });
7914
7114
  }
7915
7115
  /**
7916
- * Programmatic API to add addons to an existing I dont know project.
7116
+ * Programmatic API to add addons to an existing kubojs project.
7917
7117
  *
7918
7118
  * @example
7919
7119
  * ```typescript
@@ -7933,4 +7133,4 @@ async function add(options = {}) {
7933
7133
  return addHandler(options, { silent: true });
7934
7134
  }
7935
7135
  //#endregion
7936
- export { ProjectCreationError as C, DirectoryConflictError as S, ValidationError as T, types_exports as _, TEMPLATE_COUNT as a, CompatibilityError as b, builder as c, createVirtual as d, docs as f, sponsors as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, getLatestCLIVersion as v, UserCancelledError as w, DatabaseSetupError as x, CLIError as y };
7136
+ export { UserCancelledError as C, ProjectCreationError as S, getLatestCLIVersion as _, TEMPLATE_COUNT as a, DatabaseSetupError as b, builder as c, createVirtual as d, docs as f, types_exports as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, CLIError as v, ValidationError as w, DirectoryConflictError as x, CompatibilityError as y };