create-kubojs 0.2.1 → 0.3.0

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,15 +11,16 @@ 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, 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";
14
+ import { ADDON_COMPATIBILITY, COMMUNICATION_PRODUCT_NAMES, CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS, CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS, FRONTEND_VALUES, KUBO_CLI_TITLE, KUBO_TITLE_COLORS, KuboConfigFileSchema, KubojsConfigFileSchema, WEB_FRONTENDS_REQUIRING_ORPC, backendAllowsAiExample, backendAllowsApi, backendAllowsOnlyNoneApi, evaluate, getCommunicationCompatibilityIssue, getPaymentCompatibilityIssue, isCommunicationProvider, isWebFrontend, normalizeCommunication, normalizeCommunication as normalizeCommunication$2, normalizeLegacyKuboConfig, normalizeObservability, normalizePayments } from "@kubojs/types";
15
+ import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, KUBO_CONFIG_FILE, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, applyAddonCatalog, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, getWebAppPath, isBinaryFile, processEnvVariables, processPackageConfigs, processTemplateString, resolveLayout, serializeKuboConfigFile } 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
- import { writeTree } from "@kubojs/template-generator/fs-writer";
20
+ import { writeSelected, writeTree } from "@kubojs/template-generator/fs-writer";
21
21
  import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, TextPrompt, isCancel as isCancel$1, isCancel as isCancel$2 } from "@clack/core";
22
- import { applyEdits, modify, parse } from "jsonc-parser";
22
+ import { createJiti } from "jiti";
23
+ import { parse } from "jsonc-parser";
23
24
  import os from "node:os";
24
25
  import { format } from "oxfmt";
25
26
  import { Generator, configSchema } from "@tanstack/router-generator";
@@ -30,9 +31,30 @@ const KUBO_COLORS = {
30
31
  orange: "#e08a2e",
31
32
  cream: "#f2ede0"
32
33
  };
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
+ const TRUECOLOR_COLORTERM = new Set(["24bit", "truecolor"]);
35
+ function parseForceColor(forceColor) {
36
+ if (forceColor === void 0 || forceColor === "") return void 0;
37
+ if (forceColor === "true") return 1;
38
+ const parsed = Number.parseInt(forceColor, 10);
39
+ return Number.isNaN(parsed) ? 1 : parsed;
40
+ }
41
+ function supportsAnsiColor(env = process.env, terminal = process.stdout) {
42
+ if (env.NO_COLOR !== void 0) return false;
43
+ const forceColor = parseForceColor(env.FORCE_COLOR);
44
+ if (forceColor === 0) return false;
45
+ if (forceColor !== void 0) return true;
46
+ return terminal.isTTY === true;
47
+ }
48
+ function supportsTrueColor(env = process.env, terminal = process.stdout) {
49
+ if (!supportsAnsiColor(env, terminal)) return false;
50
+ const forceColor = parseForceColor(env.FORCE_COLOR);
51
+ if (forceColor !== void 0) return forceColor >= 3;
52
+ const colorTerm = env.COLORTERM?.toLowerCase();
53
+ if (colorTerm !== void 0 && TRUECOLOR_COLORTERM.has(colorTerm)) return true;
54
+ return env.TERM?.toLowerCase().includes("direct") === true;
55
+ }
34
56
  function createColor(hex, fallback) {
35
- if (!supportsTrueColor) return fallback;
57
+ if (!supportsTrueColor()) return fallback;
36
58
  const red = Number.parseInt(hex.slice(1, 3), 16);
37
59
  const green = Number.parseInt(hex.slice(3, 5), 16);
38
60
  const blue = Number.parseInt(hex.slice(5, 7), 16);
@@ -66,7 +88,7 @@ const DEFAULT_CONFIG_BASE = {
66
88
  auth: "better-auth",
67
89
  payments: [],
68
90
  observability: ["getmonitor"],
69
- communication: "none",
91
+ communication: ["arara"],
70
92
  addons: ["turborepo"],
71
93
  examples: [],
72
94
  testing: [],
@@ -93,27 +115,6 @@ function getDefaultConfig() {
93
115
  };
94
116
  }
95
117
  const DEFAULT_CONFIG = getDefaultConfig();
96
- const ADDON_COMPATIBILITY = {
97
- pwa: [
98
- "tanstack-router",
99
- "react-router",
100
- "solid",
101
- "next"
102
- ],
103
- tauri: desktopWebFrontends$3,
104
- electrobun: desktopWebFrontends$3,
105
- biome: [],
106
- husky: [],
107
- lefthook: [],
108
- turborepo: [],
109
- "vite-plus": [],
110
- mcp: [],
111
- oxlint: [],
112
- opentui: [],
113
- skills: [],
114
- "s3-storage": [],
115
- none: []
116
- };
117
118
  //#endregion
118
119
  //#region src/utils/context.ts
119
120
  const cliStorage = new AsyncLocalStorage();
@@ -283,6 +284,14 @@ var AddonSetupError = class extends TaggedError("AddonSetupError")() {
283
284
  }
284
285
  };
285
286
  /**
287
+ * Invalid kubo.config.ts / kubojs.jsonrc content or failed config module load
288
+ */
289
+ var KuboConfigInvalidError = class extends TaggedError("KuboConfigInvalidError")() {
290
+ constructor(args) {
291
+ super(args);
292
+ }
293
+ };
294
+ /**
286
295
  * Create a user cancelled error Result
287
296
  */
288
297
  function userCancelled(message) {
@@ -440,34 +449,16 @@ async function clearHistory() {
440
449
  }
441
450
  //#endregion
442
451
  //#region src/utils/render-title.ts
443
- const TITLE_TEXT = `
444
- ██╗ ██╗██╗ ██╗██████╗ ██████╗
445
- ██║ ██╔╝██║ ██║██╔══██╗██╔═══██╗
446
- █████╔╝ ██║ ██║██████╔╝██║ ██║
447
- ██╔═██╗ ██║ ██║██╔══██╗██║ ██║
448
- ██║ ██╗╚██████╔╝██████╔╝╚██████╔╝
449
- ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
450
- `;
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
- ];
452
+ const TITLE_TEXT = KUBO_CLI_TITLE;
464
453
  const renderTitle = () => {
465
454
  const terminalWidth = process.stdout.columns || 80;
466
455
  const titleLines = TITLE_TEXT.split("\n");
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));
456
+ const title = terminalWidth < Math.max(...titleLines.map((line) => line.length)) ? "Kubo" : TITLE_TEXT;
457
+ if (supportsTrueColor()) {
458
+ console.log(gradient([...KUBO_TITLE_COLORS]).multiline(title));
459
+ return;
460
+ }
461
+ console.log(pc.yellow(title));
471
462
  };
472
463
  //#endregion
473
464
  //#region src/commands/history.ts
@@ -566,24 +557,224 @@ import * as import__kubojs_types from "@kubojs/types";
566
557
  __reExport(types_exports, import__kubojs_types);
567
558
  //#endregion
568
559
  //#region src/utils/compatibility.ts
569
- const WEB_FRAMEWORKS = [
570
- "tanstack-router",
571
- "react-router",
572
- "tanstack-start",
573
- "next",
574
- "nuxt",
575
- "svelte",
576
- "solid",
577
- "astro"
578
- ];
560
+ const WEB_FRAMEWORKS = FRONTEND_VALUES.filter(isWebFrontend);
561
+ //#endregion
562
+ //#region src/utils/compatibility-adapter.ts
563
+ const NESTJS_MESSAGES = {
564
+ "backend-api": "NestJS currently supports no API layer yet. Please use '--api none'.",
565
+ "backend-auth": "NestJS backend currently supports Better Auth or no authentication. Please use '--auth better-auth' or '--auth none'.",
566
+ "backend-database": "NestJS backend currently supports PostgreSQL as its database. Please use '--database postgres'.",
567
+ "backend-orm": "NestJS backend currently supports Prisma as its ORM. Please use '--orm prisma'.",
568
+ "example-ai-backend": "The 'ai' example is not supported with NestJS yet. Please remove 'ai' from --examples.",
569
+ payment: "Payment integrations are not supported with NestJS yet. Please remove payment providers."
570
+ };
571
+ const FIELD_FLAGS = {
572
+ runtime: "--runtime",
573
+ database: "--database",
574
+ orm: "--orm",
575
+ api: "--api",
576
+ auth: "--auth",
577
+ dbSetup: "--db-setup",
578
+ serverDeploy: "--server-deploy",
579
+ communication: "--communication",
580
+ payments: "--payments"
581
+ };
582
+ function validationErr(message, compatibilityCode) {
583
+ return Result.err(new ValidationError({
584
+ message,
585
+ compatibilityCode
586
+ }));
587
+ }
588
+ function normalizeCompatibilityInput(config) {
589
+ return config.payments === "none" ? {
590
+ ...config,
591
+ payments: []
592
+ } : config;
593
+ }
594
+ function firstIncompatibleFrontend(frontends) {
595
+ return [
596
+ "nuxt",
597
+ "svelte",
598
+ "solid",
599
+ "astro"
600
+ ].find((frontend) => frontends.includes(frontend));
601
+ }
602
+ function getBackendOwnedMessage(issue, config) {
603
+ const field = issue.fields[0];
604
+ const flag = FIELD_FLAGS[field] ?? field;
605
+ const backend = config.backend;
606
+ if (backend === "convex") return `Convex backend requires '${flag} none'. Please remove the ${flag} flag or set it to 'none'.`;
607
+ if (backend === "none") return `Backend 'none' requires '${flag} none'. Please remove the ${flag} flag or set it to 'none'.`;
608
+ return `${backend ?? "This backend"} owns ${field}; choose a compatible value.`;
609
+ }
610
+ function getDatabaseMessage(config) {
611
+ if (config.orm === "mongoose") return "Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.";
612
+ if (config.orm === "drizzle" && config.database === "mongodb") return "Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.";
613
+ if (config.database && config.database !== "none" && config.orm === "none") return "Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.";
614
+ return "ORM selection requires a database. Please choose a database or set '--orm none'.";
615
+ }
616
+ function getDatabaseSetupMessage(config) {
617
+ if (!config.database || config.database === "none") return "Database setup requires a database. Please choose a database or set '--db-setup none'.";
618
+ switch (config.dbSetup) {
619
+ case "turso": return "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.";
620
+ case "neon": return "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.";
621
+ case "prisma-postgres": return "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.";
622
+ case "planetscale": return "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup.";
623
+ case "mongodb-atlas": return "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.";
624
+ case "supabase": return "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.";
625
+ case "d1": return "Cloudflare D1 setup requires SQLite database.";
626
+ case "docker":
627
+ if (config.database === "sqlite") return "Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.";
628
+ return "Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.";
629
+ default: return "Database setup is not compatible with the selected database.";
630
+ }
631
+ }
632
+ function getServerDeployMessage(config) {
633
+ const serverDeploy = config.serverDeploy;
634
+ if (!config.backend || config.backend === "none") return "'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.";
635
+ if (serverDeploy === "cloudflare") {
636
+ if (config.runtime !== "workers") return `Server deployment '${serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`;
637
+ return "Cloudflare server deployment requires the Hono backend. Please use '--backend hono' or choose a different server deployment.";
638
+ }
639
+ if (serverDeploy === "none" && config.runtime === "workers") return "Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.";
640
+ if (config.backend === "convex" || config.backend === "self") return `'--server-deploy ${serverDeploy}' requires a separate server backend (hono, express, fastify, elysia, nestjs). For a fullstack 'self' backend, use '--web-deploy ${serverDeploy}' instead.`;
641
+ return `'--server-deploy ${serverDeploy}' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.`;
642
+ }
643
+ function getExampleMessage(config, issue) {
644
+ if (issue.code === "example-ai-backend" && config.backend === "none") return "The 'ai' example requires a backend.";
645
+ if (issue.code === "example-ai-backend" && config.backend === "nestjs") return NESTJS_MESSAGES[issue.code] ?? "The selected backend does not support this example.";
646
+ if (issue.code === "example-ai-frontend") {
647
+ const incompatible = firstIncompatibleFrontend(config.frontend ?? []);
648
+ if (config.backend === "convex" && (incompatible === "nuxt" || incompatible === "svelte")) return "The 'ai' example with Convex backend only supports React-based frontends (Next.js, TanStack Router, TanStack Start, React Router). Svelte and Nuxt are not supported with Convex AI.";
649
+ return `The 'ai' example is not compatible with the ${incompatible ? incompatible.charAt(0).toUpperCase() + incompatible.slice(1) : "selected"} frontend.`;
650
+ }
651
+ if (issue.code === "example-todo-database") return "The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.";
652
+ if (issue.code === "example-todo-api") return "Cannot use '--examples todo' when '--api' is set to 'none'.";
653
+ return "The selected example is not compatible with this project configuration.";
654
+ }
655
+ function getAddonMessage(issue, config) {
656
+ if (issue.code === "addon-task-runner") return "Cannot combine 'turborepo' and 'vite-plus' addons. Choose one task runner.";
657
+ if (issue.code === "addon-linter") return "Cannot combine 'biome' and 'oxlint' addons. Choose one code-quality linter.";
658
+ const addon = issue.values?.[0] ?? "selected";
659
+ if (issue.code === "addon-backend") return `${addon} addon requires a separate backend or no backend because backend 'self' emits server routes that cannot be bundled as static desktop assets.`;
660
+ if (issue.code === "addon-auth") {
661
+ if (config.auth === "clerk" && config.frontend?.includes("react-router")) return `${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.`;
662
+ return `${addon} 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.`;
663
+ }
664
+ return `${addon} addon requires one of these frontends: ${(isAddon(addon) ? ADDON_COMPATIBILITY[addon] : []).join(", ")}`;
665
+ }
666
+ function isAddon(value) {
667
+ return Object.hasOwn(ADDON_COMPATIBILITY, value);
668
+ }
669
+ function getCompatibilityMessage(issue, config, options) {
670
+ if (issue.code.startsWith("backend-none-") || issue.code.startsWith("backend-convex-")) {
671
+ if (issue.code === "backend-convex-frontend") return `The following frontends are not compatible with '--backend convex': ${(config.frontend ?? []).filter((frontend) => frontend === "solid" || frontend === "astro").join(", ")}. Please choose a different frontend or backend.`;
672
+ if (issue.code === "backend-convex-better-auth-frontend") {
673
+ const incompatible = (config.frontend ?? []).filter((frontend) => includesValue(CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS, frontend));
674
+ if (incompatible.length > 0) return `Better Auth with '--backend convex' is not compatible with the following frontends: ${incompatible.join(", ")}. Please use a React-based web frontend (next, tanstack-start, tanstack-router, react-router), a supported native frontend, or choose a different auth provider.`;
675
+ return `Better Auth with '--backend convex' requires a supported frontend (${CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.join(", ")}).`;
676
+ }
677
+ return getBackendOwnedMessage(issue, config);
678
+ }
679
+ if (issue.code === "backend-self-frontend") return "Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.";
680
+ if (issue.code === "backend-self-runtime") return "Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.";
681
+ if (issue.code === "frontend-web-cardinality") return "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid, astro";
682
+ if (issue.code === "frontend-native-cardinality") return "Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles";
683
+ if (issue.code === "backend-workers") {
684
+ if (options?.runtime === "workers") return `Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`;
685
+ return `Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`;
686
+ }
687
+ if (issue.code === "backend-workers-database") {
688
+ if (options?.runtime === "workers") return "Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.";
689
+ return "MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.";
690
+ }
691
+ if (issue.code === "runtime-required") return "'--runtime none' is only supported with '--backend convex', '--backend none', or '--backend self'. Please choose 'bun', 'node', or remove the --runtime flag.";
692
+ if (issue.code === "backend-api" || issue.code === "backend-auth" || issue.code === "backend-database" || issue.code === "backend-orm") {
693
+ if (config.backend === "nestjs") return NESTJS_MESSAGES[issue.code] ?? "The selected backend does not support this value.";
694
+ if (issue.code === "backend-api" && config.api === "orval") return "Orval API requires the Hono backend. Please use '--backend hono' or choose tRPC/oRPC.";
695
+ }
696
+ if (issue.code === "api-frontend") {
697
+ const frontend = firstIncompatibleFrontend(config.frontend ?? []);
698
+ return `tRPC API is not supported with '${frontend}' frontend. Please use --api orpc or --api none or remove '${frontend}' from --frontend.`;
699
+ }
700
+ if (issue.code === "database-orm") return getDatabaseMessage(config);
701
+ if (issue.code === "database-setup" || issue.code === "database-setup-target") {
702
+ if (issue.code === "database-setup-target") return "Cloudflare D1 setup requires SQLite database and either Cloudflare Workers runtime with server deployment or backend 'self' with Cloudflare web deployment.";
703
+ return getDatabaseSetupMessage(config);
704
+ }
705
+ if (issue.code === "web-deploy-frontend") return "'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.";
706
+ if (issue.code === "server-deploy-backend" || issue.code === "server-deploy-runtime" || issue.code === "server-deploy-cloudflare") return getServerDeployMessage(config);
707
+ if (issue.code === "auth-backend") return "Clerk exige backend Convex, Hono, Express, Fastify, Elysia ou Next.js/TanStack Start fullstack";
708
+ if (issue.code === "auth-frontend") return "Clerk authentication is not compatible with the selected frontend. Please choose a different frontend or auth provider.";
709
+ if (issue.code === "addon-task-runner" || issue.code === "addon-linter" || issue.code === "addon-frontend" || issue.code === "addon-backend" || issue.code === "addon-auth") return getAddonMessage(issue, config);
710
+ if (issue.code === "testing-frontend") return "playwright testing requires a web frontend";
711
+ if (issue.code === "example-todo-database" || issue.code === "example-todo-api" || issue.code === "example-todo-orval" || issue.code === "example-ai-frontend" || issue.code === "example-ai-backend") {
712
+ if (issue.code === "example-todo-orval") return "The Orval API layer does not support the generated todo example yet. Please remove 'todo' from --examples or choose tRPC/oRPC.";
713
+ return getExampleMessage(config, issue);
714
+ }
715
+ if (issue.code === "communication") {
716
+ const failingProvider = normalizeCommunication(config.communication ?? []).find((candidate) => {
717
+ if (!isCommunicationProvider(candidate)) return false;
718
+ return getCommunicationCompatibilityIssue({
719
+ provider: candidate,
720
+ backend: config.backend,
721
+ runtime: config.runtime,
722
+ serverDeploy: config.serverDeploy
723
+ });
724
+ });
725
+ if (!failingProvider || !isCommunicationProvider(failingProvider)) return "Communication is incompatible with the selected configuration.";
726
+ return getCommunicationCompatibilityIssue({
727
+ provider: failingProvider,
728
+ backend: config.backend,
729
+ runtime: config.runtime,
730
+ serverDeploy: config.serverDeploy
731
+ }) === "workers-unsupported" ? "AraraHQ requires the official Node SDK and is not compatible with Edge/Workers runtimes. Use a Node/Bun server deployment or Convex Node Action." : `${COMMUNICATION_PRODUCT_NAMES[failingProvider]} communication requires a server backend. Please choose a backend or use '--communication none'.`;
732
+ }
733
+ if (issue.code === "payment") {
734
+ const paymentValues = config.payments;
735
+ const provider = (Array.isArray(paymentValues) ? paymentValues : typeof paymentValues === "string" ? [paymentValues] : []).find((candidate) => getPaymentCompatibilityIssue({
736
+ provider: candidate,
737
+ backend: config.backend,
738
+ frontends: config.frontend,
739
+ database: config.database,
740
+ orm: config.orm
741
+ }));
742
+ if (config.backend === "nestjs") return NESTJS_MESSAGES.payment;
743
+ const paymentIssue = provider ? getPaymentCompatibilityIssue({
744
+ provider,
745
+ backend: config.backend,
746
+ frontends: config.frontend,
747
+ database: config.database,
748
+ orm: config.orm
749
+ }) : null;
750
+ const label = provider === "stripe" ? "Stripe" : "AbacatePay";
751
+ if (paymentIssue === "convex-unsupported") return `${label} payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.`;
752
+ if (paymentIssue === "requires-web-frontend") return `${label} payments requires a web frontend. Please choose next, tanstack-start, tanstack-router, react-router, nuxt, svelte, solid, or astro.`;
753
+ if (paymentIssue === "native-only-unsupported") return `${label} payments is not compatible with native-only frontends. Please add a supported web frontend or leave payments unselected.`;
754
+ if (paymentIssue === "sql-database-required") return "AbacatePay payments v1 requires a SQL database with Drizzle or Prisma. MongoDB and Mongoose are not supported.";
755
+ return `${label} payments is not compatible with the selected project configuration.`;
756
+ }
757
+ return "The selected project configuration is not compatible.";
758
+ }
759
+ function validateWithCompatibilityEvaluator(config, options, preferredCodes = []) {
760
+ const normalizedConfig = normalizeCompatibilityInput(config);
761
+ const issues = evaluate(normalizedConfig).issues;
762
+ const firstIssue = preferredCodes.map((code) => issues.find((candidate) => candidate.code === code)).find((candidate) => candidate !== void 0) ?? issues[0];
763
+ return firstIssue ? validationErr(getCompatibilityMessage(firstIssue, normalizedConfig, options), firstIssue.code) : Result.ok(void 0);
764
+ }
765
+ function getCompatibilityValidationMessage(issue, config, options) {
766
+ return getCompatibilityMessage(issue, config, options);
767
+ }
768
+ function includesValue(values, value) {
769
+ return values.some((candidate) => candidate === value);
770
+ }
579
771
  //#endregion
580
772
  //#region src/utils/compatibility-rules.ts
581
773
  const TASK_RUNNER_ADDONS$1 = ["turborepo", "vite-plus"];
582
774
  /** Mutually exclusive code-quality linters (one slot). */
583
775
  const LINTER_ADDONS = ["biome", "oxlint"];
584
- const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
585
776
  function isLinterAddon(addon) {
586
- return LINTER_ADDONS.includes(addon);
777
+ return LINTER_ADDONS.some((value) => value === addon);
587
778
  }
588
779
  /**
589
780
  * When adding a code-quality linter, drop sibling linters so only one remains.
@@ -604,203 +795,96 @@ function mergeAddonsExclusive(existingAddons, addonsToAdd) {
604
795
  removedAddons: [...removed]
605
796
  };
606
797
  }
607
- const TAURI_STATIC_EXPORT_FRONTENDS = ["next", "tanstack-start"];
608
- const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
609
- "nuxt",
610
- "svelte",
611
- "solid",
612
- "astro"
613
- ];
614
- const CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS = [
615
- "tanstack-router",
616
- "react-router",
617
- "tanstack-start",
618
- "next",
619
- "native-bare",
620
- "native-uniwind",
621
- "native-unistyles"
622
- ];
623
- function validationErr$1(message) {
624
- return Result.err(new ValidationError({ message }));
625
- }
626
- function isWebFrontend(value) {
798
+ function isWebFrontend$1(value) {
627
799
  return WEB_FRAMEWORKS.includes(value);
628
800
  }
629
801
  function isPlaywrightAllowed(frontends = []) {
630
- return frontends.some((frontend) => isWebFrontend(frontend));
802
+ return frontends.some((frontend) => isWebFrontend$1(frontend));
631
803
  }
632
804
  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
- }
636
- function splitFrontends(values = []) {
637
- return {
638
- web: values.filter((f) => isWebFrontend(f)),
639
- native: values.filter((f) => f === "native-bare" || f === "native-uniwind" || f === "native-unistyles")
640
- };
641
- }
642
- function ensureSingleWebAndNative(frontends) {
643
- const { web, native } = splitFrontends(frontends);
644
- if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid, astro");
645
- if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
646
- return Result.ok(void 0);
647
- }
648
- const FULLSTACK_FRONTENDS$1 = [
649
- "next",
650
- "tanstack-start",
651
- "nuxt",
652
- "svelte",
653
- "astro"
654
- ];
655
- function validateSelfBackendCompatibility(providedFlags, options, config) {
656
- const backend = config.backend || options.backend;
657
- const frontends = config.frontend || options.frontend || [];
658
- if (backend === "self") {
659
- const { web, native } = splitFrontends(frontends);
660
- if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.");
661
- if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
662
- }
663
- const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
664
- if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend astro, or choose a different backend.");
665
- return Result.ok(void 0);
666
- }
667
- function validateWorkersCompatibility(providedFlags, options, config) {
668
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") return validationErr$1(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
669
- if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") return validationErr$1(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
670
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") return validationErr$1("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
671
- if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") return validationErr$1("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
672
- return Result.ok(void 0);
805
+ return validateWithCompatibilityEvaluator({
806
+ testing,
807
+ frontend: frontends
808
+ }, void 0, ["testing-frontend"]);
673
809
  }
674
810
  function validateApiFrontendCompatibility(api, frontends = []) {
675
- const includesNuxt = frontends.includes("nuxt");
676
- const includesSvelte = frontends.includes("svelte");
677
- const includesSolid = frontends.includes("solid");
678
- const includesAstro = frontends.includes("astro");
679
- if ((includesNuxt || includesSvelte || includesSolid || includesAstro) && api === "trpc") return validationErr$1(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' from --frontend.`);
680
- return Result.ok(void 0);
811
+ return validateWithCompatibilityEvaluator({
812
+ api,
813
+ frontend: frontends
814
+ }, void 0, ["api-frontend"]);
681
815
  }
682
816
  function isFrontendAllowedWithBackend(frontend, backend, auth) {
683
- if (backend === "convex") {
684
- if (auth === "better-auth" && CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS.includes(frontend)) return false;
685
- if (frontend === "solid" || frontend === "astro") return false;
686
- }
687
- if (auth === "clerk") {
688
- if ([
689
- "nuxt",
690
- "svelte",
691
- "solid",
692
- "astro"
693
- ].includes(frontend)) return false;
694
- }
695
- return true;
817
+ return !evaluate({
818
+ backend,
819
+ auth,
820
+ frontend: [frontend]
821
+ }).issues.map(({ code }) => code).some((code) => [
822
+ "backend-convex-frontend",
823
+ "backend-convex-better-auth-frontend",
824
+ "backend-self-frontend",
825
+ "auth-frontend"
826
+ ].includes(code));
696
827
  }
697
828
  function supportsConvexBetterAuth(frontends = []) {
698
- return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.includes(frontend));
829
+ return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.some((supported) => supported === frontend));
699
830
  }
700
831
  function allowedApisForFrontends(frontends = [], backend) {
701
- const includesNuxt = frontends.includes("nuxt");
702
- const includesSvelte = frontends.includes("svelte");
703
- const includesSolid = frontends.includes("solid");
704
- const includesAstro = frontends.includes("astro");
705
- const base = [
832
+ if (backendAllowsOnlyNoneApi(backend)) return ["none"];
833
+ const supportsTrpc = !frontends.some((frontend) => WEB_FRONTENDS_REQUIRING_ORPC.includes(frontend));
834
+ return [
706
835
  "trpc",
707
- "orpc",
708
- "none"
709
- ];
710
- if (backend === "hono") base.splice(2, 0, "orval");
711
- if (includesNuxt || includesSvelte || includesSolid || includesAstro) return backend === "hono" ? [
712
836
  "orpc",
713
837
  "orval",
714
838
  "none"
715
- ] : ["orpc", "none"];
716
- return base;
839
+ ].filter((api) => (api !== "trpc" || supportsTrpc) && isApiCompatibleWithBackend(api, backend));
717
840
  }
718
841
  function isApiCompatibleWithBackend(api, backend) {
719
- return api !== "orval" || backend === "hono";
842
+ return api !== "orval" || backendAllowsApi(backend, "orval");
720
843
  }
721
844
  function isExampleTodoAllowed(backend, database, api) {
722
- if (backend === "convex") return true;
723
- if (database === "none" || api === "none") return false;
724
- return true;
845
+ return !evaluate({
846
+ backend,
847
+ database,
848
+ api,
849
+ examples: ["todo"]
850
+ }).issues.some(({ code }) => [
851
+ "example-todo-database",
852
+ "example-todo-api",
853
+ "example-todo-orval"
854
+ ].includes(code));
855
+ }
856
+ function isExampleAIAllowedForBackend(backend, example) {
857
+ return example !== "ai" || backendAllowsAiExample(backend);
725
858
  }
726
859
  function isExampleAIAllowed(backend, frontends = []) {
727
- if (backend === "none") return false;
728
- const includesSolid = frontends.includes("solid");
729
- const includesAstro = frontends.includes("astro");
730
- if (includesSolid || includesAstro) return false;
731
- if (backend === "convex") {
732
- const includesNuxt = frontends.includes("nuxt");
733
- const includesSvelte = frontends.includes("svelte");
734
- if (includesNuxt || includesSvelte) return false;
735
- }
736
- return true;
737
- }
738
- function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
739
- if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) return validationErr$1("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
740
- return Result.ok(void 0);
741
- }
742
- function validateServerDeployRequiresBackend(serverDeploy, backend) {
743
- if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
744
- return Result.ok(void 0);
745
- }
746
- function validateDockerServerDeploy(serverDeploy, backend, runtime) {
747
- if (serverDeploy !== "docker") return Result.ok(void 0);
748
- if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy docker' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy docker' instead.");
749
- if (runtime === "workers") return validationErr$1("'--server-deploy docker' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
750
- return Result.ok(void 0);
751
- }
752
- function validateVercelServerDeploy(serverDeploy, backend, runtime) {
753
- if (serverDeploy !== "vercel") return Result.ok(void 0);
754
- if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy vercel' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy vercel' instead.");
755
- if (runtime === "workers") return validationErr$1("'--server-deploy vercel' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
756
- return Result.ok(void 0);
757
- }
758
- function validateGuaraCloudServerDeploy(serverDeploy, backend, runtime) {
759
- if (serverDeploy !== "guaracloud") return Result.ok(void 0);
760
- if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy guaracloud' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy guaracloud' instead.");
761
- if (runtime === "workers") return validationErr$1("'--server-deploy guaracloud' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
762
- return Result.ok(void 0);
763
- }
764
- const DOCKER_SERVER_OUTPUT_FRONTENDS = [
765
- "next",
766
- "svelte",
767
- "astro",
768
- "react-router"
769
- ];
770
- function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backend, auth) {
771
- if (webDeploy !== "docker" || !addons || !frontend) return Result.ok(void 0);
772
- const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
773
- if (desktopAddons.length === 0) return Result.ok(void 0);
774
- const affected = frontend.find((f) => DOCKER_SERVER_OUTPUT_FRONTENDS.includes(f));
775
- if (!affected) return Result.ok(void 0);
776
- if (affected === "next" && !desktopAddons.includes("tauri") && backend === "convex" && auth === "better-auth") return Result.ok(void 0);
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).`);
778
- }
779
- function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
780
- if (STATIC_DESKTOP_ADDONS.includes(addon) && auth === "clerk" && frontend.includes("react-router")) return {
781
- isCompatible: false,
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.`
783
- };
784
- if (backend === "self" && STATIC_DESKTOP_ADDONS.includes(addon)) return {
785
- isCompatible: false,
786
- reason: `${addon} addon requires a separate backend or no backend because backend 'self' emits server routes that cannot be bundled as static desktop assets.`
787
- };
788
- if (addon === "tauri" && backend === "convex" && auth === "better-auth" && frontend.some((f) => TAURI_STATIC_EXPORT_FRONTENDS.includes(f))) return {
789
- isCompatible: false,
790
- reason: "tauri addon is not compatible with Convex Better Auth on Next.js or TanStack Start because those templates use server auth bootstrap and cannot be exported as static desktop assets."
791
- };
792
- if (!(addon in ADDON_COMPATIBILITY)) return {
860
+ return !evaluate({
861
+ backend,
862
+ frontend: frontends,
863
+ examples: ["ai"]
864
+ }).issues.some(({ code }) => ["example-ai-frontend", "example-ai-backend"].includes(code));
865
+ }
866
+ function validateAddonCompatibility(addon, frontend, auth, backend, _runtime) {
867
+ const issue = evaluate({
868
+ addons: [addon],
869
+ frontend,
870
+ auth,
871
+ backend
872
+ }).issues.find(({ code }) => [
873
+ "addon-task-runner",
874
+ "addon-linter",
875
+ "addon-frontend",
876
+ "addon-backend",
877
+ "addon-auth"
878
+ ].includes(code));
879
+ return issue ? {
793
880
  isCompatible: false,
794
- reason: `${addon} is not a supported addon`
795
- };
796
- const compatibleFrontends = ADDON_COMPATIBILITY[addon];
797
- if (compatibleFrontends.length > 0) {
798
- if (!frontend.some((f) => compatibleFrontends.some((compatibleFrontend) => compatibleFrontend === f))) return {
799
- isCompatible: false,
800
- reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
801
- };
802
- }
803
- return { isCompatible: true };
881
+ reason: getCompatibilityValidationMessage(issue, {
882
+ addons: [addon],
883
+ frontend,
884
+ auth,
885
+ backend
886
+ })
887
+ } : { isCompatible: true };
804
888
  }
805
889
  function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, backend, runtime) {
806
890
  return allAddons.filter((addon) => {
@@ -812,64 +896,23 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
812
896
  });
813
897
  }
814
898
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
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.");
817
- for (const addon of addons) {
818
- if (addon === "none") continue;
819
- const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
820
- if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
821
- }
822
- return Result.ok(void 0);
899
+ return validateWithCompatibilityEvaluator({
900
+ addons,
901
+ frontend: frontends,
902
+ auth,
903
+ backend,
904
+ runtime
905
+ }, void 0, [
906
+ "addon-task-runner",
907
+ "addon-linter",
908
+ "addon-frontend",
909
+ "addon-backend",
910
+ "addon-auth"
911
+ ]);
823
912
  }
824
913
  function validateAddonsAgainstConfig(addons = [], config) {
825
914
  return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
826
915
  }
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
- }
837
- function validatePaymentsCompatibility(payments, auth, backend, frontends = [], orm, database) {
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.");
852
- }
853
- return Result.ok(void 0);
854
- }
855
- function validateExamplesCompatibility(examples, backend, database, frontend, api) {
856
- const examplesArr = examples ?? [];
857
- if (examplesArr.length === 0 || examplesArr.includes("none")) return Result.ok(void 0);
858
- if (examplesArr.includes("todo") && backend !== "convex") {
859
- if (database === "none") return validationErr$1("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
860
- if (api === "none") return validationErr$1("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
861
- }
862
- if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) return validationErr$1("The 'ai' example is not compatible with the Solid frontend.");
863
- if (examplesArr.includes("ai") && (frontend ?? []).includes("astro")) return validationErr$1("The 'ai' example is not compatible with the Astro frontend.");
864
- if (examplesArr.includes("ai") && backend === "none") return validationErr$1("The 'ai' example requires a backend.");
865
- if (examplesArr.includes("ai") && backend === "convex") {
866
- const frontendArr = frontend ?? [];
867
- const includesNuxt = frontendArr.includes("nuxt");
868
- const includesSvelte = frontendArr.includes("svelte");
869
- if (includesNuxt || includesSvelte) return validationErr$1("The 'ai' example with Convex backend only supports React-based frontends (Next.js, TanStack Router, TanStack Start, React Router). Svelte and Nuxt are not supported with Convex AI.");
870
- }
871
- return Result.ok(void 0);
872
- }
873
916
  //#endregion
874
917
  //#region src/utils/navigation.ts
875
918
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
@@ -917,6 +960,12 @@ function getMultiHint() {
917
960
  function normalizeValidationMessage(validationMessage) {
918
961
  return validationMessage instanceof Error ? validationMessage.message : validationMessage;
919
962
  }
963
+ function formatKeycap(key) {
964
+ return pc.bold(`<${key}>`);
965
+ }
966
+ function getRequiredMultiselectMessage() {
967
+ return `Please select at least one option.\n${pc.dim(`Press ${formatKeycap("space")} to select, ${formatKeycap("enter")} to submit`)}`;
968
+ }
920
969
  async function runWithNavigation(prompt) {
921
970
  let goBack = false;
922
971
  prompt.on("key", (char) => {
@@ -974,7 +1023,7 @@ async function navigableMultiselect(opts) {
974
1023
  initialValues: opts.initialValues,
975
1024
  required,
976
1025
  validate(selected) {
977
- if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
1026
+ if (required && (selected === void 0 || selected.length === 0)) return getRequiredMultiselectMessage();
978
1027
  return normalizeValidationMessage(opts.validate?.(selected));
979
1028
  },
980
1029
  render() {
@@ -1058,7 +1107,7 @@ async function navigableGroupMultiselect(opts) {
1058
1107
  required,
1059
1108
  selectableGroups: true,
1060
1109
  validate(selected) {
1061
- if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
1110
+ if (required && (selected === void 0 || selected.length === 0)) return getRequiredMultiselectMessage();
1062
1111
  return normalizeValidationMessage(opts.validate?.(selected));
1063
1112
  },
1064
1113
  render() {
@@ -1269,6 +1318,66 @@ async function getAddonsToAdd(config) {
1269
1318
  return response;
1270
1319
  }
1271
1320
  //#endregion
1321
+ //#region src/utils/config-processing.ts
1322
+ function processArrayOption(options) {
1323
+ if (!options || options.length === 0) return [];
1324
+ if (options.includes("none")) return [];
1325
+ return options.filter((item) => item !== "none");
1326
+ }
1327
+ function deriveProjectName(projectName, projectDirectory) {
1328
+ if (projectName) return projectName;
1329
+ if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
1330
+ return "";
1331
+ }
1332
+ function processFlags(options, projectName) {
1333
+ const config = {};
1334
+ if (options.api) config.api = options.api;
1335
+ if (options.addonOptions) config.addonOptions = options.addonOptions;
1336
+ if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
1337
+ if (options.backend) config.backend = options.backend;
1338
+ if (options.database) config.database = options.database;
1339
+ if (options.orm) config.orm = options.orm;
1340
+ if (options.auth !== void 0) config.auth = options.auth;
1341
+ if (options.payments !== void 0) config.payments = normalizePayments(options.payments);
1342
+ if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
1343
+ if (options.disableObservability) config.observability = [];
1344
+ if (options.communication !== void 0) config.communication = normalizeCommunication$2(options.communication);
1345
+ if (options.git !== void 0) config.git = options.git;
1346
+ if (options.install !== void 0) config.install = options.install;
1347
+ if (options.runtime) config.runtime = options.runtime;
1348
+ if (options.dbSetup) config.dbSetup = options.dbSetup;
1349
+ if (options.packageManager) config.packageManager = options.packageManager;
1350
+ if (options.webDeploy) config.webDeploy = options.webDeploy;
1351
+ if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
1352
+ const derivedName = deriveProjectName(projectName, options.projectDirectory);
1353
+ if (derivedName) config.projectName = projectName || derivedName;
1354
+ if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
1355
+ if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
1356
+ if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
1357
+ if (options.testing && options.testing.length > 0) config.testing = processArrayOption(options.testing);
1358
+ return config;
1359
+ }
1360
+ function getProvidedFlags(options) {
1361
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1362
+ }
1363
+ function validateNoneExclusivity(options, optionName) {
1364
+ if (!options || options.length === 0) return Result.ok(void 0);
1365
+ if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
1366
+ return Result.ok(void 0);
1367
+ }
1368
+ function validateArrayOptions(options) {
1369
+ if (options.disableObservability && options.observability !== void 0) return Result.err(new ValidationError({ message: "Cannot combine '--disable-observability' with '--observability'." }));
1370
+ const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
1371
+ if (frontendResult.isErr()) return frontendResult;
1372
+ const addonsResult = validateNoneExclusivity(options.addons, "addons");
1373
+ if (addonsResult.isErr()) return addonsResult;
1374
+ const examplesResult = validateNoneExclusivity(options.examples, "examples");
1375
+ if (examplesResult.isErr()) return examplesResult;
1376
+ const testingResult = validateNoneExclusivity(options.testing, "testing");
1377
+ if (testingResult.isErr()) return testingResult;
1378
+ return Result.ok(void 0);
1379
+ }
1380
+ //#endregion
1272
1381
  //#region src/utils/input-hardening.ts
1273
1382
  function hasControlCharacters(value) {
1274
1383
  for (const char of value) {
@@ -1289,34 +1398,83 @@ function validateAgentSafePathInput(value, field) {
1289
1398
  return Result.ok(void 0);
1290
1399
  }
1291
1400
  //#endregion
1292
- //#region src/utils/kubojs-config.ts
1293
- const KUBOJS_CONFIG_FILE = "kubojs.jsonrc";
1294
- /**
1295
- * Reads the kubojs configuration file from the project directory.
1296
- */
1297
- async function readKubojsConfig(projectDir) {
1298
- try {
1299
- const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1300
- if (!await fs.pathExists(configPath)) return null;
1301
- return parse(await fs.readFile(configPath, "utf-8"));
1302
- } catch {
1303
- return null;
1401
+ //#region src/utils/kubo-config.ts
1402
+ const LEGACY_CONFIG_FILE = "kubojs.jsonrc";
1403
+ const CONFIG_CANDIDATES = [
1404
+ "kubo.config.ts",
1405
+ "kubo.config.mts",
1406
+ "kubo.config.js",
1407
+ "kubo.config.mjs"
1408
+ ];
1409
+ async function findConfigFile(projectDir) {
1410
+ for (const candidate of CONFIG_CANDIDATES) {
1411
+ const absolute = path.join(projectDir, candidate);
1412
+ if (await fs.pathExists(absolute)) return absolute;
1304
1413
  }
1414
+ return null;
1305
1415
  }
1306
- /**
1307
- * Updates specific fields in the kubojs configuration file.
1308
- */
1309
- async function updateKubojsConfig(projectDir, updates) {
1310
- try {
1311
- const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1312
- if (!await fs.pathExists(configPath)) return;
1313
- let content = await fs.readFile(configPath, "utf-8");
1314
- for (const [key, value] of Object.entries(updates)) {
1315
- const edits = modify(content, [key], value, { formattingOptions: { tabSize: 2 } });
1316
- content = applyEdits(content, edits);
1317
- }
1318
- await fs.writeFile(configPath, content, "utf-8");
1319
- } catch {}
1416
+ function configInvalid(file, detail, cause) {
1417
+ return Result.err(new KuboConfigInvalidError({
1418
+ file,
1419
+ message: `Invalid ${file}: ${detail}`,
1420
+ cause
1421
+ }));
1422
+ }
1423
+ async function loadTypeScriptConfig(projectDir) {
1424
+ const configPath = await findConfigFile(projectDir);
1425
+ if (!configPath) return Result.ok(null);
1426
+ const fileName = path.basename(configPath);
1427
+ const jiti = createJiti(import.meta.url, {
1428
+ interopDefault: true,
1429
+ moduleCache: false
1430
+ });
1431
+ const importResult = await Result.tryPromise({
1432
+ try: () => jiti.import(configPath),
1433
+ catch: (e) => new KuboConfigInvalidError({
1434
+ file: fileName,
1435
+ message: `Invalid ${fileName}: ${e instanceof Error ? e.message : String(e)}`,
1436
+ cause: e
1437
+ })
1438
+ });
1439
+ if (importResult.isErr()) return Result.err(importResult.error);
1440
+ const loaded = importResult.value;
1441
+ const exported = loaded.default ?? loaded;
1442
+ const parsed = KuboConfigFileSchema.safeParse(exported);
1443
+ if (!parsed.success) return configInvalid(fileName, parsed.error.issues.map((issue) => issue.message).join("; "));
1444
+ return Result.ok(normalizeLegacyKuboConfig(parsed.data));
1445
+ }
1446
+ async function loadLegacyJsonConfig(projectDir) {
1447
+ const configPath = path.join(projectDir, LEGACY_CONFIG_FILE);
1448
+ if (!await fs.pathExists(configPath)) return Result.ok(null);
1449
+ const raw = parse(await fs.readFile(configPath, "utf-8"));
1450
+ const parsed = KubojsConfigFileSchema.safeParse(raw);
1451
+ if (!parsed.success) return configInvalid(LEGACY_CONFIG_FILE, parsed.error.issues.map((issue) => issue.message).join("; "));
1452
+ return Result.ok(normalizeLegacyKuboConfig(parsed.data));
1453
+ }
1454
+ async function loadProjectKuboConfig(projectDir) {
1455
+ const tsResult = await loadTypeScriptConfig(projectDir);
1456
+ if (tsResult.isErr()) return tsResult;
1457
+ if (tsResult.value) return tsResult;
1458
+ return loadLegacyJsonConfig(projectDir);
1459
+ }
1460
+ async function readKubojsConfig(projectDir) {
1461
+ return loadProjectKuboConfig(projectDir);
1462
+ }
1463
+ async function updateProjectKuboConfig(projectDir, updates) {
1464
+ const existingResult = await loadProjectKuboConfig(projectDir);
1465
+ if (existingResult.isErr()) return Result.err(existingResult.error);
1466
+ const existing = existingResult.value;
1467
+ if (!existing) return Result.ok(void 0);
1468
+ const merged = {
1469
+ ...existing,
1470
+ ...updates,
1471
+ addonOptions: updates.addonOptions ?? existing.addonOptions,
1472
+ dbSetupOptions: updates.dbSetupOptions ?? existing.dbSetupOptions
1473
+ };
1474
+ const configPath = await findConfigFile(projectDir) ?? path.join(projectDir, KUBO_CONFIG_FILE);
1475
+ const content = serializeKuboConfigFile(merged);
1476
+ await fs.writeFile(configPath, content, "utf-8");
1477
+ return Result.ok(void 0);
1320
1478
  }
1321
1479
  //#endregion
1322
1480
  //#region src/utils/add-package-deps.ts
@@ -2040,31 +2198,82 @@ function hasReactBasedFrontend(frontend) {
2040
2198
  function hasNativeFrontend(frontend) {
2041
2199
  return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2042
2200
  }
2201
+ const RECOMMENDED_SOURCE_RULES = {
2202
+ reactBasedFrontend: {
2203
+ when: ({ frontend }) => hasReactBasedFrontend(frontend),
2204
+ sources: ["vercel-labs/agent-skills", "shadcn/ui"]
2205
+ },
2206
+ vercelDeployment: {
2207
+ when: ({ webDeploy, serverDeploy }) => webDeploy === "vercel" || serverDeploy === "vercel",
2208
+ sources: ["vercel-labs/agent-skills"]
2209
+ },
2210
+ nextFrontend: {
2211
+ when: ({ frontend }) => frontend.includes("next"),
2212
+ sources: ["vercel-labs/next-skills"]
2213
+ },
2214
+ nuxtFrontend: {
2215
+ when: ({ frontend }) => frontend.includes("nuxt"),
2216
+ sources: ["nuxt/ui"]
2217
+ },
2218
+ nativeUniwindFrontend: {
2219
+ when: ({ frontend }) => frontend.includes("native-uniwind"),
2220
+ sources: ["heroui-inc/heroui"]
2221
+ },
2222
+ nativeFrontend: {
2223
+ when: ({ frontend }) => hasNativeFrontend(frontend),
2224
+ sources: ["expo/skills"]
2225
+ },
2226
+ betterAuth: {
2227
+ when: ({ auth }) => auth === "better-auth",
2228
+ sources: ["better-auth/skills"]
2229
+ },
2230
+ clerk: {
2231
+ when: ({ auth }) => auth === "clerk",
2232
+ sources: ["clerk/skills"]
2233
+ },
2234
+ neonDatabase: {
2235
+ when: ({ dbSetup }) => dbSetup === "neon",
2236
+ sources: ["neondatabase/agent-skills"]
2237
+ },
2238
+ supabaseDatabase: {
2239
+ when: ({ dbSetup }) => dbSetup === "supabase",
2240
+ sources: ["supabase/agent-skills"]
2241
+ },
2242
+ planetscaleDatabase: {
2243
+ when: ({ dbSetup }) => dbSetup === "planetscale",
2244
+ sources: ["planetscale/database-skills"]
2245
+ },
2246
+ prismaDatabase: {
2247
+ when: ({ orm, dbSetup }) => orm === "prisma" || dbSetup === "prisma-postgres",
2248
+ sources: ["prisma/skills"]
2249
+ },
2250
+ aiExample: {
2251
+ when: ({ examples }) => examples.includes("ai"),
2252
+ sources: ["vercel/ai"]
2253
+ },
2254
+ turborepoAddon: {
2255
+ when: ({ addons }) => addons.includes("turborepo"),
2256
+ sources: ["vercel/turborepo"]
2257
+ },
2258
+ honoBackend: {
2259
+ when: ({ backend }) => backend === "hono",
2260
+ sources: ["yusukebe/hono-skill"]
2261
+ },
2262
+ elysiaBackend: {
2263
+ when: ({ backend }) => backend === "elysia",
2264
+ sources: ["elysiajs/skills"]
2265
+ },
2266
+ convexBackend: {
2267
+ when: ({ backend }) => backend === "convex",
2268
+ sources: ["waynesutton/convexskills"]
2269
+ },
2270
+ opentuiAddon: {
2271
+ when: ({ addons }) => addons.includes("opentui"),
2272
+ sources: ["msmps/opentui-skill"]
2273
+ }
2274
+ };
2043
2275
  function getRecommendedSourceKeys(config) {
2044
- const sources = [];
2045
- const { frontend, backend, dbSetup, auth, examples, addons, orm, webDeploy, serverDeploy } = config;
2046
- if (hasReactBasedFrontend(frontend)) {
2047
- sources.push("vercel-labs/agent-skills");
2048
- sources.push("shadcn/ui");
2049
- }
2050
- if ((webDeploy === "vercel" || serverDeploy === "vercel") && !sources.includes("vercel-labs/agent-skills")) sources.push("vercel-labs/agent-skills");
2051
- if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
2052
- if (frontend.includes("nuxt")) sources.push("nuxt/ui");
2053
- if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
2054
- if (hasNativeFrontend(frontend)) sources.push("expo/skills");
2055
- if (auth === "better-auth") sources.push("better-auth/skills");
2056
- if (auth === "clerk") sources.push("clerk/skills");
2057
- if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
2058
- if (dbSetup === "supabase") sources.push("supabase/agent-skills");
2059
- if (dbSetup === "planetscale") sources.push("planetscale/database-skills");
2060
- if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
2061
- if (examples.includes("ai")) sources.push("vercel/ai");
2062
- if (addons.includes("turborepo")) sources.push("vercel/turborepo");
2063
- if (backend === "hono") sources.push("yusukebe/hono-skill");
2064
- if (backend === "elysia") sources.push("elysiajs/skills");
2065
- if (backend === "convex") sources.push("waynesutton/convexskills");
2066
- if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
2067
- return sources;
2276
+ return uniqueValues(Object.values(RECOMMENDED_SOURCE_RULES).flatMap(({ when, sources }) => when(config) ? sources : []));
2068
2277
  }
2069
2278
  const CURATED_SKILLS_BY_SOURCE = {
2070
2279
  "vercel-labs/agent-skills": (config) => {
@@ -2144,7 +2353,12 @@ function uniqueValues(values) {
2144
2353
  async function setupSkills(config) {
2145
2354
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2146
2355
  const { packageManager, projectDir } = config;
2147
- const kubojsConfig = await readKubojsConfig(projectDir);
2356
+ const kubojsConfigResult = await readKubojsConfig(projectDir);
2357
+ if (kubojsConfigResult.isErr()) return Result.err(new CLIError({
2358
+ message: kubojsConfigResult.error.message,
2359
+ cause: kubojsConfigResult.error
2360
+ }));
2361
+ const kubojsConfig = kubojsConfigResult.value;
2148
2362
  const fullConfig = kubojsConfig ? {
2149
2363
  ...config,
2150
2364
  addons: kubojsConfig.addons ?? config.addons,
@@ -2589,103 +2803,36 @@ async function installPlaywrightBrowsers(projectDir, packageManager) {
2589
2803
  }
2590
2804
  }
2591
2805
  //#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"))];
2634
- }
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);
2654
- }
2655
- //#endregion
2656
2806
  //#region src/helpers/core/detect-project-config.ts
2657
2807
  async function detectProjectConfig(projectDir) {
2658
- const result = await Result.tryPromise({
2659
- try: async () => {
2660
- const kubojsConfig = await readKubojsConfig(projectDir);
2661
- if (kubojsConfig) return {
2662
- projectDir,
2663
- projectName: path.basename(projectDir),
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
2683
- };
2684
- return null;
2685
- },
2686
- catch: () => null
2808
+ const loadResult = await readKubojsConfig(projectDir);
2809
+ if (loadResult.isErr()) return Result.err(loadResult.error);
2810
+ const kubojsConfig = loadResult.value;
2811
+ if (!kubojsConfig) return Result.ok(null);
2812
+ return Result.ok({
2813
+ projectDir,
2814
+ projectName: path.basename(projectDir),
2815
+ layout: kubojsConfig.layout,
2816
+ addonOptions: kubojsConfig.addonOptions,
2817
+ dbSetupOptions: kubojsConfig.dbSetupOptions,
2818
+ database: kubojsConfig.database,
2819
+ orm: kubojsConfig.orm,
2820
+ backend: kubojsConfig.backend,
2821
+ runtime: kubojsConfig.runtime,
2822
+ frontend: kubojsConfig.frontend,
2823
+ addons: kubojsConfig.addons,
2824
+ examples: kubojsConfig.examples,
2825
+ testing: kubojsConfig.testing,
2826
+ auth: kubojsConfig.auth,
2827
+ payments: kubojsConfig.payments,
2828
+ observability: normalizeObservability(kubojsConfig.observability),
2829
+ communication: normalizeCommunication$2(kubojsConfig.communication ?? []),
2830
+ packageManager: kubojsConfig.packageManager,
2831
+ dbSetup: kubojsConfig.dbSetup,
2832
+ api: kubojsConfig.api,
2833
+ webDeploy: kubojsConfig.webDeploy,
2834
+ serverDeploy: kubojsConfig.serverDeploy
2687
2835
  });
2688
- return result.isOk() ? result.value : null;
2689
2836
  }
2690
2837
  //#endregion
2691
2838
  //#region src/helpers/core/install-dependencies.ts
@@ -2711,32 +2858,56 @@ async function installDependencies({ projectDir, packageManager }) {
2711
2858
  return result;
2712
2859
  }
2713
2860
  //#endregion
2861
+ //#region src/helpers/core/project-vfs.ts
2862
+ const IGNORED_DIRECTORY_NAMES = new Set([
2863
+ ".git",
2864
+ ".next",
2865
+ ".nuxt",
2866
+ ".output",
2867
+ ".svelte-kit",
2868
+ ".turbo",
2869
+ "artifacts",
2870
+ "build",
2871
+ "coverage",
2872
+ "dist",
2873
+ "node_modules"
2874
+ ]);
2875
+ /**
2876
+ * Imports the editable text surface of an existing project into the VFS.
2877
+ * Generated/dependency directories and binary files remain owned by the filesystem.
2878
+ */
2879
+ async function loadProjectTextFiles(vfs, projectDir) {
2880
+ await loadDirectory(vfs, projectDir, projectDir);
2881
+ }
2882
+ async function loadDirectory(vfs, projectDir, currentDir) {
2883
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
2884
+ for (const entry of entries) {
2885
+ if (entry.isSymbolicLink()) continue;
2886
+ const absolutePath = path.join(currentDir, entry.name);
2887
+ if (entry.isDirectory()) {
2888
+ if (IGNORED_DIRECTORY_NAMES.has(entry.name)) continue;
2889
+ await loadDirectory(vfs, projectDir, absolutePath);
2890
+ continue;
2891
+ }
2892
+ if (!entry.isFile()) continue;
2893
+ const relativePath = path.relative(projectDir, absolutePath);
2894
+ if (isBinaryFile(relativePath)) continue;
2895
+ const content = await fs.readFile(absolutePath);
2896
+ if (isBinaryContent(content)) continue;
2897
+ vfs.loadFile(relativePath, content.toString("utf-8"));
2898
+ }
2899
+ }
2900
+ function isBinaryContent(content) {
2901
+ if (content.includes(0)) return true;
2902
+ try {
2903
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
2904
+ return false;
2905
+ } catch {
2906
+ return true;
2907
+ }
2908
+ }
2909
+ //#endregion
2714
2910
  //#region src/helpers/core/add-handler.ts
2715
- const ADD_PACKAGE_JSON_PATHS = [
2716
- "package.json",
2717
- "apps/server/package.json",
2718
- "apps/web/package.json",
2719
- "apps/native/package.json",
2720
- "apps/desktop/package.json",
2721
- "apps/docs/package.json",
2722
- "apps/api/package.json",
2723
- "packages/db/package.json",
2724
- "packages/auth/package.json",
2725
- "packages/backend/package.json",
2726
- "packages/config/package.json",
2727
- "packages/env/package.json",
2728
- "packages/infra/package.json",
2729
- "packages/ui/package.json"
2730
- ];
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
- ];
2740
2911
  const HOOK_ADDONS = ["husky", "lefthook"];
2741
2912
  const HOOK_LINTER_ADDONS = [
2742
2913
  "biome",
@@ -2803,8 +2974,9 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
2803
2974
  }
2804
2975
  }
2805
2976
  }
2806
- function updateViteConfigImportsForVitePlus(vfs) {
2807
- for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
2977
+ function updateViteConfigImportsForVitePlus(vfs, webPath) {
2978
+ const viteConfigPaths = [`${webPath}/vite.config.ts`];
2979
+ for (const viteConfigPath of viteConfigPaths) {
2808
2980
  const content = vfs.readFile(viteConfigPath);
2809
2981
  if (!content) continue;
2810
2982
  const updatedContent = content.replaceAll("from \"vite\";", "from \"vite-plus\";").replaceAll("from 'vite';", "from 'vite-plus';");
@@ -2847,8 +3019,13 @@ async function addHandlerInternal(input) {
2847
3019
  renderTitle();
2848
3020
  intro(cliColors.signal("Add addons to your kubojs project"));
2849
3021
  }
2850
- const existingConfig = await detectProjectConfig(projectDir);
2851
- if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubojs.jsonrc exists.` }));
3022
+ const detectResult = await detectProjectConfig(projectDir);
3023
+ if (detectResult.isErr()) return Result.err(new CLIError({
3024
+ message: detectResult.error.message,
3025
+ cause: detectResult.error
3026
+ }));
3027
+ const existingConfig = detectResult.value;
3028
+ if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubo.config.ts exists.` }));
2852
3029
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
2853
3030
  let addonsToAdd;
2854
3031
  if (input.addons && input.addons.length > 0) {
@@ -2897,10 +3074,20 @@ async function addHandlerInternal(input) {
2897
3074
  if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
2898
3075
  }
2899
3076
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
3077
+ const layout = existingConfig.layout ?? { preset: "standard" };
3078
+ const layoutResult = resolveLayout(layout, projectDir, {
3079
+ backend: existingConfig.backend,
3080
+ addonsToAdd
3081
+ });
3082
+ if (layoutResult.isErr()) return Result.err(new CLIError({
3083
+ message: layoutResult.error.message,
3084
+ cause: layoutResult.error
3085
+ }));
2900
3086
  const config = {
2901
3087
  projectName: existingConfig.projectName,
2902
3088
  projectDir,
2903
3089
  relativePath: ".",
3090
+ layout,
2904
3091
  addonOptions: mergedAddonOptions,
2905
3092
  database: existingConfig.database,
2906
3093
  orm: existingConfig.orm,
@@ -2912,7 +3099,7 @@ async function addHandlerInternal(input) {
2912
3099
  auth: existingConfig.auth,
2913
3100
  payments: existingConfig.payments,
2914
3101
  observability: existingConfig.observability,
2915
- communication: existingConfig.communication ?? "none",
3102
+ communication: normalizeCommunication$2(existingConfig.communication ?? []),
2916
3103
  testing: updatedTesting,
2917
3104
  git: false,
2918
3105
  packageManager: input.packageManager || existingConfig.packageManager,
@@ -2928,45 +3115,27 @@ async function addHandlerInternal(input) {
2928
3115
  };
2929
3116
  if (!isSilent()) log.info(pc.dim("Installing addon files..."));
2930
3117
  const vfs = new VirtualFileSystem();
2931
- for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
2932
- const fullPath = path.join(projectDir, pkgPath);
2933
- if (await fs.pathExists(fullPath)) {
2934
- const content = await fs.readFile(fullPath, "utf-8");
2935
- vfs.writeFile(pkgPath, content);
2936
- }
2937
- }
2938
- for (const filePath of ADD_TEXT_FILE_PATHS) {
2939
- const fullPath = path.join(projectDir, filePath);
2940
- if (await fs.pathExists(fullPath)) {
2941
- const content = await fs.readFile(fullPath, "utf-8");
2942
- vfs.writeFile(filePath, content);
2943
- }
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
- }
2952
- await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
2953
- processAddonsDeps(vfs, config);
3118
+ await loadProjectTextFiles(vfs, projectDir);
3119
+ await applyAddonCatalog(vfs, EMBEDDED_TEMPLATES, updatedConfig, {
3120
+ templateConfig: config,
3121
+ refreshTaskRunner: addonsToAdd.includes("turborepo"),
3122
+ refreshVitePlus: addonsToAdd.includes("vite-plus")
3123
+ });
2954
3124
  processEnvVariables(vfs, updatedConfig);
2955
- if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
2956
- if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
2957
3125
  if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
2958
- if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
3126
+ if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs, getWebAppPath(updatedConfig));
2959
3127
  if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
3128
+ const changedFiles = new Set(vfs.getChangedFiles());
2960
3129
  const tree = {
2961
3130
  root: vfs.toTree(""),
2962
- fileCount: vfs.getFileCount(),
3131
+ fileCount: changedFiles.size,
2963
3132
  directoryCount: vfs.getDirectoryCount(),
2964
3133
  config: updatedConfig
2965
3134
  };
2966
3135
  if (input.dryRun) {
2967
3136
  if (!isSilent()) {
2968
3137
  log.success(pc.green("Dry run validation passed. No addon files were written."));
2969
- log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
3138
+ log.info(pc.dim(`Planned addon files: ${changedFiles.size}`));
2970
3139
  outro(cliColors.signal("Dry run complete."));
2971
3140
  }
2972
3141
  return Result.ok({
@@ -2977,9 +3146,9 @@ async function addHandlerInternal(input) {
2977
3146
  plannedFileCount: vfs.getFileCount()
2978
3147
  });
2979
3148
  }
2980
- const writeResult = await writeTree(tree, projectDir);
3149
+ const writeResult = await writeSelected(tree, projectDir, (filePath) => changedFiles.has(filePath));
2981
3150
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
2982
- if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
3151
+ if (changedFiles.size > 0 && !isSilent()) log.info(pc.dim(`Wrote ${changedFiles.size} addon files`));
2983
3152
  await cleanupRemovedLinters(projectDir, removedAddons);
2984
3153
  const setupResult = await Result.tryPromise({
2985
3154
  try: () => setupAddons({
@@ -3003,11 +3172,16 @@ async function addHandlerInternal(input) {
3003
3172
  })
3004
3173
  });
3005
3174
  if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
3006
- await updateKubojsConfig(projectDir, {
3175
+ const configUpdateResult = await updateProjectKuboConfig(projectDir, {
3007
3176
  addons: updatedAddons,
3008
3177
  addonOptions: updatedConfig.addonOptions,
3009
- testing: updatedTesting
3178
+ testing: updatedTesting,
3179
+ layout
3010
3180
  });
3181
+ if (configUpdateResult.isErr()) return Result.err(new CLIError({
3182
+ message: configUpdateResult.error.message,
3183
+ cause: configUpdateResult.error
3184
+ }));
3011
3185
  if (input.install) {
3012
3186
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
3013
3187
  await installDependencies({
@@ -3149,6 +3323,10 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
3149
3323
  value: "elysia",
3150
3324
  label: "Elysia",
3151
3325
  hint: "Ergonomic web framework for building backend servers"
3326
+ }, {
3327
+ value: "nestjs",
3328
+ label: "NestJS",
3329
+ hint: "Opinionated TypeScript framework for scalable server applications"
3152
3330
  });
3153
3331
  if (!hasIncompatibleFrontend) backendOptions.push({
3154
3332
  value: "convex",
@@ -3170,35 +3348,31 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
3170
3348
  }
3171
3349
  //#endregion
3172
3350
  //#region src/prompts/communication.ts
3351
+ const options$1 = [
3352
+ {
3353
+ value: "arara",
3354
+ label: "AraraHQ",
3355
+ hint: "Recommended — WhatsApp messaging via the official Node SDK"
3356
+ },
3357
+ {
3358
+ value: "resend",
3359
+ label: "Resend",
3360
+ hint: "Transactional email for developers"
3361
+ },
3362
+ {
3363
+ value: "notifique",
3364
+ label: "Notifique",
3365
+ hint: "Omnichannel BR messaging API"
3366
+ }
3367
+ ];
3173
3368
  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)
3369
+ if (communication !== void 0) return normalizeCommunication$2(communication);
3370
+ if (backend === "none") return [];
3371
+ const response = await navigableMultiselect({
3372
+ message: "Select communication providers",
3373
+ options: options$1,
3374
+ required: false,
3375
+ initialValues: previousValue ?? DEFAULT_CONFIG.communication
3202
3376
  });
3203
3377
  if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3204
3378
  return response;
@@ -3354,7 +3528,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
3354
3528
  label: "Todo App",
3355
3529
  hint: "A simple CRUD example app"
3356
3530
  });
3357
- if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
3531
+ if (isExampleAIAllowed(backend, frontends ?? []) && isExampleAIAllowedForBackend(backend, "ai")) options.push({
3358
3532
  value: "ai",
3359
3533
  label: "AI Chat",
3360
3534
  hint: "A simple AI chat interface using AI SDK"
@@ -3545,219 +3719,21 @@ async function getObservabilityChoice(observability, previousValue) {
3545
3719
  }
3546
3720
  //#endregion
3547
3721
  //#region src/utils/config-validation.ts
3548
- function validationErr(message) {
3549
- return Result.err(new ValidationError({ message }));
3550
- }
3551
- function hasResolvedWorkersD1Target(config) {
3552
- return config.backend === "hono" && config.runtime === "workers" && config.serverDeploy === "cloudflare";
3553
- }
3554
- function hasResolvedSelfCloudflareD1Target(config) {
3555
- return config.backend === "self" && config.runtime === "none" && config.webDeploy === "cloudflare";
3556
- }
3557
- function canResolveWorkersD1Target(config) {
3558
- return (config.backend === void 0 || config.backend === "hono") && (config.runtime === void 0 || config.runtime === "workers") && (config.serverDeploy === void 0 || config.serverDeploy === "cloudflare");
3559
- }
3560
- function canResolveSelfCloudflareD1Target(config) {
3561
- return (config.backend === void 0 || config.backend === "self") && (config.runtime === void 0 || config.runtime === "none") && (config.webDeploy === void 0 || config.webDeploy === "cloudflare");
3562
- }
3563
3722
  /**
3564
- * Pure ORM + database compatibility check. Used by the flag-path
3565
- * validator below and by the orm prompt directly (for flag+prompt
3566
- * combos where one value came from a flag and the other from a
3567
- * prompt).
3723
+ * Keeps the prompt-level API stable while compatibility facts live in the
3724
+ * shared evaluator.
3568
3725
  */
3569
3726
  function validateOrmDatabaseCompat(orm, database) {
3570
- if (orm === "mongoose" && database && database !== "mongodb") return validationErr("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
3571
- if (orm === "drizzle" && database === "mongodb") return validationErr("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
3572
- if (database === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") return validationErr("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
3573
- if (database && database !== "none" && orm === "none") return validationErr("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
3574
- if (orm && orm !== "none" && database === "none") return validationErr("ORM selection requires a database. Please choose a database or set '--orm none'.");
3575
- return Result.ok(void 0);
3576
- }
3577
- function validateDatabaseOrmAuth(cfg, flags) {
3578
- const has = (k) => flags ? flags.has(k) : true;
3579
- if (!has("orm") || !has("database")) return Result.ok(void 0);
3580
- return validateOrmDatabaseCompat(cfg.orm, cfg.database);
3581
- }
3582
- function validateDatabaseSetup(config, providedFlags) {
3583
- const { dbSetup, database, runtime } = config;
3584
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") return validationErr("Database setup requires a database. Please choose a database or set '--db-setup none'.");
3585
- const setupValidations = {
3586
- turso: {
3587
- database: "sqlite",
3588
- errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
3589
- },
3590
- neon: {
3591
- database: "postgres",
3592
- errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3593
- },
3594
- "prisma-postgres": {
3595
- database: "postgres",
3596
- errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3597
- },
3598
- planetscale: { errorMessage: "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup." },
3599
- "mongodb-atlas": {
3600
- database: "mongodb",
3601
- errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
3602
- },
3603
- supabase: {
3604
- database: "postgres",
3605
- errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3606
- },
3607
- d1: {
3608
- database: "sqlite",
3609
- errorMessage: "Cloudflare D1 setup requires SQLite database."
3610
- },
3611
- docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
3612
- none: { errorMessage: "" }
3613
- };
3614
- if (dbSetup && dbSetup !== "none") {
3615
- const validation = setupValidations[dbSetup];
3616
- if (dbSetup === "planetscale") {
3617
- if (database !== "postgres" && database !== "mysql") return validationErr(validation.errorMessage);
3618
- } else if (validation.database && database !== validation.database) return validationErr(validation.errorMessage);
3619
- if (validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
3620
- if (dbSetup === "d1") {
3621
- const isWorkersTarget = hasResolvedWorkersD1Target(config);
3622
- const isSelfCloudflareTarget = hasResolvedSelfCloudflareD1Target(config);
3623
- const canResolveWorkersTarget = canResolveWorkersD1Target(config);
3624
- const canResolveSelfCloudflareTarget = canResolveSelfCloudflareD1Target(config);
3625
- if (!isWorkersTarget && !isSelfCloudflareTarget && !canResolveWorkersTarget && !canResolveSelfCloudflareTarget) return validationErr("Cloudflare D1 setup requires SQLite database and either Cloudflare Workers runtime with server deployment or backend 'self' with Cloudflare web deployment.");
3626
- }
3627
- if (dbSetup === "docker") {
3628
- if (database === "sqlite") return validationErr("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
3629
- if (runtime === "workers") return validationErr("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
3630
- }
3631
- }
3632
- return Result.ok(void 0);
3633
- }
3634
- function validateConvexConstraints(config, providedFlags) {
3635
- const { backend } = config;
3636
- if (backend !== "convex") return Result.ok(void 0);
3637
- const has = (k) => providedFlags.has(k);
3638
- if (has("runtime") && config.runtime !== "none") return validationErr("Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3639
- if (has("database") && config.database !== "none") return validationErr("Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.");
3640
- if (has("orm") && config.orm !== "none") return validationErr("Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
3641
- if (has("api") && config.api !== "none") return validationErr("Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.");
3642
- if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
3643
- if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
3644
- if (has("auth") && config.auth === "better-auth") {
3645
- const incompatibleFrontends = config.frontend?.filter((f) => CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS.includes(f)) ?? [];
3646
- const hasSupportedFrontend = supportsConvexBetterAuth(config.frontend);
3647
- if (incompatibleFrontends.length > 0) return validationErr(`Better Auth with '--backend convex' is not compatible with the following frontends: ${incompatibleFrontends.join(", ")}. Please use a React-based web frontend (next, tanstack-start, tanstack-router, react-router), a supported native frontend, or choose a different auth provider.`);
3648
- if (!hasSupportedFrontend) return validationErr(`Better Auth with '--backend convex' requires a supported frontend (${CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.join(", ")}).`);
3649
- }
3650
- return Result.ok(void 0);
3651
- }
3652
- function validateBackendNoneConstraints(config, providedFlags) {
3653
- const { backend } = config;
3654
- if (backend !== "none") return Result.ok(void 0);
3655
- const has = (k) => providedFlags.has(k);
3656
- if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3657
- if (has("database") && config.database !== "none") return validationErr("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
3658
- if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
3659
- if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
3660
- if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth 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'.");
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'.");
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'.");
3665
- return Result.ok(void 0);
3666
- }
3667
- function validateSelfBackendConstraints(config, providedFlags) {
3668
- const { backend } = config;
3669
- if (backend !== "self") return Result.ok(void 0);
3670
- const has = (k) => providedFlags.has(k);
3671
- if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3672
- return Result.ok(void 0);
3673
- }
3674
- function validateBackendConstraints(config, providedFlags, options) {
3675
- const { backend } = config;
3676
- if (config.auth === "clerk" && config.frontend) {
3677
- const incompatibleFrontends = config.frontend.filter((f) => [
3678
- "nuxt",
3679
- "svelte",
3680
- "solid",
3681
- "astro"
3682
- ].includes(f));
3683
- if (incompatibleFrontends.length > 0) return validationErr(`Clerk authentication is not compatible with the following frontends: ${incompatibleFrontends.join(", ")}. Please choose a different frontend or auth provider.`);
3684
- }
3685
- if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self") {
3686
- if (providedFlags.has("runtime") && options.runtime === "none") return validationErr("'--runtime none' is only supported with '--backend convex', '--backend none', or '--backend self'. Please choose 'bun', 'node', or remove the --runtime flag.");
3687
- }
3688
- if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
3689
- const incompatibleFrontends = options.frontend.filter((f) => f === "solid" || f === "astro");
3690
- if (incompatibleFrontends.length > 0) return validationErr(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
3691
- }
3692
- return Result.ok(void 0);
3693
- }
3694
- function validateFrontendConstraints(config, providedFlags) {
3695
- const { frontend } = config;
3696
- if (frontend && frontend.length > 0) {
3697
- const singleWebNativeResult = ensureSingleWebAndNative(frontend);
3698
- if (singleWebNativeResult.isErr()) return singleWebNativeResult;
3699
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) {
3700
- const apiResult = validateApiFrontendCompatibility(config.api, frontend);
3701
- if (apiResult.isErr()) return apiResult;
3702
- }
3703
- }
3704
- const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
3705
- const webDeployResult = validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
3706
- if (webDeployResult.isErr()) return webDeployResult;
3707
- return Result.ok(void 0);
3708
- }
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.");
3712
- if (config.api === "none") {
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.");
3714
- }
3715
- return Result.ok(void 0);
3716
- }
3717
- function validateFullConfig(config, providedFlags, options) {
3718
- return Result.gen(function* () {
3719
- yield* validateDatabaseOrmAuth(config, providedFlags);
3720
- yield* validateDatabaseSetup(config, providedFlags);
3721
- yield* validateConvexConstraints(config, providedFlags);
3722
- yield* validateBackendNoneConstraints(config, providedFlags);
3723
- yield* validateSelfBackendConstraints(config, providedFlags);
3724
- yield* validateBackendConstraints(config, providedFlags, options);
3725
- yield* validateFrontendConstraints(config, providedFlags);
3726
- yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
3727
- yield* validateApiConstraints(config, options);
3728
- yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
3729
- yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
3730
- yield* validateVercelServerDeploy(config.serverDeploy, config.backend, config.runtime);
3731
- yield* validateGuaraCloudServerDeploy(config.serverDeploy, config.backend, config.runtime);
3732
- yield* validateDockerWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend, config.backend, config.auth);
3733
- yield* validateSelfBackendCompatibility(providedFlags, options, config);
3734
- yield* validateWorkersCompatibility(providedFlags, options, config);
3735
- if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
3736
- if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
3737
- if (config.addons && config.addons.length > 0) {
3738
- yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
3739
- config.addons = [...new Set(config.addons)];
3740
- }
3741
- yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
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);
3745
- return Result.ok(void 0);
3727
+ return validateWithCompatibilityEvaluator({
3728
+ orm,
3729
+ database
3746
3730
  });
3747
3731
  }
3732
+ function validateFullConfig(config, _providedFlags, options) {
3733
+ return validateWithCompatibilityEvaluator(config, options);
3734
+ }
3748
3735
  function validateConfigForProgrammaticUse(config) {
3749
- return Result.gen(function* () {
3750
- yield* validateDatabaseOrmAuth(config);
3751
- if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
3752
- yield* validateApiFrontendCompatibility(config.api, config.frontend);
3753
- yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
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);
3757
- if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
3758
- yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
3759
- return Result.ok(void 0);
3760
- });
3736
+ return validateWithCompatibilityEvaluator(config);
3761
3737
  }
3762
3738
  //#endregion
3763
3739
  //#region src/prompts/orm.ts
@@ -3898,26 +3874,33 @@ const SERVER_APP_BACKENDS = [
3898
3874
  "hono",
3899
3875
  "express",
3900
3876
  "fastify",
3901
- "elysia"
3877
+ "elysia",
3878
+ "nestjs"
3902
3879
  ];
3903
- function getDeploymentDisplay$1(deployment) {
3904
- if (deployment === "cloudflare") return {
3880
+ const DEPLOYMENT_DISPLAYS$1 = {
3881
+ cloudflare: {
3905
3882
  label: "Cloudflare",
3906
3883
  hint: "Deploy to Cloudflare Workers using Alchemy"
3907
- };
3908
- if (deployment === "docker") return {
3884
+ },
3885
+ docker: {
3909
3886
  label: "Docker",
3910
3887
  hint: "Self-host with a Dockerfile and docker-compose.yml"
3911
- };
3912
- if (deployment === "vercel") return {
3888
+ },
3889
+ vercel: {
3913
3890
  label: "Vercel",
3914
3891
  hint: "Deploy to Vercel with Services"
3915
- };
3916
- if (deployment === "guaracloud") return {
3892
+ },
3893
+ railway: {
3894
+ label: "Railway",
3895
+ hint: "Deploy with Railpack and railway.json"
3896
+ },
3897
+ guaracloud: {
3917
3898
  label: "Guara Cloud",
3918
3899
  hint: "Deploy containers on Guara Cloud via GitHub or Docker image"
3919
- };
3920
- return {
3900
+ }
3901
+ };
3902
+ function getDeploymentDisplay$1(deployment) {
3903
+ return DEPLOYMENT_DISPLAYS$1[deployment] ?? {
3921
3904
  label: deployment,
3922
3905
  hint: `Add ${deployment} deployment`
3923
3906
  };
@@ -3930,6 +3913,8 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
3930
3913
  const options = [
3931
3914
  "docker",
3932
3915
  "vercel",
3916
+ "railway",
3917
+ "guaracloud",
3933
3918
  "none"
3934
3919
  ].map((deploy) => {
3935
3920
  const { label, hint } = deploy === "none" ? {
@@ -3978,24 +3963,30 @@ async function getTestingChoice(testing, frontends, previousValue) {
3978
3963
  function hasWebFrontend(frontends) {
3979
3964
  return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
3980
3965
  }
3981
- function getDeploymentDisplay(deployment) {
3982
- if (deployment === "cloudflare") return {
3966
+ const DEPLOYMENT_DISPLAYS = {
3967
+ cloudflare: {
3983
3968
  label: "Cloudflare",
3984
3969
  hint: "Deploy to Cloudflare Workers using Alchemy"
3985
- };
3986
- if (deployment === "docker") return {
3970
+ },
3971
+ docker: {
3987
3972
  label: "Docker",
3988
3973
  hint: "Self-host with a Dockerfile and docker-compose.yml"
3989
- };
3990
- if (deployment === "vercel") return {
3974
+ },
3975
+ vercel: {
3991
3976
  label: "Vercel",
3992
3977
  hint: "Deploy to Vercel with Services"
3993
- };
3994
- if (deployment === "guaracloud") return {
3978
+ },
3979
+ railway: {
3980
+ label: "Railway",
3981
+ hint: "Deploy with Railpack and railway.json"
3982
+ },
3983
+ guaracloud: {
3995
3984
  label: "Guara Cloud",
3996
3985
  hint: "Deploy containers on Guara Cloud via GitHub or Docker image"
3997
- };
3998
- return {
3986
+ }
3987
+ };
3988
+ function getDeploymentDisplay(deployment) {
3989
+ return DEPLOYMENT_DISPLAYS[deployment] ?? {
3999
3990
  label: deployment,
4000
3991
  hint: `Add ${deployment} deployment`
4001
3992
  };
@@ -4008,6 +3999,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
4008
3999
  "cloudflare",
4009
4000
  "docker",
4010
4001
  "vercel",
4002
+ "railway",
4011
4003
  "guaracloud",
4012
4004
  "none"
4013
4005
  ].map((deploy) => {
@@ -4043,7 +4035,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
4043
4035
  auth: flags.auth ?? DEFAULT_CONFIG.auth,
4044
4036
  payments: flags.payments ?? DEFAULT_CONFIG.payments,
4045
4037
  observability: flags.observability ?? DEFAULT_CONFIG.observability,
4046
- communication: flags.communication ?? DEFAULT_CONFIG.communication,
4038
+ communication: normalizeCommunication$2(flags.communication ?? DEFAULT_CONFIG.communication),
4047
4039
  addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
4048
4040
  examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
4049
4041
  testing: flags.testing ?? [...DEFAULT_CONFIG.testing],
@@ -4118,7 +4110,7 @@ function kuboText(options) {
4118
4110
  initialValue: options.initialValue,
4119
4111
  render() {
4120
4112
  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("_"));
4113
+ const placeholder = options.placeholder ? pc.dim(options.placeholder) : pc.dim("_");
4122
4114
  const input = this.userInput ? this.userInputWithCursor : placeholder;
4123
4115
  const value = this.value ?? "";
4124
4116
  switch (this.state) {
@@ -4200,10 +4192,25 @@ function isTelemetryEnabled() {
4200
4192
  async function sendConvexEvent(payload) {}
4201
4193
  async function trackProjectCreation(config, disableAnalytics = false) {
4202
4194
  if (!isTelemetryEnabled() || disableAnalytics) return;
4203
- const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
4195
+ const { database, orm, backend, runtime, frontend, addons, examples, auth, payments, git, packageManager, install, dbSetup, api, webDeploy, serverDeploy } = config;
4204
4196
  await Result.tryPromise({
4205
4197
  try: () => sendConvexEvent({
4206
- ...safeConfig,
4198
+ database,
4199
+ orm,
4200
+ backend,
4201
+ runtime,
4202
+ frontend,
4203
+ addons,
4204
+ examples,
4205
+ auth,
4206
+ payments,
4207
+ git,
4208
+ packageManager,
4209
+ install,
4210
+ dbSetup,
4211
+ api,
4212
+ webDeploy,
4213
+ serverDeploy,
4207
4214
  cli_version: getLatestCLIVersion(),
4208
4215
  node_version: typeof process !== "undefined" ? process.version : "",
4209
4216
  platform: typeof process !== "undefined" ? process.platform : ""
@@ -4228,7 +4235,10 @@ function displayConfig(config) {
4228
4235
  if (config.orm !== void 0) configDisplay.push(`${cliColors.signal("ORM:")} ${String(config.orm)}`);
4229
4236
  if (config.auth !== void 0) configDisplay.push(`${cliColors.signal("Auth:")} ${String(config.auth)}`);
4230
4237
  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)}`);
4238
+ if (config.communication !== void 0) {
4239
+ const communication = normalizeCommunication$2(config.communication);
4240
+ configDisplay.push(`${cliColors.signal("Communication:")} ${communication.join(", ") || "none"}`);
4241
+ }
4232
4242
  if (config.observability !== void 0) configDisplay.push(`${cliColors.signal("Observability:")} ${config.observability.join(", ") || "none"}`);
4233
4243
  if (config.addons !== void 0) {
4234
4244
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
@@ -4388,7 +4398,7 @@ const TEMPLATE_PRESETS = {
4388
4398
  auth: "better-auth",
4389
4399
  payments: [],
4390
4400
  observability: ["getmonitor"],
4391
- communication: "none",
4401
+ communication: [],
4392
4402
  addons: ["turborepo"],
4393
4403
  examples: ["todo"],
4394
4404
  dbSetup: "mongodb-atlas",
@@ -4405,7 +4415,7 @@ const TEMPLATE_PRESETS = {
4405
4415
  auth: "better-auth",
4406
4416
  payments: [],
4407
4417
  observability: ["getmonitor"],
4408
- communication: "none",
4418
+ communication: [],
4409
4419
  addons: ["turborepo"],
4410
4420
  examples: ["todo"],
4411
4421
  dbSetup: "none",
@@ -4422,7 +4432,7 @@ const TEMPLATE_PRESETS = {
4422
4432
  auth: "better-auth",
4423
4433
  payments: [],
4424
4434
  observability: ["getmonitor"],
4425
- communication: "none",
4435
+ communication: [],
4426
4436
  addons: ["biome", "turborepo"],
4427
4437
  examples: ["none"],
4428
4438
  dbSetup: "none",
@@ -4439,7 +4449,7 @@ const TEMPLATE_PRESETS = {
4439
4449
  auth: "none",
4440
4450
  payments: [],
4441
4451
  observability: [],
4442
- communication: "none",
4452
+ communication: [],
4443
4453
  addons: ["none"],
4444
4454
  examples: ["none"],
4445
4455
  dbSetup: "none",
@@ -4535,6 +4545,7 @@ const formatOptions = {
4535
4545
  /** Files that must not be rewritten by post-scaffold formatters. */
4536
4546
  const SKIP_FILE_NAMES = new Set([
4537
4547
  "kubojs.jsonrc",
4548
+ "kubo.config.ts",
4538
4549
  "package-lock.json",
4539
4550
  "pnpm-lock.yaml",
4540
4551
  "yarn.lock",
@@ -6117,7 +6128,8 @@ function getDesktopStaticBuildNote(frontend) {
6117
6128
  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.`;
6118
6129
  }
6119
6130
  async function displayPostInstallInstructions(config) {
6120
- const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication } = config;
6131
+ const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication: rawCommunication } = config;
6132
+ const communication = normalizeCommunication$2(rawCommunication ?? []);
6121
6133
  const isConvex = backend === "convex";
6122
6134
  const isBackendSelf = backend === "self";
6123
6135
  const runCmd = packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
@@ -6140,9 +6152,9 @@ async function displayPostInstallInstructions(config) {
6140
6152
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
6141
6153
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
6142
6154
  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) : "";
6155
+ const resendInstructions = communication.includes("resend") ? getResendInstructions() : "";
6156
+ const notifiqueInstructions = communication.includes("notifique") ? getNotifiqueInstructions() : "";
6157
+ const araraInstructions = communication.includes("arara") ? getAraraInstructions(backend) : "";
6146
6158
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
6147
6159
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
6148
6160
  const webPort = (0, types_exports.getWebPort)(frontend ?? []);
@@ -6540,7 +6552,7 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
6540
6552
  auth: "none",
6541
6553
  payments: [],
6542
6554
  observability: [],
6543
- communication: "none",
6555
+ communication: [],
6544
6556
  git: false,
6545
6557
  packageManager: "npm",
6546
6558
  install: false,
@@ -7074,7 +7086,7 @@ async function createVirtual(options) {
7074
7086
  auth: options.auth || "none",
7075
7087
  payments: rawPayments === "none" ? [] : Array.isArray(rawPayments) ? rawPayments : rawPayments ? [rawPayments] : [],
7076
7088
  observability: options.disableObservability ? [] : rawObservability === "none" ? [] : Array.isArray(rawObservability) ? rawObservability : rawObservability ? [rawObservability] : [],
7077
- communication: options.communication || "none",
7089
+ communication: options.communication ? (0, types_exports.normalizeCommunication)(options.communication) : [],
7078
7090
  git: options.git ?? false,
7079
7091
  packageManager: options.packageManager || "bun",
7080
7092
  install: false,
@@ -7105,6 +7117,7 @@ async function createVirtual(options) {
7105
7117
  if (validationResult.isErr()) return Result.err(new GeneratorError({
7106
7118
  message: validationResult.error.message,
7107
7119
  phase: "validation",
7120
+ compatibilityCode: validationResult.error.compatibilityCode,
7108
7121
  cause: validationResult.error
7109
7122
  }));
7110
7123
  return generate({