create-kubojs 0.2.2 → 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";
@@ -87,7 +88,7 @@ const DEFAULT_CONFIG_BASE = {
87
88
  auth: "better-auth",
88
89
  payments: [],
89
90
  observability: ["getmonitor"],
90
- communication: "none",
91
+ communication: ["arara"],
91
92
  addons: ["turborepo"],
92
93
  examples: [],
93
94
  testing: [],
@@ -114,27 +115,6 @@ function getDefaultConfig() {
114
115
  };
115
116
  }
116
117
  const DEFAULT_CONFIG = getDefaultConfig();
117
- const ADDON_COMPATIBILITY = {
118
- pwa: [
119
- "tanstack-router",
120
- "react-router",
121
- "solid",
122
- "next"
123
- ],
124
- tauri: desktopWebFrontends$3,
125
- electrobun: desktopWebFrontends$3,
126
- biome: [],
127
- husky: [],
128
- lefthook: [],
129
- turborepo: [],
130
- "vite-plus": [],
131
- mcp: [],
132
- oxlint: [],
133
- opentui: [],
134
- skills: [],
135
- "s3-storage": [],
136
- none: []
137
- };
138
118
  //#endregion
139
119
  //#region src/utils/context.ts
140
120
  const cliStorage = new AsyncLocalStorage();
@@ -304,6 +284,14 @@ var AddonSetupError = class extends TaggedError("AddonSetupError")() {
304
284
  }
305
285
  };
306
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
+ /**
307
295
  * Create a user cancelled error Result
308
296
  */
309
297
  function userCancelled(message) {
@@ -461,27 +449,7 @@ async function clearHistory() {
461
449
  }
462
450
  //#endregion
463
451
  //#region src/utils/render-title.ts
464
- const TITLE_TEXT = `
465
- ██╗ ██╗██╗ ██╗██████╗ ██████╗
466
- ██║ ██╔╝██║ ██║██╔══██╗██╔═══██╗
467
- █████╔╝ ██║ ██║██████╔╝██║ ██║
468
- ██╔═██╗ ██║ ██║██╔══██╗██║ ██║
469
- ██║ ██╗╚██████╔╝██████╔╝╚██████╔╝
470
- ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
471
- `;
472
- const KUBO_TITLE_COLORS = [
473
- "#D6A72B",
474
- "#F5D76E",
475
- "#E0B43E",
476
- "#E08A2E",
477
- "#E0B43E",
478
- "#E8A43A",
479
- "#E08A2E",
480
- "#E08A2E",
481
- "#C49314",
482
- "#E8C978",
483
- "#E5D3A5"
484
- ];
452
+ const TITLE_TEXT = KUBO_CLI_TITLE;
485
453
  const renderTitle = () => {
486
454
  const terminalWidth = process.stdout.columns || 80;
487
455
  const titleLines = TITLE_TEXT.split("\n");
@@ -589,24 +557,224 @@ import * as import__kubojs_types from "@kubojs/types";
589
557
  __reExport(types_exports, import__kubojs_types);
590
558
  //#endregion
591
559
  //#region src/utils/compatibility.ts
592
- const WEB_FRAMEWORKS = [
593
- "tanstack-router",
594
- "react-router",
595
- "tanstack-start",
596
- "next",
597
- "nuxt",
598
- "svelte",
599
- "solid",
600
- "astro"
601
- ];
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
+ }
602
771
  //#endregion
603
772
  //#region src/utils/compatibility-rules.ts
604
773
  const TASK_RUNNER_ADDONS$1 = ["turborepo", "vite-plus"];
605
774
  /** Mutually exclusive code-quality linters (one slot). */
606
775
  const LINTER_ADDONS = ["biome", "oxlint"];
607
- const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
608
776
  function isLinterAddon(addon) {
609
- return LINTER_ADDONS.includes(addon);
777
+ return LINTER_ADDONS.some((value) => value === addon);
610
778
  }
611
779
  /**
612
780
  * When adding a code-quality linter, drop sibling linters so only one remains.
@@ -627,203 +795,96 @@ function mergeAddonsExclusive(existingAddons, addonsToAdd) {
627
795
  removedAddons: [...removed]
628
796
  };
629
797
  }
630
- const TAURI_STATIC_EXPORT_FRONTENDS = ["next", "tanstack-start"];
631
- const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
632
- "nuxt",
633
- "svelte",
634
- "solid",
635
- "astro"
636
- ];
637
- const CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS = [
638
- "tanstack-router",
639
- "react-router",
640
- "tanstack-start",
641
- "next",
642
- "native-bare",
643
- "native-uniwind",
644
- "native-unistyles"
645
- ];
646
- function validationErr$1(message) {
647
- return Result.err(new ValidationError({ message }));
648
- }
649
- function isWebFrontend(value) {
798
+ function isWebFrontend$1(value) {
650
799
  return WEB_FRAMEWORKS.includes(value);
651
800
  }
652
801
  function isPlaywrightAllowed(frontends = []) {
653
- return frontends.some((frontend) => isWebFrontend(frontend));
802
+ return frontends.some((frontend) => isWebFrontend$1(frontend));
654
803
  }
655
804
  function validateTestingAgainstFrontends(testing = [], frontends = []) {
656
- if (testing.includes("playwright") && !isPlaywrightAllowed(frontends)) return validationErr$1("playwright testing requires a web frontend");
657
- return Result.ok(void 0);
658
- }
659
- function splitFrontends(values = []) {
660
- return {
661
- web: values.filter((f) => isWebFrontend(f)),
662
- native: values.filter((f) => f === "native-bare" || f === "native-uniwind" || f === "native-unistyles")
663
- };
664
- }
665
- function ensureSingleWebAndNative(frontends) {
666
- const { web, native } = splitFrontends(frontends);
667
- 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");
668
- if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
669
- return Result.ok(void 0);
670
- }
671
- const FULLSTACK_FRONTENDS$1 = [
672
- "next",
673
- "tanstack-start",
674
- "nuxt",
675
- "svelte",
676
- "astro"
677
- ];
678
- function validateSelfBackendCompatibility(providedFlags, options, config) {
679
- const backend = config.backend || options.backend;
680
- const frontends = config.frontend || options.frontend || [];
681
- if (backend === "self") {
682
- const { web, native } = splitFrontends(frontends);
683
- 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.");
684
- if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
685
- }
686
- const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
687
- 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.");
688
- return Result.ok(void 0);
689
- }
690
- function validateWorkersCompatibility(providedFlags, options, config) {
691
- 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.`);
692
- 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.`);
693
- 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.");
694
- 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.");
695
- return Result.ok(void 0);
805
+ return validateWithCompatibilityEvaluator({
806
+ testing,
807
+ frontend: frontends
808
+ }, void 0, ["testing-frontend"]);
696
809
  }
697
810
  function validateApiFrontendCompatibility(api, frontends = []) {
698
- const includesNuxt = frontends.includes("nuxt");
699
- const includesSvelte = frontends.includes("svelte");
700
- const includesSolid = frontends.includes("solid");
701
- const includesAstro = frontends.includes("astro");
702
- 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.`);
703
- return Result.ok(void 0);
811
+ return validateWithCompatibilityEvaluator({
812
+ api,
813
+ frontend: frontends
814
+ }, void 0, ["api-frontend"]);
704
815
  }
705
816
  function isFrontendAllowedWithBackend(frontend, backend, auth) {
706
- if (backend === "convex") {
707
- if (auth === "better-auth" && CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS.includes(frontend)) return false;
708
- if (frontend === "solid" || frontend === "astro") return false;
709
- }
710
- if (auth === "clerk") {
711
- if ([
712
- "nuxt",
713
- "svelte",
714
- "solid",
715
- "astro"
716
- ].includes(frontend)) return false;
717
- }
718
- 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));
719
827
  }
720
828
  function supportsConvexBetterAuth(frontends = []) {
721
- 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));
722
830
  }
723
831
  function allowedApisForFrontends(frontends = [], backend) {
724
- const includesNuxt = frontends.includes("nuxt");
725
- const includesSvelte = frontends.includes("svelte");
726
- const includesSolid = frontends.includes("solid");
727
- const includesAstro = frontends.includes("astro");
728
- const base = [
832
+ if (backendAllowsOnlyNoneApi(backend)) return ["none"];
833
+ const supportsTrpc = !frontends.some((frontend) => WEB_FRONTENDS_REQUIRING_ORPC.includes(frontend));
834
+ return [
729
835
  "trpc",
730
- "orpc",
731
- "none"
732
- ];
733
- if (backend === "hono") base.splice(2, 0, "orval");
734
- if (includesNuxt || includesSvelte || includesSolid || includesAstro) return backend === "hono" ? [
735
836
  "orpc",
736
837
  "orval",
737
838
  "none"
738
- ] : ["orpc", "none"];
739
- return base;
839
+ ].filter((api) => (api !== "trpc" || supportsTrpc) && isApiCompatibleWithBackend(api, backend));
740
840
  }
741
841
  function isApiCompatibleWithBackend(api, backend) {
742
- return api !== "orval" || backend === "hono";
842
+ return api !== "orval" || backendAllowsApi(backend, "orval");
743
843
  }
744
844
  function isExampleTodoAllowed(backend, database, api) {
745
- if (backend === "convex") return true;
746
- if (database === "none" || api === "none") return false;
747
- 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);
748
858
  }
749
859
  function isExampleAIAllowed(backend, frontends = []) {
750
- if (backend === "none") return false;
751
- const includesSolid = frontends.includes("solid");
752
- const includesAstro = frontends.includes("astro");
753
- if (includesSolid || includesAstro) return false;
754
- if (backend === "convex") {
755
- const includesNuxt = frontends.includes("nuxt");
756
- const includesSvelte = frontends.includes("svelte");
757
- if (includesNuxt || includesSvelte) return false;
758
- }
759
- return true;
760
- }
761
- function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
762
- if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) return validationErr$1("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
763
- return Result.ok(void 0);
764
- }
765
- function validateServerDeployRequiresBackend(serverDeploy, backend) {
766
- if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
767
- return Result.ok(void 0);
768
- }
769
- function validateDockerServerDeploy(serverDeploy, backend, runtime) {
770
- if (serverDeploy !== "docker") return Result.ok(void 0);
771
- 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.");
772
- 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'.");
773
- return Result.ok(void 0);
774
- }
775
- function validateVercelServerDeploy(serverDeploy, backend, runtime) {
776
- if (serverDeploy !== "vercel") return Result.ok(void 0);
777
- 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.");
778
- 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'.");
779
- return Result.ok(void 0);
780
- }
781
- function validateGuaraCloudServerDeploy(serverDeploy, backend, runtime) {
782
- if (serverDeploy !== "guaracloud") return Result.ok(void 0);
783
- 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.");
784
- 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'.");
785
- return Result.ok(void 0);
786
- }
787
- const DOCKER_SERVER_OUTPUT_FRONTENDS = [
788
- "next",
789
- "svelte",
790
- "astro",
791
- "react-router"
792
- ];
793
- function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backend, auth) {
794
- if (webDeploy !== "docker" || !addons || !frontend) return Result.ok(void 0);
795
- const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
796
- if (desktopAddons.length === 0) return Result.ok(void 0);
797
- const affected = frontend.find((f) => DOCKER_SERVER_OUTPUT_FRONTENDS.includes(f));
798
- if (!affected) return Result.ok(void 0);
799
- if (affected === "next" && !desktopAddons.includes("tauri") && backend === "convex" && auth === "better-auth") return Result.ok(void 0);
800
- return validationErr$1(`'--web-deploy docker' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons switch the web build to a static export, which the docker image cannot serve. Remove the addon or use a static-serving frontend (tanstack-router, solid).`);
801
- }
802
- function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
803
- if (STATIC_DESKTOP_ADDONS.includes(addon) && auth === "clerk" && frontend.includes("react-router")) return {
804
- isCompatible: false,
805
- reason: `${addon} addon forces React Router into a static export, but Clerk on React Router requires SSR middleware. Remove the addon or use a different auth/frontend.`
806
- };
807
- if (backend === "self" && STATIC_DESKTOP_ADDONS.includes(addon)) return {
808
- isCompatible: false,
809
- reason: `${addon} addon requires a separate backend or no backend because backend 'self' emits server routes that cannot be bundled as static desktop assets.`
810
- };
811
- if (addon === "tauri" && backend === "convex" && auth === "better-auth" && frontend.some((f) => TAURI_STATIC_EXPORT_FRONTENDS.includes(f))) return {
812
- isCompatible: false,
813
- reason: "tauri addon is not compatible with Convex Better Auth on Next.js or TanStack Start because those templates use server auth bootstrap and cannot be exported as static desktop assets."
814
- };
815
- 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 ? {
816
880
  isCompatible: false,
817
- reason: `${addon} is not a supported addon`
818
- };
819
- const compatibleFrontends = ADDON_COMPATIBILITY[addon];
820
- if (compatibleFrontends.length > 0) {
821
- if (!frontend.some((f) => compatibleFrontends.some((compatibleFrontend) => compatibleFrontend === f))) return {
822
- isCompatible: false,
823
- reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
824
- };
825
- }
826
- return { isCompatible: true };
881
+ reason: getCompatibilityValidationMessage(issue, {
882
+ addons: [addon],
883
+ frontend,
884
+ auth,
885
+ backend
886
+ })
887
+ } : { isCompatible: true };
827
888
  }
828
889
  function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, backend, runtime) {
829
890
  return allAddons.filter((addon) => {
@@ -835,64 +896,23 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
835
896
  });
836
897
  }
837
898
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
838
- if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo' and 'vite-plus' addons. Choose one task runner.");
839
- if ([...new Set(addons.filter((addon) => LINTER_ADDONS.includes(addon)))].length > 1) return validationErr$1("Cannot combine 'biome' and 'oxlint' addons. Choose one code-quality linter.");
840
- for (const addon of addons) {
841
- if (addon === "none") continue;
842
- const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
843
- if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
844
- }
845
- 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
+ ]);
846
912
  }
847
913
  function validateAddonsAgainstConfig(addons = [], config) {
848
914
  return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
849
915
  }
850
- function validateCommunicationCompatibility(communication, backend) {
851
- if (!communication || communication === "none") return Result.ok(void 0);
852
- if ((communication === "resend" || communication === "notifique" || communication === "arara") && backend === "none") return validationErr$1(`${communication === "notifique" ? "Notifique" : communication === "arara" ? "AraraHQ" : "Resend"} communication requires a server backend. Please choose a backend or use '--communication none'.`);
853
- return Result.ok(void 0);
854
- }
855
- function validateAraraRuntimeCompatibility(communication, backend, runtime, serverDeploy) {
856
- if (communication !== "arara" || backend === "convex") return Result.ok(void 0);
857
- if (runtime === "workers" || serverDeploy === "cloudflare") return validationErr$1("AraraHQ requires the official Node SDK and is not compatible with Edge/Workers runtimes. Use a Node/Bun server deployment or Convex Node Action.");
858
- return Result.ok(void 0);
859
- }
860
- function validatePaymentsCompatibility(payments, auth, backend, frontends = [], orm, database) {
861
- if (!payments || payments.length === 0) return Result.ok(void 0);
862
- for (const provider of payments) {
863
- const issue = getPaymentCompatibilityIssue({
864
- provider,
865
- backend,
866
- frontends,
867
- database,
868
- orm
869
- });
870
- const label = provider === "stripe" ? "Stripe" : "AbacatePay";
871
- if (issue === "convex-unsupported") return validationErr$1(`${label} payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.`);
872
- if (issue === "requires-web-frontend") return validationErr$1(`${label} payments requires a web frontend. Please choose next, tanstack-start, tanstack-router, react-router, nuxt, svelte, solid, or astro.`);
873
- if (issue === "native-only-unsupported") return validationErr$1(`${label} payments is not compatible with native-only frontends. Please add a supported web frontend or leave payments unselected.`);
874
- if (issue === "sql-database-required") return validationErr$1("AbacatePay payments v1 requires a SQL database with Drizzle or Prisma. MongoDB and Mongoose are not supported.");
875
- }
876
- return Result.ok(void 0);
877
- }
878
- function validateExamplesCompatibility(examples, backend, database, frontend, api) {
879
- const examplesArr = examples ?? [];
880
- if (examplesArr.length === 0 || examplesArr.includes("none")) return Result.ok(void 0);
881
- if (examplesArr.includes("todo") && backend !== "convex") {
882
- if (database === "none") return validationErr$1("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
883
- if (api === "none") return validationErr$1("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
884
- }
885
- if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) return validationErr$1("The 'ai' example is not compatible with the Solid frontend.");
886
- if (examplesArr.includes("ai") && (frontend ?? []).includes("astro")) return validationErr$1("The 'ai' example is not compatible with the Astro frontend.");
887
- if (examplesArr.includes("ai") && backend === "none") return validationErr$1("The 'ai' example requires a backend.");
888
- if (examplesArr.includes("ai") && backend === "convex") {
889
- const frontendArr = frontend ?? [];
890
- const includesNuxt = frontendArr.includes("nuxt");
891
- const includesSvelte = frontendArr.includes("svelte");
892
- 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.");
893
- }
894
- return Result.ok(void 0);
895
- }
896
916
  //#endregion
897
917
  //#region src/utils/navigation.ts
898
918
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
@@ -1298,6 +1318,66 @@ async function getAddonsToAdd(config) {
1298
1318
  return response;
1299
1319
  }
1300
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
1301
1381
  //#region src/utils/input-hardening.ts
1302
1382
  function hasControlCharacters(value) {
1303
1383
  for (const char of value) {
@@ -1318,34 +1398,83 @@ function validateAgentSafePathInput(value, field) {
1318
1398
  return Result.ok(void 0);
1319
1399
  }
1320
1400
  //#endregion
1321
- //#region src/utils/kubojs-config.ts
1322
- const KUBOJS_CONFIG_FILE = "kubojs.jsonrc";
1323
- /**
1324
- * Reads the kubojs configuration file from the project directory.
1325
- */
1326
- async function readKubojsConfig(projectDir) {
1327
- try {
1328
- const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1329
- if (!await fs.pathExists(configPath)) return null;
1330
- return parse(await fs.readFile(configPath, "utf-8"));
1331
- } catch {
1332
- 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;
1333
1413
  }
1414
+ return null;
1334
1415
  }
1335
- /**
1336
- * Updates specific fields in the kubojs configuration file.
1337
- */
1338
- async function updateKubojsConfig(projectDir, updates) {
1339
- try {
1340
- const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1341
- if (!await fs.pathExists(configPath)) return;
1342
- let content = await fs.readFile(configPath, "utf-8");
1343
- for (const [key, value] of Object.entries(updates)) {
1344
- const edits = modify(content, [key], value, { formattingOptions: { tabSize: 2 } });
1345
- content = applyEdits(content, edits);
1346
- }
1347
- await fs.writeFile(configPath, content, "utf-8");
1348
- } 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);
1349
1478
  }
1350
1479
  //#endregion
1351
1480
  //#region src/utils/add-package-deps.ts
@@ -2069,31 +2198,82 @@ function hasReactBasedFrontend(frontend) {
2069
2198
  function hasNativeFrontend(frontend) {
2070
2199
  return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2071
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
+ };
2072
2275
  function getRecommendedSourceKeys(config) {
2073
- const sources = [];
2074
- const { frontend, backend, dbSetup, auth, examples, addons, orm, webDeploy, serverDeploy } = config;
2075
- if (hasReactBasedFrontend(frontend)) {
2076
- sources.push("vercel-labs/agent-skills");
2077
- sources.push("shadcn/ui");
2078
- }
2079
- if ((webDeploy === "vercel" || serverDeploy === "vercel") && !sources.includes("vercel-labs/agent-skills")) sources.push("vercel-labs/agent-skills");
2080
- if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
2081
- if (frontend.includes("nuxt")) sources.push("nuxt/ui");
2082
- if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
2083
- if (hasNativeFrontend(frontend)) sources.push("expo/skills");
2084
- if (auth === "better-auth") sources.push("better-auth/skills");
2085
- if (auth === "clerk") sources.push("clerk/skills");
2086
- if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
2087
- if (dbSetup === "supabase") sources.push("supabase/agent-skills");
2088
- if (dbSetup === "planetscale") sources.push("planetscale/database-skills");
2089
- if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
2090
- if (examples.includes("ai")) sources.push("vercel/ai");
2091
- if (addons.includes("turborepo")) sources.push("vercel/turborepo");
2092
- if (backend === "hono") sources.push("yusukebe/hono-skill");
2093
- if (backend === "elysia") sources.push("elysiajs/skills");
2094
- if (backend === "convex") sources.push("waynesutton/convexskills");
2095
- if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
2096
- return sources;
2276
+ return uniqueValues(Object.values(RECOMMENDED_SOURCE_RULES).flatMap(({ when, sources }) => when(config) ? sources : []));
2097
2277
  }
2098
2278
  const CURATED_SKILLS_BY_SOURCE = {
2099
2279
  "vercel-labs/agent-skills": (config) => {
@@ -2173,7 +2353,12 @@ function uniqueValues(values) {
2173
2353
  async function setupSkills(config) {
2174
2354
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2175
2355
  const { packageManager, projectDir } = config;
2176
- 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;
2177
2362
  const fullConfig = kubojsConfig ? {
2178
2363
  ...config,
2179
2364
  addons: kubojsConfig.addons ?? config.addons,
@@ -2618,103 +2803,36 @@ async function installPlaywrightBrowsers(projectDir, packageManager) {
2618
2803
  }
2619
2804
  }
2620
2805
  //#endregion
2621
- //#region src/utils/config-processing.ts
2622
- function processArrayOption(options) {
2623
- if (!options || options.length === 0) return [];
2624
- if (options.includes("none")) return [];
2625
- return options.filter((item) => item !== "none");
2626
- }
2627
- function deriveProjectName(projectName, projectDirectory) {
2628
- if (projectName) return projectName;
2629
- if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
2630
- return "";
2631
- }
2632
- function processFlags(options, projectName) {
2633
- const config = {};
2634
- if (options.api) config.api = options.api;
2635
- if (options.addonOptions) config.addonOptions = options.addonOptions;
2636
- if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
2637
- if (options.backend) config.backend = options.backend;
2638
- if (options.database) config.database = options.database;
2639
- if (options.orm) config.orm = options.orm;
2640
- if (options.auth !== void 0) config.auth = options.auth;
2641
- if (options.payments !== void 0) config.payments = normalizePayments(options.payments);
2642
- if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
2643
- if (options.disableObservability) config.observability = [];
2644
- if (options.communication !== void 0) config.communication = options.communication;
2645
- if (options.git !== void 0) config.git = options.git;
2646
- if (options.install !== void 0) config.install = options.install;
2647
- if (options.runtime) config.runtime = options.runtime;
2648
- if (options.dbSetup) config.dbSetup = options.dbSetup;
2649
- if (options.packageManager) config.packageManager = options.packageManager;
2650
- if (options.webDeploy) config.webDeploy = options.webDeploy;
2651
- if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
2652
- const derivedName = deriveProjectName(projectName, options.projectDirectory);
2653
- if (derivedName) config.projectName = projectName || derivedName;
2654
- if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
2655
- if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
2656
- if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
2657
- if (options.testing && options.testing.length > 0) config.testing = processArrayOption(options.testing);
2658
- return config;
2659
- }
2660
- function normalizeObservability(value) {
2661
- if (value === "none" || value === void 0) return [];
2662
- return [...new Set((Array.isArray(value) ? value : [value]).filter((item) => item === "getmonitor" || item === "himetrica"))];
2663
- }
2664
- function getProvidedFlags(options) {
2665
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
2666
- }
2667
- function validateNoneExclusivity(options, optionName) {
2668
- if (!options || options.length === 0) return Result.ok(void 0);
2669
- if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
2670
- return Result.ok(void 0);
2671
- }
2672
- function validateArrayOptions(options) {
2673
- if (options.disableObservability && options.observability !== void 0) return Result.err(new ValidationError({ message: "Cannot combine '--disable-observability' with '--observability'." }));
2674
- const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
2675
- if (frontendResult.isErr()) return frontendResult;
2676
- const addonsResult = validateNoneExclusivity(options.addons, "addons");
2677
- if (addonsResult.isErr()) return addonsResult;
2678
- const examplesResult = validateNoneExclusivity(options.examples, "examples");
2679
- if (examplesResult.isErr()) return examplesResult;
2680
- const testingResult = validateNoneExclusivity(options.testing, "testing");
2681
- if (testingResult.isErr()) return testingResult;
2682
- return Result.ok(void 0);
2683
- }
2684
- //#endregion
2685
2806
  //#region src/helpers/core/detect-project-config.ts
2686
2807
  async function detectProjectConfig(projectDir) {
2687
- const result = await Result.tryPromise({
2688
- try: async () => {
2689
- const kubojsConfig = await readKubojsConfig(projectDir);
2690
- if (kubojsConfig) return {
2691
- projectDir,
2692
- projectName: path.basename(projectDir),
2693
- addonOptions: kubojsConfig.addonOptions,
2694
- dbSetupOptions: kubojsConfig.dbSetupOptions,
2695
- database: kubojsConfig.database,
2696
- orm: kubojsConfig.orm,
2697
- backend: kubojsConfig.backend,
2698
- runtime: kubojsConfig.runtime,
2699
- frontend: kubojsConfig.frontend,
2700
- addons: kubojsConfig.addons,
2701
- examples: kubojsConfig.examples,
2702
- testing: kubojsConfig.testing,
2703
- auth: kubojsConfig.auth,
2704
- payments: kubojsConfig.payments,
2705
- observability: normalizeObservability(kubojsConfig.observability),
2706
- communication: kubojsConfig.communication ?? "none",
2707
- packageManager: kubojsConfig.packageManager,
2708
- dbSetup: kubojsConfig.dbSetup,
2709
- api: kubojsConfig.api,
2710
- webDeploy: kubojsConfig.webDeploy,
2711
- serverDeploy: kubojsConfig.serverDeploy
2712
- };
2713
- return null;
2714
- },
2715
- 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
2716
2835
  });
2717
- return result.isOk() ? result.value : null;
2718
2836
  }
2719
2837
  //#endregion
2720
2838
  //#region src/helpers/core/install-dependencies.ts
@@ -2740,32 +2858,56 @@ async function installDependencies({ projectDir, packageManager }) {
2740
2858
  return result;
2741
2859
  }
2742
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
2743
2910
  //#region src/helpers/core/add-handler.ts
2744
- const ADD_PACKAGE_JSON_PATHS = [
2745
- "package.json",
2746
- "apps/server/package.json",
2747
- "apps/web/package.json",
2748
- "apps/native/package.json",
2749
- "apps/desktop/package.json",
2750
- "apps/docs/package.json",
2751
- "apps/api/package.json",
2752
- "packages/db/package.json",
2753
- "packages/auth/package.json",
2754
- "packages/backend/package.json",
2755
- "packages/config/package.json",
2756
- "packages/env/package.json",
2757
- "packages/infra/package.json",
2758
- "packages/ui/package.json"
2759
- ];
2760
- const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
2761
- const ADD_ENV_FILE_PATHS = [
2762
- ".env",
2763
- "apps/web/.env",
2764
- "apps/server/.env",
2765
- "apps/native/.env",
2766
- "packages/backend/.env.local",
2767
- "packages/infra/.env"
2768
- ];
2769
2911
  const HOOK_ADDONS = ["husky", "lefthook"];
2770
2912
  const HOOK_LINTER_ADDONS = [
2771
2913
  "biome",
@@ -2832,8 +2974,9 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
2832
2974
  }
2833
2975
  }
2834
2976
  }
2835
- function updateViteConfigImportsForVitePlus(vfs) {
2836
- 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) {
2837
2980
  const content = vfs.readFile(viteConfigPath);
2838
2981
  if (!content) continue;
2839
2982
  const updatedContent = content.replaceAll("from \"vite\";", "from \"vite-plus\";").replaceAll("from 'vite';", "from 'vite-plus';");
@@ -2876,8 +3019,13 @@ async function addHandlerInternal(input) {
2876
3019
  renderTitle();
2877
3020
  intro(cliColors.signal("Add addons to your kubojs project"));
2878
3021
  }
2879
- const existingConfig = await detectProjectConfig(projectDir);
2880
- 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.` }));
2881
3029
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
2882
3030
  let addonsToAdd;
2883
3031
  if (input.addons && input.addons.length > 0) {
@@ -2926,10 +3074,20 @@ async function addHandlerInternal(input) {
2926
3074
  if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
2927
3075
  }
2928
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
+ }));
2929
3086
  const config = {
2930
3087
  projectName: existingConfig.projectName,
2931
3088
  projectDir,
2932
3089
  relativePath: ".",
3090
+ layout,
2933
3091
  addonOptions: mergedAddonOptions,
2934
3092
  database: existingConfig.database,
2935
3093
  orm: existingConfig.orm,
@@ -2941,7 +3099,7 @@ async function addHandlerInternal(input) {
2941
3099
  auth: existingConfig.auth,
2942
3100
  payments: existingConfig.payments,
2943
3101
  observability: existingConfig.observability,
2944
- communication: existingConfig.communication ?? "none",
3102
+ communication: normalizeCommunication$2(existingConfig.communication ?? []),
2945
3103
  testing: updatedTesting,
2946
3104
  git: false,
2947
3105
  packageManager: input.packageManager || existingConfig.packageManager,
@@ -2957,45 +3115,27 @@ async function addHandlerInternal(input) {
2957
3115
  };
2958
3116
  if (!isSilent()) log.info(pc.dim("Installing addon files..."));
2959
3117
  const vfs = new VirtualFileSystem();
2960
- for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
2961
- const fullPath = path.join(projectDir, pkgPath);
2962
- if (await fs.pathExists(fullPath)) {
2963
- const content = await fs.readFile(fullPath, "utf-8");
2964
- vfs.writeFile(pkgPath, content);
2965
- }
2966
- }
2967
- for (const filePath of ADD_TEXT_FILE_PATHS) {
2968
- const fullPath = path.join(projectDir, filePath);
2969
- if (await fs.pathExists(fullPath)) {
2970
- const content = await fs.readFile(fullPath, "utf-8");
2971
- vfs.writeFile(filePath, content);
2972
- }
2973
- }
2974
- for (const filePath of ADD_ENV_FILE_PATHS) {
2975
- const fullPath = path.join(projectDir, filePath);
2976
- if (await fs.pathExists(fullPath)) {
2977
- const content = await fs.readFile(fullPath, "utf-8");
2978
- vfs.writeFile(filePath, content);
2979
- }
2980
- }
2981
- await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
2982
- 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
+ });
2983
3124
  processEnvVariables(vfs, updatedConfig);
2984
- if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
2985
- if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
2986
3125
  if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
2987
- if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
3126
+ if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs, getWebAppPath(updatedConfig));
2988
3127
  if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
3128
+ const changedFiles = new Set(vfs.getChangedFiles());
2989
3129
  const tree = {
2990
3130
  root: vfs.toTree(""),
2991
- fileCount: vfs.getFileCount(),
3131
+ fileCount: changedFiles.size,
2992
3132
  directoryCount: vfs.getDirectoryCount(),
2993
3133
  config: updatedConfig
2994
3134
  };
2995
3135
  if (input.dryRun) {
2996
3136
  if (!isSilent()) {
2997
3137
  log.success(pc.green("Dry run validation passed. No addon files were written."));
2998
- log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
3138
+ log.info(pc.dim(`Planned addon files: ${changedFiles.size}`));
2999
3139
  outro(cliColors.signal("Dry run complete."));
3000
3140
  }
3001
3141
  return Result.ok({
@@ -3006,9 +3146,9 @@ async function addHandlerInternal(input) {
3006
3146
  plannedFileCount: vfs.getFileCount()
3007
3147
  });
3008
3148
  }
3009
- const writeResult = await writeTree(tree, projectDir);
3149
+ const writeResult = await writeSelected(tree, projectDir, (filePath) => changedFiles.has(filePath));
3010
3150
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
3011
- 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`));
3012
3152
  await cleanupRemovedLinters(projectDir, removedAddons);
3013
3153
  const setupResult = await Result.tryPromise({
3014
3154
  try: () => setupAddons({
@@ -3032,11 +3172,16 @@ async function addHandlerInternal(input) {
3032
3172
  })
3033
3173
  });
3034
3174
  if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
3035
- await updateKubojsConfig(projectDir, {
3175
+ const configUpdateResult = await updateProjectKuboConfig(projectDir, {
3036
3176
  addons: updatedAddons,
3037
3177
  addonOptions: updatedConfig.addonOptions,
3038
- testing: updatedTesting
3178
+ testing: updatedTesting,
3179
+ layout
3039
3180
  });
3181
+ if (configUpdateResult.isErr()) return Result.err(new CLIError({
3182
+ message: configUpdateResult.error.message,
3183
+ cause: configUpdateResult.error
3184
+ }));
3040
3185
  if (input.install) {
3041
3186
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
3042
3187
  await installDependencies({
@@ -3178,6 +3323,10 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
3178
3323
  value: "elysia",
3179
3324
  label: "Elysia",
3180
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"
3181
3330
  });
3182
3331
  if (!hasIncompatibleFrontend) backendOptions.push({
3183
3332
  value: "convex",
@@ -3199,35 +3348,31 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
3199
3348
  }
3200
3349
  //#endregion
3201
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
+ ];
3202
3368
  async function getCommunicationChoice(communication, backend, previousValue) {
3203
- if (communication !== void 0) return communication;
3204
- if (backend === "none") return "none";
3205
- const options = [
3206
- {
3207
- value: "none",
3208
- label: "None",
3209
- hint: "No email/communication provider"
3210
- },
3211
- {
3212
- value: "resend",
3213
- label: "Resend",
3214
- hint: "Transactional email for developers"
3215
- },
3216
- {
3217
- value: "notifique",
3218
- label: "Notifique",
3219
- hint: "Omnichannel BR messaging API"
3220
- },
3221
- {
3222
- value: "arara",
3223
- label: "AraraHQ",
3224
- hint: "WhatsApp messaging via the official Node SDK"
3225
- }
3226
- ];
3227
- const response = await navigableSelect({
3228
- message: "Select communication provider",
3229
- options,
3230
- 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
3231
3376
  });
3232
3377
  if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3233
3378
  return response;
@@ -3383,7 +3528,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
3383
3528
  label: "Todo App",
3384
3529
  hint: "A simple CRUD example app"
3385
3530
  });
3386
- if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
3531
+ if (isExampleAIAllowed(backend, frontends ?? []) && isExampleAIAllowedForBackend(backend, "ai")) options.push({
3387
3532
  value: "ai",
3388
3533
  label: "AI Chat",
3389
3534
  hint: "A simple AI chat interface using AI SDK"
@@ -3574,219 +3719,21 @@ async function getObservabilityChoice(observability, previousValue) {
3574
3719
  }
3575
3720
  //#endregion
3576
3721
  //#region src/utils/config-validation.ts
3577
- function validationErr(message) {
3578
- return Result.err(new ValidationError({ message }));
3579
- }
3580
- function hasResolvedWorkersD1Target(config) {
3581
- return config.backend === "hono" && config.runtime === "workers" && config.serverDeploy === "cloudflare";
3582
- }
3583
- function hasResolvedSelfCloudflareD1Target(config) {
3584
- return config.backend === "self" && config.runtime === "none" && config.webDeploy === "cloudflare";
3585
- }
3586
- function canResolveWorkersD1Target(config) {
3587
- return (config.backend === void 0 || config.backend === "hono") && (config.runtime === void 0 || config.runtime === "workers") && (config.serverDeploy === void 0 || config.serverDeploy === "cloudflare");
3588
- }
3589
- function canResolveSelfCloudflareD1Target(config) {
3590
- return (config.backend === void 0 || config.backend === "self") && (config.runtime === void 0 || config.runtime === "none") && (config.webDeploy === void 0 || config.webDeploy === "cloudflare");
3591
- }
3592
3722
  /**
3593
- * Pure ORM + database compatibility check. Used by the flag-path
3594
- * validator below and by the orm prompt directly (for flag+prompt
3595
- * combos where one value came from a flag and the other from a
3596
- * prompt).
3723
+ * Keeps the prompt-level API stable while compatibility facts live in the
3724
+ * shared evaluator.
3597
3725
  */
3598
3726
  function validateOrmDatabaseCompat(orm, database) {
3599
- if (orm === "mongoose" && database && database !== "mongodb") return validationErr("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
3600
- 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.");
3601
- 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.");
3602
- if (database && database !== "none" && orm === "none") return validationErr("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
3603
- if (orm && orm !== "none" && database === "none") return validationErr("ORM selection requires a database. Please choose a database or set '--orm none'.");
3604
- return Result.ok(void 0);
3605
- }
3606
- function validateDatabaseOrmAuth(cfg, flags) {
3607
- const has = (k) => flags ? flags.has(k) : true;
3608
- if (!has("orm") || !has("database")) return Result.ok(void 0);
3609
- return validateOrmDatabaseCompat(cfg.orm, cfg.database);
3610
- }
3611
- function validateDatabaseSetup(config, providedFlags) {
3612
- const { dbSetup, database, runtime } = config;
3613
- 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'.");
3614
- const setupValidations = {
3615
- turso: {
3616
- database: "sqlite",
3617
- errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
3618
- },
3619
- neon: {
3620
- database: "postgres",
3621
- errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3622
- },
3623
- "prisma-postgres": {
3624
- database: "postgres",
3625
- errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3626
- },
3627
- planetscale: { errorMessage: "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup." },
3628
- "mongodb-atlas": {
3629
- database: "mongodb",
3630
- errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
3631
- },
3632
- supabase: {
3633
- database: "postgres",
3634
- errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
3635
- },
3636
- d1: {
3637
- database: "sqlite",
3638
- errorMessage: "Cloudflare D1 setup requires SQLite database."
3639
- },
3640
- docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
3641
- none: { errorMessage: "" }
3642
- };
3643
- if (dbSetup && dbSetup !== "none") {
3644
- const validation = setupValidations[dbSetup];
3645
- if (dbSetup === "planetscale") {
3646
- if (database !== "postgres" && database !== "mysql") return validationErr(validation.errorMessage);
3647
- } else if (validation.database && database !== validation.database) return validationErr(validation.errorMessage);
3648
- if (validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
3649
- if (dbSetup === "d1") {
3650
- const isWorkersTarget = hasResolvedWorkersD1Target(config);
3651
- const isSelfCloudflareTarget = hasResolvedSelfCloudflareD1Target(config);
3652
- const canResolveWorkersTarget = canResolveWorkersD1Target(config);
3653
- const canResolveSelfCloudflareTarget = canResolveSelfCloudflareD1Target(config);
3654
- 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.");
3655
- }
3656
- if (dbSetup === "docker") {
3657
- 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.");
3658
- 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.");
3659
- }
3660
- }
3661
- return Result.ok(void 0);
3662
- }
3663
- function validateConvexConstraints(config, providedFlags) {
3664
- const { backend } = config;
3665
- if (backend !== "convex") return Result.ok(void 0);
3666
- const has = (k) => providedFlags.has(k);
3667
- if (has("runtime") && config.runtime !== "none") return validationErr("Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3668
- if (has("database") && config.database !== "none") return validationErr("Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.");
3669
- if (has("orm") && config.orm !== "none") return validationErr("Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
3670
- if (has("api") && config.api !== "none") return validationErr("Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.");
3671
- 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'.");
3672
- 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'.");
3673
- if (has("auth") && config.auth === "better-auth") {
3674
- const incompatibleFrontends = config.frontend?.filter((f) => CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS.includes(f)) ?? [];
3675
- const hasSupportedFrontend = supportsConvexBetterAuth(config.frontend);
3676
- 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.`);
3677
- if (!hasSupportedFrontend) return validationErr(`Better Auth with '--backend convex' requires a supported frontend (${CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.join(", ")}).`);
3678
- }
3679
- return Result.ok(void 0);
3680
- }
3681
- function validateBackendNoneConstraints(config, providedFlags) {
3682
- const { backend } = config;
3683
- if (backend !== "none") return Result.ok(void 0);
3684
- const has = (k) => providedFlags.has(k);
3685
- if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3686
- if (has("database") && config.database !== "none") return validationErr("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
3687
- if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
3688
- if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
3689
- if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
3690
- if (has("payments") && (config.payments?.length ?? 0) > 0) return validationErr("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
3691
- if (has("communication") && config.communication !== "none") return validationErr("Backend 'none' requires '--communication none'. Please remove the --communication flag or set it to 'none'.");
3692
- if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
3693
- if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
3694
- return Result.ok(void 0);
3695
- }
3696
- function validateSelfBackendConstraints(config, providedFlags) {
3697
- const { backend } = config;
3698
- if (backend !== "self") return Result.ok(void 0);
3699
- const has = (k) => providedFlags.has(k);
3700
- if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
3701
- return Result.ok(void 0);
3702
- }
3703
- function validateBackendConstraints(config, providedFlags, options) {
3704
- const { backend } = config;
3705
- if (config.auth === "clerk" && config.frontend) {
3706
- const incompatibleFrontends = config.frontend.filter((f) => [
3707
- "nuxt",
3708
- "svelte",
3709
- "solid",
3710
- "astro"
3711
- ].includes(f));
3712
- 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.`);
3713
- }
3714
- if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self") {
3715
- 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.");
3716
- }
3717
- if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
3718
- const incompatibleFrontends = options.frontend.filter((f) => f === "solid" || f === "astro");
3719
- if (incompatibleFrontends.length > 0) return validationErr(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
3720
- }
3721
- return Result.ok(void 0);
3722
- }
3723
- function validateFrontendConstraints(config, providedFlags) {
3724
- const { frontend } = config;
3725
- if (frontend && frontend.length > 0) {
3726
- const singleWebNativeResult = ensureSingleWebAndNative(frontend);
3727
- if (singleWebNativeResult.isErr()) return singleWebNativeResult;
3728
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) {
3729
- const apiResult = validateApiFrontendCompatibility(config.api, frontend);
3730
- if (apiResult.isErr()) return apiResult;
3731
- }
3732
- }
3733
- const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
3734
- const webDeployResult = validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
3735
- if (webDeployResult.isErr()) return webDeployResult;
3736
- return Result.ok(void 0);
3737
- }
3738
- function validateApiConstraints(config, options) {
3739
- if (!isApiCompatibleWithBackend(config.api, config.backend)) return validationErr("Orval API requires the Hono backend in this version. Please use '--backend hono' or choose tRPC/oRPC.");
3740
- if (config.api === "orval" && options.examples?.includes("todo")) return validationErr("The Orval API layer does not support the generated todo example yet. Please remove 'todo' from --examples or choose tRPC/oRPC.");
3741
- if (config.api === "none") {
3742
- if (options.examples?.includes("todo") && options.backend !== "convex" && options.backend !== "none") return validationErr("Cannot use '--examples todo' when '--api' is set to 'none'. The todo example requires an API layer. Please remove 'todo' from --examples or choose an API type.");
3743
- }
3744
- return Result.ok(void 0);
3745
- }
3746
- function validateFullConfig(config, providedFlags, options) {
3747
- return Result.gen(function* () {
3748
- yield* validateDatabaseOrmAuth(config, providedFlags);
3749
- yield* validateDatabaseSetup(config, providedFlags);
3750
- yield* validateConvexConstraints(config, providedFlags);
3751
- yield* validateBackendNoneConstraints(config, providedFlags);
3752
- yield* validateSelfBackendConstraints(config, providedFlags);
3753
- yield* validateBackendConstraints(config, providedFlags, options);
3754
- yield* validateFrontendConstraints(config, providedFlags);
3755
- yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
3756
- yield* validateApiConstraints(config, options);
3757
- yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
3758
- yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
3759
- yield* validateVercelServerDeploy(config.serverDeploy, config.backend, config.runtime);
3760
- yield* validateGuaraCloudServerDeploy(config.serverDeploy, config.backend, config.runtime);
3761
- yield* validateDockerWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend, config.backend, config.auth);
3762
- yield* validateSelfBackendCompatibility(providedFlags, options, config);
3763
- yield* validateWorkersCompatibility(providedFlags, options, config);
3764
- if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
3765
- 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.`);
3766
- if (config.addons && config.addons.length > 0) {
3767
- yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
3768
- config.addons = [...new Set(config.addons)];
3769
- }
3770
- yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
3771
- yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? [], config.orm, config.database);
3772
- yield* validateCommunicationCompatibility(config.communication, config.backend);
3773
- yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
3774
- return Result.ok(void 0);
3727
+ return validateWithCompatibilityEvaluator({
3728
+ orm,
3729
+ database
3775
3730
  });
3776
3731
  }
3732
+ function validateFullConfig(config, _providedFlags, options) {
3733
+ return validateWithCompatibilityEvaluator(config, options);
3734
+ }
3777
3735
  function validateConfigForProgrammaticUse(config) {
3778
- return Result.gen(function* () {
3779
- yield* validateDatabaseOrmAuth(config);
3780
- if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
3781
- yield* validateApiFrontendCompatibility(config.api, config.frontend);
3782
- yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
3783
- yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend, config.orm, config.database);
3784
- yield* validateCommunicationCompatibility(config.communication, config.backend);
3785
- yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
3786
- if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
3787
- yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
3788
- return Result.ok(void 0);
3789
- });
3736
+ return validateWithCompatibilityEvaluator(config);
3790
3737
  }
3791
3738
  //#endregion
3792
3739
  //#region src/prompts/orm.ts
@@ -3927,26 +3874,33 @@ const SERVER_APP_BACKENDS = [
3927
3874
  "hono",
3928
3875
  "express",
3929
3876
  "fastify",
3930
- "elysia"
3877
+ "elysia",
3878
+ "nestjs"
3931
3879
  ];
3932
- function getDeploymentDisplay$1(deployment) {
3933
- if (deployment === "cloudflare") return {
3880
+ const DEPLOYMENT_DISPLAYS$1 = {
3881
+ cloudflare: {
3934
3882
  label: "Cloudflare",
3935
3883
  hint: "Deploy to Cloudflare Workers using Alchemy"
3936
- };
3937
- if (deployment === "docker") return {
3884
+ },
3885
+ docker: {
3938
3886
  label: "Docker",
3939
3887
  hint: "Self-host with a Dockerfile and docker-compose.yml"
3940
- };
3941
- if (deployment === "vercel") return {
3888
+ },
3889
+ vercel: {
3942
3890
  label: "Vercel",
3943
3891
  hint: "Deploy to Vercel with Services"
3944
- };
3945
- if (deployment === "guaracloud") return {
3892
+ },
3893
+ railway: {
3894
+ label: "Railway",
3895
+ hint: "Deploy with Railpack and railway.json"
3896
+ },
3897
+ guaracloud: {
3946
3898
  label: "Guara Cloud",
3947
3899
  hint: "Deploy containers on Guara Cloud via GitHub or Docker image"
3948
- };
3949
- return {
3900
+ }
3901
+ };
3902
+ function getDeploymentDisplay$1(deployment) {
3903
+ return DEPLOYMENT_DISPLAYS$1[deployment] ?? {
3950
3904
  label: deployment,
3951
3905
  hint: `Add ${deployment} deployment`
3952
3906
  };
@@ -3959,6 +3913,8 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
3959
3913
  const options = [
3960
3914
  "docker",
3961
3915
  "vercel",
3916
+ "railway",
3917
+ "guaracloud",
3962
3918
  "none"
3963
3919
  ].map((deploy) => {
3964
3920
  const { label, hint } = deploy === "none" ? {
@@ -4007,24 +3963,30 @@ async function getTestingChoice(testing, frontends, previousValue) {
4007
3963
  function hasWebFrontend(frontends) {
4008
3964
  return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
4009
3965
  }
4010
- function getDeploymentDisplay(deployment) {
4011
- if (deployment === "cloudflare") return {
3966
+ const DEPLOYMENT_DISPLAYS = {
3967
+ cloudflare: {
4012
3968
  label: "Cloudflare",
4013
3969
  hint: "Deploy to Cloudflare Workers using Alchemy"
4014
- };
4015
- if (deployment === "docker") return {
3970
+ },
3971
+ docker: {
4016
3972
  label: "Docker",
4017
3973
  hint: "Self-host with a Dockerfile and docker-compose.yml"
4018
- };
4019
- if (deployment === "vercel") return {
3974
+ },
3975
+ vercel: {
4020
3976
  label: "Vercel",
4021
3977
  hint: "Deploy to Vercel with Services"
4022
- };
4023
- if (deployment === "guaracloud") return {
3978
+ },
3979
+ railway: {
3980
+ label: "Railway",
3981
+ hint: "Deploy with Railpack and railway.json"
3982
+ },
3983
+ guaracloud: {
4024
3984
  label: "Guara Cloud",
4025
3985
  hint: "Deploy containers on Guara Cloud via GitHub or Docker image"
4026
- };
4027
- return {
3986
+ }
3987
+ };
3988
+ function getDeploymentDisplay(deployment) {
3989
+ return DEPLOYMENT_DISPLAYS[deployment] ?? {
4028
3990
  label: deployment,
4029
3991
  hint: `Add ${deployment} deployment`
4030
3992
  };
@@ -4037,6 +3999,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
4037
3999
  "cloudflare",
4038
4000
  "docker",
4039
4001
  "vercel",
4002
+ "railway",
4040
4003
  "guaracloud",
4041
4004
  "none"
4042
4005
  ].map((deploy) => {
@@ -4072,7 +4035,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
4072
4035
  auth: flags.auth ?? DEFAULT_CONFIG.auth,
4073
4036
  payments: flags.payments ?? DEFAULT_CONFIG.payments,
4074
4037
  observability: flags.observability ?? DEFAULT_CONFIG.observability,
4075
- communication: flags.communication ?? DEFAULT_CONFIG.communication,
4038
+ communication: normalizeCommunication$2(flags.communication ?? DEFAULT_CONFIG.communication),
4076
4039
  addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
4077
4040
  examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
4078
4041
  testing: flags.testing ?? [...DEFAULT_CONFIG.testing],
@@ -4229,10 +4192,25 @@ function isTelemetryEnabled() {
4229
4192
  async function sendConvexEvent(payload) {}
4230
4193
  async function trackProjectCreation(config, disableAnalytics = false) {
4231
4194
  if (!isTelemetryEnabled() || disableAnalytics) return;
4232
- 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;
4233
4196
  await Result.tryPromise({
4234
4197
  try: () => sendConvexEvent({
4235
- ...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,
4236
4214
  cli_version: getLatestCLIVersion(),
4237
4215
  node_version: typeof process !== "undefined" ? process.version : "",
4238
4216
  platform: typeof process !== "undefined" ? process.platform : ""
@@ -4257,7 +4235,10 @@ function displayConfig(config) {
4257
4235
  if (config.orm !== void 0) configDisplay.push(`${cliColors.signal("ORM:")} ${String(config.orm)}`);
4258
4236
  if (config.auth !== void 0) configDisplay.push(`${cliColors.signal("Auth:")} ${String(config.auth)}`);
4259
4237
  if (config.payments !== void 0) configDisplay.push(`${cliColors.signal("Payments:")} ${config.payments.join(", ") || "none"}`);
4260
- if (config.communication !== void 0) configDisplay.push(`${cliColors.signal("Communication:")} ${String(config.communication)}`);
4238
+ if (config.communication !== void 0) {
4239
+ const communication = normalizeCommunication$2(config.communication);
4240
+ configDisplay.push(`${cliColors.signal("Communication:")} ${communication.join(", ") || "none"}`);
4241
+ }
4261
4242
  if (config.observability !== void 0) configDisplay.push(`${cliColors.signal("Observability:")} ${config.observability.join(", ") || "none"}`);
4262
4243
  if (config.addons !== void 0) {
4263
4244
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
@@ -4417,7 +4398,7 @@ const TEMPLATE_PRESETS = {
4417
4398
  auth: "better-auth",
4418
4399
  payments: [],
4419
4400
  observability: ["getmonitor"],
4420
- communication: "none",
4401
+ communication: [],
4421
4402
  addons: ["turborepo"],
4422
4403
  examples: ["todo"],
4423
4404
  dbSetup: "mongodb-atlas",
@@ -4434,7 +4415,7 @@ const TEMPLATE_PRESETS = {
4434
4415
  auth: "better-auth",
4435
4416
  payments: [],
4436
4417
  observability: ["getmonitor"],
4437
- communication: "none",
4418
+ communication: [],
4438
4419
  addons: ["turborepo"],
4439
4420
  examples: ["todo"],
4440
4421
  dbSetup: "none",
@@ -4451,7 +4432,7 @@ const TEMPLATE_PRESETS = {
4451
4432
  auth: "better-auth",
4452
4433
  payments: [],
4453
4434
  observability: ["getmonitor"],
4454
- communication: "none",
4435
+ communication: [],
4455
4436
  addons: ["biome", "turborepo"],
4456
4437
  examples: ["none"],
4457
4438
  dbSetup: "none",
@@ -4468,7 +4449,7 @@ const TEMPLATE_PRESETS = {
4468
4449
  auth: "none",
4469
4450
  payments: [],
4470
4451
  observability: [],
4471
- communication: "none",
4452
+ communication: [],
4472
4453
  addons: ["none"],
4473
4454
  examples: ["none"],
4474
4455
  dbSetup: "none",
@@ -4564,6 +4545,7 @@ const formatOptions = {
4564
4545
  /** Files that must not be rewritten by post-scaffold formatters. */
4565
4546
  const SKIP_FILE_NAMES = new Set([
4566
4547
  "kubojs.jsonrc",
4548
+ "kubo.config.ts",
4567
4549
  "package-lock.json",
4568
4550
  "pnpm-lock.yaml",
4569
4551
  "yarn.lock",
@@ -6146,7 +6128,8 @@ function getDesktopStaticBuildNote(frontend) {
6146
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.`;
6147
6129
  }
6148
6130
  async function displayPostInstallInstructions(config) {
6149
- 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 ?? []);
6150
6133
  const isConvex = backend === "convex";
6151
6134
  const isBackendSelf = backend === "self";
6152
6135
  const runCmd = packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
@@ -6169,9 +6152,9 @@ async function displayPostInstallInstructions(config) {
6169
6152
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
6170
6153
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
6171
6154
  const getMonitorInstructions = observability.includes("getmonitor") ? getGetMonitorInstructions() : "";
6172
- const resendInstructions = communication === "resend" ? getResendInstructions() : "";
6173
- const notifiqueInstructions = communication === "notifique" ? getNotifiqueInstructions() : "";
6174
- 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) : "";
6175
6158
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
6176
6159
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
6177
6160
  const webPort = (0, types_exports.getWebPort)(frontend ?? []);
@@ -6569,7 +6552,7 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
6569
6552
  auth: "none",
6570
6553
  payments: [],
6571
6554
  observability: [],
6572
- communication: "none",
6555
+ communication: [],
6573
6556
  git: false,
6574
6557
  packageManager: "npm",
6575
6558
  install: false,
@@ -7103,7 +7086,7 @@ async function createVirtual(options) {
7103
7086
  auth: options.auth || "none",
7104
7087
  payments: rawPayments === "none" ? [] : Array.isArray(rawPayments) ? rawPayments : rawPayments ? [rawPayments] : [],
7105
7088
  observability: options.disableObservability ? [] : rawObservability === "none" ? [] : Array.isArray(rawObservability) ? rawObservability : rawObservability ? [rawObservability] : [],
7106
- communication: options.communication || "none",
7089
+ communication: options.communication ? (0, types_exports.normalizeCommunication)(options.communication) : [],
7107
7090
  git: options.git ?? false,
7108
7091
  packageManager: options.packageManager || "bun",
7109
7092
  install: false,
@@ -7134,6 +7117,7 @@ async function createVirtual(options) {
7134
7117
  if (validationResult.isErr()) return Result.err(new GeneratorError({
7135
7118
  message: validationResult.error.message,
7136
7119
  phase: "validation",
7120
+ compatibilityCode: validationResult.error.compatibilityCode,
7137
7121
  cause: validationResult.error
7138
7122
  }));
7139
7123
  return generate({