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