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.
- package/README.md +6 -6
- package/dist/cli.mjs +141 -2
- package/dist/index.d.mts +15 -12
- package/dist/index.mjs +1 -1
- package/dist/{src-CAMyqVYx.mjs → src-9z53Xnti.mjs} +748 -764
- package/package.json +4 -3
|
@@ -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 {
|
|
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,
|
|
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 {
|
|
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: "
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
"
|
|
597
|
-
"
|
|
598
|
-
"
|
|
599
|
-
"
|
|
600
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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.
|
|
829
|
+
return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.some((supported) => supported === frontend));
|
|
722
830
|
}
|
|
723
831
|
function allowedApisForFrontends(frontends = [], backend) {
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
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
|
-
]
|
|
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
|
|
842
|
+
return api !== "orval" || backendAllowsApi(backend, "orval");
|
|
743
843
|
}
|
|
744
844
|
function isExampleTodoAllowed(backend, database, api) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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:
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
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/
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
return
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
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
|
-
|
|
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
|
|
2880
|
-
if (
|
|
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 ??
|
|
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
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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:
|
|
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: ${
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
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
|
-
*
|
|
3594
|
-
*
|
|
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
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
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
|
|
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
|
-
|
|
3933
|
-
|
|
3880
|
+
const DEPLOYMENT_DISPLAYS$1 = {
|
|
3881
|
+
cloudflare: {
|
|
3934
3882
|
label: "Cloudflare",
|
|
3935
3883
|
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
3936
|
-
}
|
|
3937
|
-
|
|
3884
|
+
},
|
|
3885
|
+
docker: {
|
|
3938
3886
|
label: "Docker",
|
|
3939
3887
|
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
3940
|
-
}
|
|
3941
|
-
|
|
3888
|
+
},
|
|
3889
|
+
vercel: {
|
|
3942
3890
|
label: "Vercel",
|
|
3943
3891
|
hint: "Deploy to Vercel with Services"
|
|
3944
|
-
}
|
|
3945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4011
|
-
|
|
3966
|
+
const DEPLOYMENT_DISPLAYS = {
|
|
3967
|
+
cloudflare: {
|
|
4012
3968
|
label: "Cloudflare",
|
|
4013
3969
|
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
4014
|
-
}
|
|
4015
|
-
|
|
3970
|
+
},
|
|
3971
|
+
docker: {
|
|
4016
3972
|
label: "Docker",
|
|
4017
3973
|
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
4018
|
-
}
|
|
4019
|
-
|
|
3974
|
+
},
|
|
3975
|
+
vercel: {
|
|
4020
3976
|
label: "Vercel",
|
|
4021
3977
|
hint: "Deploy to Vercel with Services"
|
|
4022
|
-
}
|
|
4023
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
6173
|
-
const notifiqueInstructions = communication
|
|
6174
|
-
const araraInstructions = communication
|
|
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:
|
|
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
|
|
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({
|