create-better-t-stack 3.38.2 → 3.39.2

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 CHANGED
@@ -27,12 +27,23 @@ npx create-better-t-stack@latest
27
27
 
28
28
  Follow the prompts to configure your project or use the `--yes` flag for defaults.
29
29
 
30
+ ## Requirements
31
+
32
+ - Node.js 22 or newer to run the CLI with Node.js. The selected framework may require a newer release; the CLI checks the exact stack before writing files.
33
+ - Bun 1.3 or newer is recommended. Bun 1.2.14 remains the minimum because generated workspaces use dependency catalogs.
34
+ - pnpm 10.26.0 or newer when using pnpm (catalogs and `allowBuilds`).
35
+ - npm 11.16.0 or newer when using npm (`allowScripts`).
36
+
37
+ Docker Compose 2.24 or newer is required by generated Docker stacks. Git, Rust, Docker,
38
+ and platform SDKs otherwise remain optional and are only needed when you use their corresponding
39
+ generated commands.
40
+
30
41
  ## Features
31
42
 
32
43
  | Category | Options |
33
44
  | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
45
  | **TypeScript** | End-to-end type safety across all parts of your application |
35
- | **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
46
+ | **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidStart v2 (SSR)<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
36
47
  | **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
37
48
  | **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
38
49
  | **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
@@ -245,13 +256,13 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
245
256
  - **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
246
257
  - **Runtime 'none'**: Only available with Convex backend, backend `none`, or backend `self`.
247
258
  - **Cloudflare Workers runtime**: Only compatible with Hono backend. If a database is used, MongoDB is not supported.
248
- - **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, and `astro`.
259
+ - **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, `solid`, and `astro`.
249
260
  - **Addons 'none'**: Skips all addons.
250
261
  - **Examples 'none'**: Skips all example implementations (todo, AI chat).
251
- - **Nuxt, Svelte, SolidJS, and Astro** frontends are only compatible with oRPC API layer
252
- - **PWA support** requires TanStack Router, React Router, Next.js, or SolidJS
253
- - **Tauri desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, SolidJS, or Astro
254
- - **Electrobun desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, SolidJS, or Astro. Desktop packaging uses static web assets, so SSR-first frontends need a static/export build before desktop builds will work.
262
+ - **Nuxt, Svelte, SolidStart, and Astro** frontends are only compatible with the oRPC API layer
263
+ - **PWA support** requires TanStack Router, React Router, Next.js, or SolidStart
264
+ - **Tauri desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, or Astro
265
+ - **Electrobun desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, or Astro. Desktop packaging uses static web assets, so SSR-first frontends need a static/export build before desktop builds will work.
255
266
  - **AI example** is not compatible with Solid or Astro. With Convex backend, it also excludes Nuxt and Svelte.
256
267
 
257
268
  ## Project Structure
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-ihjK9T9S.mjs";
2
+ import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-BYgmfow5.mjs";
3
3
  import z from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -44,8 +44,8 @@ function formatToolSuccess(data) {
44
44
  }
45
45
  };
46
46
  }
47
- function formatToolError(error) {
48
- const message = error instanceof Error ? error.message : String(error);
47
+ function formatToolError(cause) {
48
+ const message = cause instanceof Error ? cause.message : String(cause);
49
49
  return {
50
50
  content: [{
51
51
  type: "text",
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Result, Result as Result$1 } from "better-result";
3
- import { TrpcCliMeta, createCli } from "trpc-cli";
3
+ import { TrpcCli, TrpcCliMeta } from "trpc-cli";
4
4
  import z from "zod";
5
5
  import { EMBEDDED_TEMPLATES, GeneratorError, GeneratorError as GeneratorError$1, GeneratorOptions, TEMPLATE_COUNT, VirtualDirectory, VirtualFile, VirtualFileSystem, VirtualFileTree, VirtualFileTree as VirtualFileTree$1, VirtualNode, generate } from "@better-t-stack/template-generator";
6
6
  import * as import__better_t_stack_types from "@better-t-stack/types";
@@ -31,12 +31,12 @@ declare const ValidationError_base: import("better-result").TaggedErrorClass<"Va
31
31
  */
32
32
  declare class ValidationError extends ValidationError_base<{
33
33
  field?: string;
34
- value?: unknown;
34
+ value?: string | number | boolean | null;
35
35
  message: string;
36
36
  }> {
37
37
  constructor(args: {
38
38
  field?: string;
39
- value?: unknown;
39
+ value?: string | number | boolean | null;
40
40
  message: string;
41
41
  });
42
42
  }
@@ -179,8 +179,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
179
179
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
180
180
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
181
181
  api?: "none" | "trpc" | "orpc" | undefined;
182
- webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
183
- serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
182
+ webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
183
+ serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
184
184
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
185
185
  renderTitle?: boolean | undefined;
186
186
  disableAnalytics?: boolean | undefined;
@@ -278,8 +278,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
278
278
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
279
279
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
280
280
  api?: "none" | "trpc" | "orpc" | undefined;
281
- webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
282
- serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
281
+ webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
282
+ serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
283
283
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
284
284
  renderTitle?: boolean | undefined;
285
285
  disableAnalytics?: boolean | undefined;
@@ -359,8 +359,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
359
359
  hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
360
360
  } | undefined;
361
361
  } | undefined;
362
- webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
363
- serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
362
+ webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
363
+ serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
364
364
  projectDir?: string | undefined;
365
365
  install?: boolean | undefined;
366
366
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
@@ -379,7 +379,7 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
379
379
  meta: TrpcCliMeta;
380
380
  }>;
381
381
  }>>;
382
- declare function createBtsCli(): ReturnType<typeof createCli>;
382
+ declare function createBtsCli(): TrpcCli;
383
383
  /**
384
384
  * Error types that can be returned from create/createVirtual
385
385
  */
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-ihjK9T9S.mjs";
2
+ import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-BYgmfow5.mjs";
3
3
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
@@ -11,7 +11,7 @@ import path from "node:path";
11
11
  import envPaths from "env-paths";
12
12
  import fs from "fs-extra";
13
13
  import { fileURLToPath } from "node:url";
14
- import { desktopWebFrontends as desktopWebFrontends$3 } from "@better-t-stack/types";
14
+ import { desktopWebFrontends as desktopWebFrontends$2, usesAlchemyManagedDatabase } from "@better-t-stack/types";
15
15
  import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig } from "@better-t-stack/template-generator";
16
16
  import { consola, createConsola } from "consola";
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -21,6 +21,7 @@ import { writeTree } from "@better-t-stack/template-generator/fs-writer";
21
21
  import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
22
22
  import { applyEdits, modify, parse } from "jsonc-parser";
23
23
  import os from "node:os";
24
+ import { clean, coerce, satisfies } from "semver";
24
25
  import { format } from "oxfmt";
25
26
  //#region src/utils/display-config.ts
26
27
  const VALUE_LABELS = {
@@ -90,11 +91,16 @@ const VALUE_LABELS = {
90
91
  npm: "npm",
91
92
  pnpm: "pnpm"
92
93
  };
94
+ function isKnownValueLabel(value) {
95
+ return Object.hasOwn(VALUE_LABELS, value);
96
+ }
93
97
  function formatConfigValue(value) {
94
- if (typeof value === "boolean") return value ? "Yes" : "No";
98
+ if (value === true) return "Yes";
99
+ if (value === false) return "No";
95
100
  if (Array.isArray(value)) return value.length > 0 ? value.map(formatConfigValue).join(", ") : "None";
96
101
  const text = String(value);
97
- return VALUE_LABELS[text] ?? text.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
102
+ if (isKnownValueLabel(text)) return VALUE_LABELS[text];
103
+ return text.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
98
104
  }
99
105
  function section(title, entries) {
100
106
  const rows = entries.filter(([, value]) => value !== void 0).map(([label, value, format]) => ({
@@ -202,8 +208,8 @@ const ADDON_COMPATIBILITY = {
202
208
  "solid",
203
209
  "next"
204
210
  ],
205
- tauri: desktopWebFrontends$3,
206
- electrobun: desktopWebFrontends$3,
211
+ tauri: desktopWebFrontends$2,
212
+ electrobun: desktopWebFrontends$2,
207
213
  biome: [],
208
214
  husky: [],
209
215
  lefthook: [],
@@ -820,19 +826,19 @@ async function fetchSponsorsData({ url = SPONSORS_JSON_URL, withSpinner = false,
820
826
  if (timeout) clearTimeout(timeout);
821
827
  }
822
828
  }
823
- function normalizeSponsorFetchError(error) {
824
- if (error instanceof Error && error.name === "AbortError") return new CLIError({
829
+ function normalizeSponsorFetchError(cause) {
830
+ if (cause instanceof Error && cause.name === "AbortError") return new CLIError({
825
831
  message: "Failed to fetch sponsors: request timed out",
826
- cause: error
832
+ cause
827
833
  });
828
- if (CLIError.is(error)) return error;
829
- if (error instanceof Error) return new CLIError({
830
- message: error.message.startsWith("Failed to fetch sponsors:") ? error.message : `Failed to fetch sponsors: ${error.message}`,
831
- cause: error
834
+ if (CLIError.is(cause)) return cause;
835
+ if (cause instanceof Error) return new CLIError({
836
+ message: cause.message.startsWith("Failed to fetch sponsors:") ? cause.message : `Failed to fetch sponsors: ${cause.message}`,
837
+ cause
832
838
  });
833
839
  return new CLIError({
834
- message: `Failed to fetch sponsors: ${String(error)}`,
835
- cause: error
840
+ message: `Failed to fetch sponsors: ${String(cause)}`,
841
+ cause
836
842
  });
837
843
  }
838
844
  //#endregion
@@ -927,6 +933,7 @@ const FULLSTACK_FRONTENDS$1 = [
927
933
  "tanstack-start",
928
934
  "nuxt",
929
935
  "svelte",
936
+ "solid",
930
937
  "astro"
931
938
  ];
932
939
  const EVLOG_SERVER_BACKENDS = [
@@ -935,7 +942,13 @@ const EVLOG_SERVER_BACKENDS = [
935
942
  "fastify",
936
943
  "elysia"
937
944
  ];
938
- const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
945
+ const EVLOG_FULLSTACK_FRONTENDS = [
946
+ "next",
947
+ "tanstack-start",
948
+ "nuxt",
949
+ "svelte",
950
+ "astro"
951
+ ];
939
952
  const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
940
953
  function supportsEvlogAddon(frontend = [], backend, _runtime) {
941
954
  if (!backend) return true;
@@ -951,11 +964,11 @@ function validateSelfBackendCompatibility(providedFlags, options, config) {
951
964
  const frontends = config.frontend || options.frontend || [];
952
965
  if (backend === "self") {
953
966
  const { web, native } = splitFrontends(frontends);
954
- 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.");
967
+ 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, SolidStart, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend solid, or --frontend astro.");
955
968
  if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
956
969
  }
957
970
  const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
958
- 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.");
971
+ if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, SolidStart, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend solid, --frontend astro, or choose a different backend.");
959
972
  return Result.ok(void 0);
960
973
  }
961
974
  function validateWorkersCompatibility(providedFlags, options, config) {
@@ -1041,7 +1054,35 @@ function validateVercelServerDeploy(serverDeploy, backend, runtime) {
1041
1054
  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'.");
1042
1055
  return Result.ok(void 0);
1043
1056
  }
1044
- const DOCKER_SERVER_OUTPUT_FRONTENDS = [
1057
+ function validatePrismaServerDeploy(serverDeploy, backend, runtime) {
1058
+ if (serverDeploy !== "prisma") return Result.ok(void 0);
1059
+ if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy prisma' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy prisma' instead.");
1060
+ if (runtime !== "bun" && runtime !== "node") return validationErr$1("'--server-deploy prisma' requires '--runtime bun' or '--runtime node'. Use '--server-deploy cloudflare' for Workers.");
1061
+ return Result.ok(void 0);
1062
+ }
1063
+ const PRISMA_COMPUTE_WEB_FRONTENDS = [
1064
+ "next",
1065
+ "nuxt",
1066
+ "astro",
1067
+ "react-router",
1068
+ "tanstack-start",
1069
+ "svelte",
1070
+ "solid"
1071
+ ];
1072
+ function supportsPrismaWebDeploy(frontend) {
1073
+ return frontend.some((value) => PRISMA_COMPUTE_WEB_FRONTENDS.includes(value));
1074
+ }
1075
+ function validatePrismaWebDeploy(webDeploy, frontend) {
1076
+ if (webDeploy !== "prisma" || !frontend) return Result.ok(void 0);
1077
+ if (!supportsPrismaWebDeploy(frontend)) return validationErr$1("'--web-deploy prisma' requires a supported server frontend. Choose Next.js, Nuxt, Astro, React Router, TanStack Start, SvelteKit, or SolidStart. TanStack Router is a static SPA, while Prisma Compute requires an executable server artifact.");
1078
+ return Result.ok(void 0);
1079
+ }
1080
+ function validateCloudflareWebDeployKnownIssues(config) {
1081
+ if (config.webDeploy !== "cloudflare" || !config.frontend?.includes("next")) return Result.ok(void 0);
1082
+ if (config.database === "postgres" && config.orm === "prisma" && config.dbSetup !== "neon" && config.dbSetup !== "prisma-postgres") return validationErr$1("This Prisma PostgreSQL setup with Next.js on Cloudflare is temporarily unavailable because OpenNext does not preserve pg-cloudflare's workerd files. Use Neon or Prisma Postgres, choose another Cloudflare frontend, or choose Prisma, Docker, or Vercel deployment.");
1083
+ return Result.ok(void 0);
1084
+ }
1085
+ const DESKTOP_STATIC_EXPORT_FRONTENDS = [
1045
1086
  "next",
1046
1087
  "svelte",
1047
1088
  "astro",
@@ -1051,10 +1092,18 @@ function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backe
1051
1092
  if (webDeploy !== "docker" || !addons || !frontend) return Result.ok(void 0);
1052
1093
  const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
1053
1094
  if (desktopAddons.length === 0) return Result.ok(void 0);
1054
- const affected = frontend.find((f) => DOCKER_SERVER_OUTPUT_FRONTENDS.includes(f));
1095
+ const affected = frontend.find((f) => DESKTOP_STATIC_EXPORT_FRONTENDS.includes(f));
1055
1096
  if (!affected) return Result.ok(void 0);
1056
1097
  if (affected === "next" && !desktopAddons.includes("tauri") && backend === "convex" && auth === "better-auth") return Result.ok(void 0);
1057
- 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).`);
1098
+ 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 the static-serving tanstack-router frontend.`);
1099
+ }
1100
+ function validatePrismaWebDeployDesktopAddons(webDeploy, addons, frontend) {
1101
+ if (webDeploy !== "prisma" || !addons || !frontend) return Result.ok(void 0);
1102
+ const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
1103
+ if (desktopAddons.length === 0) return Result.ok(void 0);
1104
+ const affected = frontend.find((value) => DESKTOP_STATIC_EXPORT_FRONTENDS.includes(value));
1105
+ if (!affected) return Result.ok(void 0);
1106
+ return validationErr$1(`'--web-deploy prisma' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons replace its executable server output with a static export, while Prisma Compute requires an executable server artifact. Remove the addon or choose a server deployment that supports this desktop build.`);
1058
1107
  }
1059
1108
  function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
1060
1109
  if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
@@ -1101,7 +1150,13 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backe
1101
1150
  return Result.ok(void 0);
1102
1151
  }
1103
1152
  function validateAddonsAgainstConfig(addons = [], config) {
1104
- return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
1153
+ const addonResult = validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
1154
+ if (addonResult.isErr()) return addonResult;
1155
+ const cloudflareResult = validateCloudflareWebDeployKnownIssues(config);
1156
+ if (cloudflareResult.isErr()) return cloudflareResult;
1157
+ const dockerResult = validateDockerWebDeployDesktopAddons(config.webDeploy, addons, config.frontend, config.backend, config.auth);
1158
+ if (dockerResult.isErr()) return dockerResult;
1159
+ return validatePrismaWebDeployDesktopAddons(config.webDeploy, addons, config.frontend);
1105
1160
  }
1106
1161
  function validatePaymentsCompatibility(payments, auth, _backend, _frontends = []) {
1107
1162
  if (!payments || payments === "none") return Result.ok(void 0);
@@ -1344,7 +1399,7 @@ async function navigableGroupMultiselect(opts) {
1344
1399
  const required = opts.required ?? true;
1345
1400
  const opt = (option, state, options = []) => {
1346
1401
  const label = option.label ?? String(option.value);
1347
- const isItem = typeof option.group === "string";
1402
+ const isItem = option.group !== true && option.group !== false;
1348
1403
  const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
1349
1404
  const isLast = isItem && next && next.group === true;
1350
1405
  const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
@@ -1378,7 +1433,7 @@ async function navigableGroupMultiselect(opts) {
1378
1433
  const value = this.value ?? [];
1379
1434
  const styleOption = (option, active) => {
1380
1435
  const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1381
- if (!active && typeof option.group === "string" && this.options[this.cursor]?.value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", this.options);
1436
+ if (!active && option.group !== true && option.group !== false && this.options[this.cursor]?.value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", this.options);
1382
1437
  if (active && selected) return opt(option, "active-selected", this.options);
1383
1438
  if (selected) return opt(option, "selected", this.options);
1384
1439
  return opt(option, active ? "active" : "inactive", this.options);
@@ -1665,6 +1720,157 @@ function validateAgentSafePathInput(value, field) {
1665
1720
  return Result.ok(void 0);
1666
1721
  }
1667
1722
  //#endregion
1723
+ //#region src/utils/external-commands.ts
1724
+ function shouldSkipExternalCommands() {
1725
+ return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
1726
+ }
1727
+ //#endregion
1728
+ //#region src/utils/requirements.ts
1729
+ const PACKAGE_MANAGER_VERSION_RANGES = {
1730
+ bun: ">=1.2.14",
1731
+ npm: ">=11.16.0",
1732
+ pnpm: ">=10.26.0"
1733
+ };
1734
+ const PACKAGE_MANAGER_REASONS = {
1735
+ bun: "generated Bun workspaces use dependency catalogs",
1736
+ npm: "generated npm workspaces use the allowScripts install-script policy",
1737
+ pnpm: "generated pnpm workspaces use catalogs and the allowBuilds policy"
1738
+ };
1739
+ const PACKAGE_MANAGER_UPGRADE_INSTRUCTIONS = {
1740
+ bun: "Run `bun upgrade`.",
1741
+ npm: "After updating Node.js, run `npm install --global npm@latest`.",
1742
+ pnpm: "Run `pnpm self-update`, or install the latest pnpm globally."
1743
+ };
1744
+ const NODE_UPGRADE_INSTRUCTION = "Install the latest Node.js 24 LTS release from https://nodejs.org, then rerun the command.";
1745
+ function normalizeVersion(value) {
1746
+ const trimmed = value.trim();
1747
+ return clean(trimmed) ?? coerce(trimmed)?.version ?? null;
1748
+ }
1749
+ function addNodeRequirement(requirements, range, reason) {
1750
+ requirements.push({
1751
+ tool: "node",
1752
+ range,
1753
+ reason
1754
+ });
1755
+ }
1756
+ function getNodeToolingRequirements(config) {
1757
+ const requirements = [];
1758
+ for (const frontend of config.frontend) switch (frontend) {
1759
+ case "astro":
1760
+ addNodeRequirement(requirements, ">=22.12.0", "Astro 7");
1761
+ break;
1762
+ case "nuxt":
1763
+ addNodeRequirement(requirements, "^22.19.0 || ^24.11.0 || >=26.0.0", "Nuxt 4");
1764
+ break;
1765
+ case "solid":
1766
+ addNodeRequirement(requirements, ">=24.0.0", "SolidStart 2");
1767
+ break;
1768
+ case "react-router":
1769
+ addNodeRequirement(requirements, ">=22.22.0", "React Router 8");
1770
+ break;
1771
+ case "svelte":
1772
+ case "tanstack-router":
1773
+ case "tanstack-start":
1774
+ addNodeRequirement(requirements, "^20.19.0 || >=22.12.0", "Vite 8");
1775
+ break;
1776
+ case "next":
1777
+ addNodeRequirement(requirements, ">=20.9.0", "Next.js 16");
1778
+ break;
1779
+ case "native-bare":
1780
+ case "native-uniwind":
1781
+ case "native-unistyles":
1782
+ addNodeRequirement(requirements, "^22.13.0 || ^24.3.0 || >=26.0.0", "React Native 0.86");
1783
+ break;
1784
+ }
1785
+ if (![
1786
+ "none",
1787
+ "self",
1788
+ "convex"
1789
+ ].includes(config.backend)) addNodeRequirement(requirements, "^22.18.0 || >=24.11.0", "the generated server build (tsdown)");
1790
+ if (config.orm === "prisma") addNodeRequirement(requirements, "^20.19.0 || ^22.12.0 || >=24.0.0", "Prisma 7");
1791
+ if (config.orm === "mongoose") addNodeRequirement(requirements, ">=20.19.0", "Mongoose 9 and MongoDB 7");
1792
+ if (config.examples.includes("ai")) addNodeRequirement(requirements, ">=22.0.0", "AI SDK 7");
1793
+ if (config.addons.includes("oxlint") || config.addons.includes("ultracite") && config.addonOptions?.ultracite?.linter === "oxlint") addNodeRequirement(requirements, "^20.19.0 || >=22.12.0", "Oxlint and Oxfmt");
1794
+ if (config.addons.includes("husky")) addNodeRequirement(requirements, ">=22.22.1", "lint-staged 17");
1795
+ if (config.addons.includes("vite-plus")) addNodeRequirement(requirements, "^20.19.0 || ^22.18.0 || >=24.11.0", "Vite+");
1796
+ if (config.addons.includes("starlight")) addNodeRequirement(requirements, ">=22.12.0", "Starlight's Astro toolchain");
1797
+ if (config.addons.includes("wxt")) addNodeRequirement(requirements, ">=22.0.0", "WXT");
1798
+ if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") addNodeRequirement(requirements, ">=22.0.0", "Wrangler 4");
1799
+ return requirements;
1800
+ }
1801
+ function getLocalVersionRequirements(config, hostRuntime) {
1802
+ const requirements = [{
1803
+ tool: config.packageManager,
1804
+ range: PACKAGE_MANAGER_VERSION_RANGES[config.packageManager],
1805
+ reason: PACKAGE_MANAGER_REASONS[config.packageManager]
1806
+ }];
1807
+ if (hostRuntime === "node") addNodeRequirement(requirements, ">=22.0.0", "create-better-t-stack");
1808
+ if (config.packageManager !== "bun") requirements.push(...getNodeToolingRequirements(config));
1809
+ else if (config.runtime === "node") addNodeRequirement(requirements, ">=22.0.0", "the selected Node.js server runtime");
1810
+ return requirements;
1811
+ }
1812
+ function formatRequirementError(requirement, rawVersion) {
1813
+ const label = requirement.tool === "node" ? "Node.js" : requirement.tool;
1814
+ if (!rawVersion) return `${label} is not available (required ${requirement.range} for ${requirement.reason}).`;
1815
+ const version = normalizeVersion(rawVersion);
1816
+ if (!version) return `Could not read the ${label} version from "${rawVersion}".`;
1817
+ if (!satisfies(version, requirement.range, { includePrerelease: true })) return `${label} ${version} does not satisfy ${requirement.range} required by ${requirement.reason}.`;
1818
+ return null;
1819
+ }
1820
+ function validateLocalToolVersions(config, versions, hostRuntime) {
1821
+ const requirements = getLocalVersionRequirements(config, hostRuntime);
1822
+ const failures = requirements.map((requirement) => formatRequirementError(requirement, versions[requirement.tool])).filter((failure) => failure !== null);
1823
+ if (failures.length === 0) return Result.ok(void 0);
1824
+ const upgradeInstructions = /* @__PURE__ */ new Set();
1825
+ for (const requirement of requirements) {
1826
+ if (!formatRequirementError(requirement, versions[requirement.tool])) continue;
1827
+ if (requirement.tool === "node") upgradeInstructions.add(NODE_UPGRADE_INSTRUCTION);
1828
+ else upgradeInstructions.add(PACKAGE_MANAGER_UPGRADE_INSTRUCTIONS[requirement.tool]);
1829
+ }
1830
+ return Result.err(new CLIError({ message: [
1831
+ "Your local toolchain does not meet this stack's requirements:",
1832
+ ...failures.map((failure) => `- ${failure}`),
1833
+ "",
1834
+ ...upgradeInstructions
1835
+ ].join("\n") }));
1836
+ }
1837
+ function getLocalToolRecommendations(config, versions) {
1838
+ if (config.packageManager !== "bun") return [];
1839
+ const rawVersion = versions.bun;
1840
+ if (!rawVersion) return [];
1841
+ const version = normalizeVersion(rawVersion);
1842
+ if (!version || !satisfies(version, PACKAGE_MANAGER_VERSION_RANGES.bun, { includePrerelease: true }) || satisfies(version, ">=1.3.0", { includePrerelease: true })) return [];
1843
+ return [`Bun ${version} meets the minimum requirement, but Bun 1.3 or newer is recommended. Run \`bun upgrade\`.`];
1844
+ }
1845
+ async function readToolVersion(tool) {
1846
+ const result = await Result.tryPromise({
1847
+ try: async () => {
1848
+ const { stdout } = await execa(tool, ["--version"], {
1849
+ cwd: os.tmpdir(),
1850
+ stderr: "pipe"
1851
+ });
1852
+ return normalizeVersion(stdout);
1853
+ },
1854
+ catch: () => null
1855
+ });
1856
+ return result.isOk() ? result.value : null;
1857
+ }
1858
+ async function checkLocalRequirements(config) {
1859
+ const hostRuntime = process.versions.bun ? "bun" : "node";
1860
+ const versions = {};
1861
+ const packageManagerVersion = await readToolVersion(config.packageManager);
1862
+ if (packageManagerVersion) versions[config.packageManager] = packageManagerVersion;
1863
+ if (getLocalVersionRequirements(config, hostRuntime).some((requirement) => requirement.tool === "node")) versions.node = hostRuntime === "node" ? process.versions.node : await readToolVersion("node") ?? void 0;
1864
+ if (!shouldSkipExternalCommands()) {
1865
+ const validationResult = validateLocalToolVersions(config, versions, hostRuntime);
1866
+ if (validationResult.isErr()) return Result.err(validationResult.error);
1867
+ }
1868
+ return Result.ok({
1869
+ packageManagerVersion: packageManagerVersion ?? "latest",
1870
+ warnings: getLocalToolRecommendations(config, versions)
1871
+ });
1872
+ }
1873
+ //#endregion
1668
1874
  //#region src/utils/add-package-deps.ts
1669
1875
  const addPackageDependency = async (opts) => {
1670
1876
  const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
@@ -1873,7 +2079,7 @@ function addNuxtEvlogSetup(content, serviceName) {
1873
2079
  function addSvelteViteEvlogSetup(content, serviceName) {
1874
2080
  let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
1875
2081
  if (nextContent.includes("evlog({")) return nextContent;
1876
- return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
2082
+ return nextContent.replace("sveltekit(),", `sveltekit(),\n evlog({ service: "${serviceName}" }),`);
1877
2083
  }
1878
2084
  function getSvelteEvlogHooksCall(fsDrain) {
1879
2085
  return fsDrain ? `createEvlogHooks({ drain: ${SVELTE_DEV_FS_DRAIN_EXPRESSION} })` : "createEvlogHooks()";
@@ -2104,18 +2310,23 @@ export default defineNitroPlugin((nitroApp) => {
2104
2310
  `;
2105
2311
  }
2106
2312
  function getNuxtEvlogAuthMiddlewareFile(config) {
2107
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
2313
+ if (usesCreateAuthFactory(config)) {
2314
+ const usesCloudflareRequestEnv = config.backend === "self" && config.webDeploy === "cloudflare";
2315
+ const authExpression = usesCloudflareRequestEnv ? "createAuth((event.context.cloudflare as { env: CloudflareEnv }).env)" : getAuthExpression(config);
2316
+ return `${getAuthImportLine(config)}
2317
+ ${usesCloudflareRequestEnv ? `import type { CloudflareEnv } from "@${config.projectName}/env/server";\n` : ""}
2108
2318
  import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
2109
2319
 
2110
2320
  export default defineEventHandler(async (event) => {
2111
2321
  if (!event.context.log) return;
2112
- const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
2322
+ const identify = createAuthMiddleware(${authExpression} as BetterAuthInstance, {
2113
2323
  exclude: ["/api/auth/**"],
2114
2324
  maskEmail: true,
2115
2325
  });
2116
2326
  await identify(event.context.log, event.headers, event.path);
2117
2327
  });
2118
2328
  `;
2329
+ }
2119
2330
  return `${getAuthImportLine(config)}
2120
2331
  import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
2121
2332
 
@@ -2360,7 +2571,7 @@ async function navigableGroup(prompts, opts) {
2360
2571
  continue;
2361
2572
  }
2362
2573
  if (isCancel$1(result)) {
2363
- if (typeof opts?.onCancel === "function") {
2574
+ if (opts?.onCancel) {
2364
2575
  results[name] = "canceled";
2365
2576
  opts.onCancel({ results });
2366
2577
  }
@@ -2383,11 +2594,6 @@ async function navigableGroup(prompts, opts) {
2383
2594
  return results;
2384
2595
  }
2385
2596
  //#endregion
2386
- //#region src/utils/external-commands.ts
2387
- function shouldSkipExternalCommands() {
2388
- return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
2389
- }
2390
- //#endregion
2391
2597
  //#region src/utils/package-runner.ts
2392
2598
  function splitCommandArgs(commandWithArgs) {
2393
2599
  const args = [];
@@ -4007,21 +4213,21 @@ const DEFAULT_EDITORS = ["vscode"];
4007
4213
  const DEFAULT_AGENTS = ["universal"];
4008
4214
  const DEFAULT_HOOKS = [];
4009
4215
  function getFrameworksFromFrontend(frontend) {
4010
- const frameworkMap = {
4011
- "tanstack-router": ["react", "tanstack"],
4012
- "react-router": ["react", "remix"],
4013
- "tanstack-start": ["react", "tanstack"],
4014
- next: ["react", "next"],
4015
- nuxt: ["vue"],
4016
- "native-bare": ["react"],
4017
- "native-uniwind": ["react"],
4018
- "native-unistyles": ["react"],
4019
- svelte: ["svelte"],
4020
- solid: ["solid"],
4021
- astro: ["astro"]
4022
- };
4216
+ const frameworkMap = /* @__PURE__ */ new Map([
4217
+ ["tanstack-router", ["react", "tanstack"]],
4218
+ ["react-router", ["react", "remix"]],
4219
+ ["tanstack-start", ["react", "tanstack"]],
4220
+ ["next", ["react", "next"]],
4221
+ ["nuxt", ["vue"]],
4222
+ ["native-bare", ["react"]],
4223
+ ["native-uniwind", ["react"]],
4224
+ ["native-unistyles", ["react"]],
4225
+ ["svelte", ["svelte"]],
4226
+ ["solid", ["solid"]],
4227
+ ["astro", ["astro"]]
4228
+ ]);
4023
4229
  const frameworks = /* @__PURE__ */ new Set();
4024
- for (const f of frontend) for (const framework of frameworkMap[f] ?? []) frameworks.add(framework);
4230
+ for (const f of frontend) for (const framework of frameworkMap.get(f) ?? []) frameworks.add(framework);
4025
4231
  return Array.from(frameworks);
4026
4232
  }
4027
4233
  function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
@@ -4445,16 +4651,23 @@ const TASK_RUNNER_ADDONS = [
4445
4651
  ];
4446
4652
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
4447
4653
  if (!existingAddonOptions && !nextAddonOptions) return;
4448
- const mergedAddonOptions = { ...existingAddonOptions };
4449
- if (nextAddonOptions) for (const addonKey of Object.keys(nextAddonOptions)) {
4450
- const existingOptionsForAddon = existingAddonOptions?.[addonKey];
4451
- const nextOptionsForAddon = nextAddonOptions[addonKey];
4452
- mergedAddonOptions[addonKey] = existingOptionsForAddon && nextOptionsForAddon ? {
4453
- ...existingOptionsForAddon,
4454
- ...nextOptionsForAddon
4455
- } : nextOptionsForAddon;
4456
- }
4457
- return Object.keys(mergedAddonOptions).length > 0 ? mergedAddonOptions : void 0;
4654
+ const mergeOption = (existing, next) => {
4655
+ if (!existing) return next;
4656
+ if (!next) return existing;
4657
+ return {
4658
+ ...existing,
4659
+ ...next
4660
+ };
4661
+ };
4662
+ const mergedAddonOptions = {
4663
+ wxt: mergeOption(existingAddonOptions?.wxt, nextAddonOptions?.wxt),
4664
+ fumadocs: mergeOption(existingAddonOptions?.fumadocs, nextAddonOptions?.fumadocs),
4665
+ opentui: mergeOption(existingAddonOptions?.opentui, nextAddonOptions?.opentui),
4666
+ mcp: mergeOption(existingAddonOptions?.mcp, nextAddonOptions?.mcp),
4667
+ skills: mergeOption(existingAddonOptions?.skills, nextAddonOptions?.skills),
4668
+ ultracite: mergeOption(existingAddonOptions?.ultracite, nextAddonOptions?.ultracite)
4669
+ };
4670
+ return Object.values(mergedAddonOptions).some(Boolean) ? mergedAddonOptions : void 0;
4458
4671
  }
4459
4672
  function getSetupAddons(addonsToAdd, updatedAddons) {
4460
4673
  const setupAddons = new Set(addonsToAdd);
@@ -4536,11 +4749,11 @@ async function addHandlerInternal(input) {
4536
4749
  else {
4537
4750
  const promptResult = await Result.tryPromise({
4538
4751
  try: () => getAddonsToAdd(existingConfig),
4539
- catch: (e) => {
4540
- if (UserCancelledError.is(e)) return e;
4752
+ catch: (cause) => {
4753
+ if (UserCancelledError.is(cause)) return cause;
4541
4754
  return new CLIError({
4542
- message: e instanceof Error ? e.message : String(e),
4543
- cause: e
4755
+ message: cause instanceof Error ? cause.message : String(cause),
4756
+ cause
4544
4757
  });
4545
4758
  }
4546
4759
  });
@@ -4587,6 +4800,9 @@ async function addHandlerInternal(input) {
4587
4800
  ...config,
4588
4801
  addons: updatedAddons
4589
4802
  };
4803
+ const requirementsResult = await checkLocalRequirements(updatedConfig);
4804
+ if (requirementsResult.isErr()) return Result.err(requirementsResult.error);
4805
+ if (!isSilent()) for (const warning of requirementsResult.value.warnings) log.warn(pc.yellow(warning));
4590
4806
  if (!isSilent()) log.info(pc.dim("Preparing addon files…"));
4591
4807
  const vfs = new VirtualFileSystem();
4592
4808
  for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
@@ -4639,11 +4855,11 @@ async function addHandlerInternal(input) {
4639
4855
  ...config,
4640
4856
  addons: getSetupAddons(addonsToAdd, updatedAddons)
4641
4857
  }),
4642
- catch: (e) => {
4643
- if (UserCancelledError.is(e)) return e;
4858
+ catch: (cause) => {
4859
+ if (UserCancelledError.is(cause)) return cause;
4644
4860
  return new CLIError({
4645
- message: e instanceof Error ? e.message : String(e),
4646
- cause: e
4861
+ message: cause instanceof Error ? cause.message : String(cause),
4862
+ cause
4647
4863
  });
4648
4864
  }
4649
4865
  });
@@ -4761,6 +4977,7 @@ const FULLSTACK_FRONTENDS = [
4761
4977
  "tanstack-start",
4762
4978
  "nuxt",
4763
4979
  "svelte",
4980
+ "solid",
4764
4981
  "astro"
4765
4982
  ];
4766
4983
  async function getBackendFrameworkChoice(backendFramework, frontends, previousValue) {
@@ -5198,8 +5415,8 @@ function validateDatabaseSetup(config, providedFlags) {
5198
5415
  const validation = setupValidations[dbSetup];
5199
5416
  if (dbSetup === "planetscale") {
5200
5417
  if (database !== "postgres" && database !== "mysql") return validationErr(validation.errorMessage);
5201
- } else if (validation.database && database !== validation.database) return validationErr(validation.errorMessage);
5202
- if (validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
5418
+ } else if ("database" in validation && validation.database && database !== validation.database) return validationErr(validation.errorMessage);
5419
+ if ("runtime" in validation && validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
5203
5420
  if (dbSetup === "d1") {
5204
5421
  const isWorkersTarget = hasResolvedWorkersD1Target(config);
5205
5422
  const isSelfCloudflareTarget = hasResolvedSelfCloudflareD1Target(config);
@@ -5307,7 +5524,11 @@ function validateFullConfig(config, providedFlags, options) {
5307
5524
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
5308
5525
  yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
5309
5526
  yield* validateVercelServerDeploy(config.serverDeploy, config.backend, config.runtime);
5527
+ yield* validatePrismaServerDeploy(config.serverDeploy, config.backend, config.runtime);
5528
+ yield* validatePrismaWebDeploy(config.webDeploy, config.frontend);
5529
+ yield* validateCloudflareWebDeployKnownIssues(config);
5310
5530
  yield* validateDockerWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend, config.backend, config.auth);
5531
+ yield* validatePrismaWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend);
5311
5532
  yield* validateSelfBackendCompatibility(providedFlags, options, config);
5312
5533
  yield* validateWorkersCompatibility(providedFlags, options, config);
5313
5534
  if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
@@ -5464,9 +5685,13 @@ function getDeploymentDisplay$1(deployment) {
5464
5685
  label: "Docker",
5465
5686
  hint: "Self-host with a Dockerfile and docker-compose.yml"
5466
5687
  };
5688
+ if (deployment === "prisma") return {
5689
+ label: "Prisma",
5690
+ hint: "Deploy with Prisma using Alchemy"
5691
+ };
5467
5692
  if (deployment === "vercel") return {
5468
- label: "Vercel",
5469
- hint: "Deploy to Vercel with Services"
5693
+ label: "Vercel (experimental)",
5694
+ hint: "Deploy to Vercel with Services; not fully tested"
5470
5695
  };
5471
5696
  return {
5472
5697
  label: deployment,
@@ -5479,6 +5704,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
5479
5704
  if (runtime === "workers") return "cloudflare";
5480
5705
  if (runtime !== "bun" && runtime !== "node") return "none";
5481
5706
  const options = [
5707
+ "prisma",
5482
5708
  "docker",
5483
5709
  "vercel",
5484
5710
  "none"
@@ -5515,21 +5741,33 @@ function getDeploymentDisplay(deployment) {
5515
5741
  label: "Docker",
5516
5742
  hint: "Self-host with a Dockerfile and docker-compose.yml"
5517
5743
  };
5744
+ if (deployment === "prisma") return {
5745
+ label: "Prisma",
5746
+ hint: "Deploy with Prisma using Alchemy"
5747
+ };
5518
5748
  if (deployment === "vercel") return {
5519
- label: "Vercel",
5520
- hint: "Deploy to Vercel with Services"
5749
+ label: "Vercel (experimental)",
5750
+ hint: "Deploy to Vercel with Services; not fully tested"
5521
5751
  };
5522
5752
  return {
5523
5753
  label: deployment,
5524
5754
  hint: `Add ${deployment} deployment`
5525
5755
  };
5526
5756
  }
5527
- async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, previousValue) {
5757
+ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, database, orm, addons = [], previousValue) {
5528
5758
  if (deployment !== void 0) return deployment;
5529
5759
  if (!hasWebFrontend(frontend)) return "none";
5530
5760
  if (backend === "self" && dbSetup === "d1") return "cloudflare";
5761
+ const supportsPrismaCompute = supportsPrismaWebDeploy(frontend);
5531
5762
  const options = [
5532
- "cloudflare",
5763
+ ...validateCloudflareWebDeployKnownIssues({
5764
+ webDeploy: "cloudflare",
5765
+ frontend,
5766
+ dbSetup,
5767
+ database,
5768
+ orm
5769
+ }).isOk() ? ["cloudflare"] : [],
5770
+ ...supportsPrismaCompute ? ["prisma"] : [],
5533
5771
  "docker",
5534
5772
  "vercel",
5535
5773
  "none"
@@ -5587,7 +5825,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
5587
5825
  payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
5588
5826
  addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
5589
5827
  examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
5590
- webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
5828
+ webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, results.database, results.orm, results.addons, previousAnswer),
5591
5829
  serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
5592
5830
  git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
5593
5831
  packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
@@ -5661,6 +5899,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
5661
5899
  };
5662
5900
  }
5663
5901
  //#endregion
5902
+ //#region src/utils/fs-error.ts
5903
+ function isMissingPathError(cause) {
5904
+ return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
5905
+ }
5906
+ //#endregion
5664
5907
  //#region src/prompts/project-name.ts
5665
5908
  function isPathWithinCwd$1(targetPath) {
5666
5909
  const resolved = path.resolve(targetPath);
@@ -5690,7 +5933,7 @@ async function getProjectName(initialName) {
5690
5933
  try {
5691
5934
  stats = await fs.lstat(defaultPath);
5692
5935
  } catch (error) {
5693
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
5936
+ if (isMissingPathError(error)) break;
5694
5937
  throw error;
5695
5938
  }
5696
5939
  if (stats.isDirectory() && (await fs.readdir(defaultPath)).length === 0) break;
@@ -5744,16 +5987,33 @@ async function sendConvexEvent(payload) {
5744
5987
  catch: () => void 0
5745
5988
  });
5746
5989
  }
5990
+ function buildAnalyticsEvent(config) {
5991
+ return types_exports.AnalyticsEventSchema.parse({
5992
+ database: config.database,
5993
+ orm: config.orm,
5994
+ backend: config.backend,
5995
+ runtime: config.runtime,
5996
+ frontend: config.frontend,
5997
+ addons: config.addons,
5998
+ examples: config.examples,
5999
+ auth: config.auth,
6000
+ payments: config.payments,
6001
+ git: config.git,
6002
+ packageManager: config.packageManager,
6003
+ install: config.install,
6004
+ dbSetup: config.dbSetup,
6005
+ api: config.api,
6006
+ webDeploy: config.webDeploy,
6007
+ serverDeploy: config.serverDeploy,
6008
+ cli_version: getLatestCLIVersion(),
6009
+ node_version: process.version,
6010
+ platform: process.platform
6011
+ });
6012
+ }
5747
6013
  async function trackProjectCreation(config, disableAnalytics = false) {
5748
6014
  if (!isTelemetryEnabled() || disableAnalytics) return;
5749
- const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
5750
6015
  await Result.tryPromise({
5751
- try: () => sendConvexEvent({
5752
- ...safeConfig,
5753
- cli_version: getLatestCLIVersion(),
5754
- node_version: typeof process !== "undefined" ? process.version : "",
5755
- platform: typeof process !== "undefined" ? process.platform : ""
5756
- }),
6016
+ try: () => sendConvexEvent(buildAnalyticsEvent(config)),
5757
6017
  catch: () => void 0
5758
6018
  });
5759
6019
  }
@@ -5895,7 +6155,7 @@ async function validateSafeProjectDirectoryPath(finalPathInput) {
5895
6155
  try {
5896
6156
  stats = await fs.lstat(candidatePath);
5897
6157
  } catch (error) {
5898
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
6158
+ if (isMissingPathError(error)) break;
5899
6159
  throw error;
5900
6160
  }
5901
6161
  if (stats.isSymbolicLink()) throw new CLIError({ message: `Project path "${finalPathInput}" passes through symbolic link "${path.relative(cwd, candidatePath)}". Choose a real directory within the current working directory.` });
@@ -5919,7 +6179,7 @@ async function inspectProjectPath(targetPath) {
5919
6179
  try {
5920
6180
  stats = await fs.lstat(targetPath);
5921
6181
  } catch (error) {
5922
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "missing";
6182
+ if (isMissingPathError(error)) return "missing";
5923
6183
  throw error;
5924
6184
  }
5925
6185
  if (stats.isSymbolicLink()) return "symbolic-link";
@@ -6248,9 +6508,10 @@ async function setupCloudflareD1(config) {
6248
6508
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
6249
6509
  await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
6250
6510
  key: "DATABASE_URL",
6251
- value: `file:${path.join(projectDir, targetApp, "local.db")}`,
6511
+ value: "file:./local.db",
6252
6512
  condition: true
6253
6513
  }]);
6514
+ await fs.ensureFile(path.join(projectDir, "packages/db/local.db"));
6254
6515
  await addPackageDependency({
6255
6516
  dependencies: ["@prisma/adapter-d1"],
6256
6517
  projectDir: path.join(projectDir, backend === "self" ? "apps/web" : "apps/server")
@@ -6336,7 +6597,7 @@ function mergeResolvedDbSetupOptions(dbSetup, dbSetupOptions, cliOptions = {}) {
6336
6597
  if (!dbSetupOptions && !resolvedMode) return;
6337
6598
  return {
6338
6599
  ...dbSetupOptions,
6339
- ...resolvedMode ? { mode: resolvedMode } : {}
6600
+ mode: resolvedMode
6340
6601
  };
6341
6602
  }
6342
6603
  //#endregion
@@ -7510,6 +7771,7 @@ async function setupDatabase(config, cliInput) {
7510
7771
  }
7511
7772
  const dbPackageDir = path.join(projectDir, "packages/db");
7512
7773
  if (!await fs.pathExists(dbPackageDir)) return;
7774
+ if (usesAlchemyManagedDatabase(config)) return;
7513
7775
  async function runSetup(setupFn) {
7514
7776
  const result = await setupFn();
7515
7777
  if (result.isErr()) {
@@ -7653,7 +7915,7 @@ async function displayPostInstallInstructions(config) {
7653
7915
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
7654
7916
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
7655
7917
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
7656
- const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
7918
+ const hasWeb = frontend?.some((f) => types_exports.webFrontends.includes(f));
7657
7919
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
7658
7920
  const hasReactRouter = frontend?.includes("react-router");
7659
7921
  const hasSvelte = frontend?.includes("svelte");
@@ -7667,6 +7929,13 @@ async function displayPostInstallInstructions(config) {
7667
7929
  let stepCounter = 2;
7668
7930
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
7669
7931
  if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
7932
+ const hasAlchemyD1 = dbSetup === "d1" && (serverDeploy === "cloudflare" || isBackendSelf && webDeploy === "cloudflare");
7933
+ const hasWranglerLocalD1 = (0, types_exports.getLocalD1Owner)(config) === "wrangler";
7934
+ if (hasAlchemyD1 && orm !== "none") {
7935
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:generate\n`;
7936
+ if (orm === "prisma") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:migrate\n`;
7937
+ if (hasWranglerLocalD1) output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:migrate:local\n`;
7938
+ }
7670
7939
  if (isConvex) {
7671
7940
  output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(" (this will guide you through Convex project setup)")}\n`;
7672
7941
  output += `${pc.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc.white(" packages/backend/.env.local")} to ${pc.white("apps/*/.env")}\n`;
@@ -7674,10 +7943,7 @@ async function displayPostInstallInstructions(config) {
7674
7943
  } else if (isBackendSelf) output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7675
7944
  else {
7676
7945
  if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7677
- if (runtime === "workers") {
7678
- if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
7679
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7680
- }
7946
+ if (runtime === "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7681
7947
  }
7682
7948
  const hasStandaloneBackend = backend !== "none";
7683
7949
  if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
@@ -7776,27 +8042,22 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7776
8042
  const notes = [];
7777
8043
  const commands = [];
7778
8044
  const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
8045
+ const isAlchemyManagedDatabase = (0, types_exports.usesAlchemyManagedDatabase)({
8046
+ backend,
8047
+ dbSetup,
8048
+ webDeploy,
8049
+ serverDeploy
8050
+ });
7779
8051
  if (dbSetup === "docker") {
7780
8052
  const dockerStatus = await getDockerStatus(database);
7781
8053
  if (dockerStatus.message) notes.push(dockerStatus.message);
7782
8054
  }
7783
- if (isD1Alchemy) {
7784
- if (orm === "drizzle") commands.push({
7785
- label: "Generate migrations",
7786
- command: `${runCmd} db:generate`
7787
- });
7788
- else if (orm === "prisma") {
7789
- commands.push({
7790
- label: "Generate client",
7791
- command: `${runCmd} db:generate`
7792
- });
7793
- commands.push({
7794
- label: "Apply migrations",
7795
- command: `${runCmd} db:migrate`
7796
- });
7797
- }
8055
+ if (isAlchemyManagedDatabase) {
8056
+ const provider = dbSetup === "prisma-postgres" ? "Prisma Postgres" : dbSetup === "planetscale" ? "PlanetScale" : "Neon";
8057
+ notes.push(`${pc.cyan("INFO:")} Alchemy provisions ${provider}, injects its connection credentials, and applies checked-in migrations during deploy.`);
8058
+ if (dbSetup === "planetscale") notes.push(`${pc.yellow("NOTE:")} The generated PlanetScale database uses the PS_DEV size, which may incur usage charges.`);
7798
8059
  }
7799
- if (dbSetup === "planetscale") {
8060
+ if (dbSetup === "planetscale" && !isAlchemyManagedDatabase) {
7800
8061
  if (database === "mysql" && orm === "drizzle") notes.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
7801
8062
  if (database === "mysql" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
7802
8063
  }
@@ -7807,7 +8068,11 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7807
8068
  label: "Start database",
7808
8069
  command: `${runCmd} db:start`
7809
8070
  });
7810
- if (!isD1Alchemy) {
8071
+ if (isAlchemyManagedDatabase) commands.push({
8072
+ label: "Generate client",
8073
+ command: `${runCmd} db:generate`
8074
+ });
8075
+ else if (!isD1Alchemy) {
7811
8076
  commands.push({
7812
8077
  label: "Generate client",
7813
8078
  command: `${runCmd} db:generate`
@@ -7817,7 +8082,7 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7817
8082
  command: `${runCmd} db:push`
7818
8083
  });
7819
8084
  }
7820
- if (!isD1Alchemy) commands.push({
8085
+ if (!isD1Alchemy && !isAlchemyManagedDatabase) commands.push({
7821
8086
  label: "Open studio",
7822
8087
  command: `${runCmd} db:studio`
7823
8088
  });
@@ -7826,11 +8091,15 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7826
8091
  label: "Start database",
7827
8092
  command: `${runCmd} db:start`
7828
8093
  });
7829
- if (!isD1Alchemy) commands.push({
8094
+ if (isAlchemyManagedDatabase) commands.push({
8095
+ label: "Generate migrations",
8096
+ command: `${runCmd} db:generate`
8097
+ });
8098
+ else if (!isD1Alchemy) commands.push({
7830
8099
  label: "Apply schema",
7831
8100
  command: `${runCmd} db:push`
7832
8101
  });
7833
- if (!isD1Alchemy) commands.push({
8102
+ if (!isD1Alchemy && !isAlchemyManagedDatabase) commands.push({
7834
8103
  label: "Open studio",
7835
8104
  command: `${runCmd} db:studio`
7836
8105
  });
@@ -7944,19 +8213,20 @@ function getPolarInstructions(backend, packageManager) {
7944
8213
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
7945
8214
  const instructions = [];
7946
8215
  const isBackendSelf = backend === "self";
7947
- if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) {
7948
- const cfDeploy = serverDeploy === "vercel" ? "deploy:web" : "deploy";
7949
- instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("")} Dev: ${`${runCmd} dev`}\n${pc.cyan("")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7950
- } else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) {
7951
- const cfDeploy = webDeploy === "vercel" ? "deploy:server" : "deploy";
7952
- instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("")} Dev: ${`${runCmd} dev`}\n${pc.cyan("")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc.cyan("")} Destroy: ${`${runCmd} destroy`}`);
7953
- } else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
8216
+ const hasAlchemyWeb = (0, types_exports.isAlchemyDeployTarget)(webDeploy);
8217
+ const hasAlchemyServer = (0, types_exports.isAlchemyDeployTarget)(serverDeploy);
8218
+ const alchemyExec = runCmd === "npm run" ? "npx" : runCmd === "pnpm run" ? "pnpm exec" : "bunx";
8219
+ if (hasAlchemyWeb || hasAlchemyServer) {
8220
+ const targetParts = [...hasAlchemyWeb ? [`web on ${webDeploy === "cloudflare" ? "Cloudflare" : "Prisma"}`] : [], ...hasAlchemyServer && !isBackendSelf ? [`server on ${serverDeploy === "cloudflare" ? "Cloudflare" : "Prisma"}`] : []];
8221
+ const deployScript = webDeploy === "vercel" ? "deploy:server" : serverDeploy === "vercel" ? "deploy:web" : "deploy";
8222
+ instructions.push(`${pc.bold(`Deploy with Alchemy (${targetParts.join(" + ")}):`)}\n${pc.cyan("•")} Configure provider login: ${`cd packages/infra && ${alchemyExec} alchemy login --configure`}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${deployScript}`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
8223
+ }
7954
8224
  if (webDeploy === "docker" || serverDeploy === "docker") {
7955
8225
  const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
7956
8226
  instructions.push(`${pc.bold(`Deploy ${dockerTargets} with Docker Compose:`)}\n${pc.cyan("•")} Start: ${`${runCmd} docker:up`}\n${pc.cyan("•")} Logs: ${`${runCmd} docker:logs`}\n${pc.cyan("•")} Stop: ${`${runCmd} docker:down`}\n${pc.cyan("•")} Config: docker-compose.yml`);
7957
8227
  }
7958
8228
  if (webDeploy === "vercel" || serverDeploy === "vercel") {
7959
- const mixedWithCloudflare = webDeploy === "cloudflare" || serverDeploy === "cloudflare";
8229
+ const mixedWithCloudflare = hasAlchemyWeb || hasAlchemyServer;
7960
8230
  const vercelSetupScript = "deploy:setup";
7961
8231
  const vercelEnvScript = "env:production";
7962
8232
  const vercelDeployScript = mixedWithCloudflare ? webDeploy === "vercel" ? "deploy:web:prod" : "deploy:server:prod" : "deploy:prod";
@@ -7971,7 +8241,7 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
7971
8241
  * Creates a new project with the given configuration.
7972
8242
  * Returns a Result with the project directory path on success, or an error on failure.
7973
8243
  */
7974
- async function createProject(options, cliInput = {}) {
8244
+ async function createProject(options, cliInput) {
7975
8245
  return Result.gen(async function* () {
7976
8246
  const projectDir = options.projectDir;
7977
8247
  const isConvex = options.backend === "convex";
@@ -7997,7 +8267,7 @@ async function createProject(options, cliInput = {}) {
7997
8267
  message: e.message,
7998
8268
  cause: e
7999
8269
  }))));
8000
- yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager));
8270
+ yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager, cliInput.packageManagerVersion));
8001
8271
  if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
8002
8272
  try: () => setupDatabase(options, cliInput),
8003
8273
  catch: (e) => new ProjectCreationError({
@@ -8028,21 +8298,13 @@ async function createProject(options, cliInput = {}) {
8028
8298
  return Result.ok(projectDir);
8029
8299
  });
8030
8300
  }
8031
- async function setPackageManagerVersion(projectDir, packageManager) {
8301
+ async function setPackageManagerVersion(projectDir, packageManager, version) {
8032
8302
  const pkgJsonPath = path.join(projectDir, "package.json");
8033
8303
  if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
8034
- const versionResult = await Result.tryPromise({
8035
- try: async () => {
8036
- const { stdout } = await $({ cwd: os.tmpdir() })`${packageManager} -v`;
8037
- return stdout.trim();
8038
- },
8039
- catch: () => null
8040
- });
8041
8304
  return Result.tryPromise({
8042
8305
  try: async () => {
8043
8306
  const pkgJson = await fs.readJson(pkgJsonPath);
8044
- if (versionResult.isOk() && versionResult.value) pkgJson.packageManager = `${packageManager}@${versionResult.value}`;
8045
- else delete pkgJson.packageManager;
8307
+ pkgJson.packageManager = `${packageManager}@${version}`;
8046
8308
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
8047
8309
  },
8048
8310
  catch: (e) => new ProjectCreationError({
@@ -8134,11 +8396,11 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8134
8396
  currentPathInput = defaultName;
8135
8397
  } else currentPathInput = yield* Result.await(Result.tryPromise({
8136
8398
  try: async () => getProjectName(input.projectName),
8137
- catch: (e) => {
8138
- if (e instanceof UserCancelledError) return e;
8399
+ catch: (cause) => {
8400
+ if (cause instanceof UserCancelledError) return cause;
8139
8401
  return new CLIError({
8140
- message: e instanceof Error ? e.message : String(e),
8141
- cause: e
8402
+ message: cause instanceof Error ? cause.message : String(cause),
8403
+ cause
8142
8404
  });
8143
8405
  }
8144
8406
  }));
@@ -8163,8 +8425,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8163
8425
  const templateName = input.template.toUpperCase();
8164
8426
  const templateDescription = getTemplateDescription(input.template);
8165
8427
  if (!isSilent()) log.info(`${pc.dim("Template")} ${pc.bold(pc.cyan(templateName))}\n${pc.dim(templateDescription)}`);
8166
- const userOverrides = {};
8167
- for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
8428
+ const userOverrides = Object.fromEntries(Object.entries(originalInput).filter(([, value]) => value !== void 0));
8168
8429
  cliInput = {
8169
8430
  ...templateConfig,
8170
8431
  ...userOverrides,
@@ -8206,11 +8467,11 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8206
8467
  if (!isSilent() && !isTemplateSetup && Object.keys(otherFlags).length > 0) log.info(pc.dim("Command-line options applied."));
8207
8468
  config = yield* Result.await(Result.tryPromise({
8208
8469
  try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput, { skipCompatibilityChecks: cliInput.yolo }),
8209
- catch: (e) => {
8210
- if (e instanceof UserCancelledError) return e;
8470
+ catch: (cause) => {
8471
+ if (cause instanceof UserCancelledError) return cause;
8211
8472
  return new CLIError({
8212
- message: e instanceof Error ? e.message : String(e),
8213
- cause: e
8473
+ message: cause instanceof Error ? cause.message : String(cause),
8474
+ cause
8214
8475
  });
8215
8476
  }
8216
8477
  }));
@@ -8230,13 +8491,15 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8230
8491
  cause: resolvedConfigValidationResult.error
8231
8492
  });
8232
8493
  }
8494
+ const localRequirements = yield* Result.await(checkLocalRequirements(config));
8495
+ if (!isSilent()) for (const warning of localRequirements.warnings) log.warn(pc.yellow(warning));
8233
8496
  if (!input.dryRun) yield* Result.await(Result.tryPromise({
8234
8497
  try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
8235
- catch: (e) => {
8236
- if (e instanceof UserCancelledError) return e;
8498
+ catch: (cause) => {
8499
+ if (cause instanceof UserCancelledError) return cause;
8237
8500
  return new CLIError({
8238
- message: e instanceof Error ? e.message : String(e),
8239
- cause: e
8501
+ message: cause instanceof Error ? cause.message : String(cause),
8502
+ cause
8240
8503
  });
8241
8504
  }
8242
8505
  }));
@@ -8266,7 +8529,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8266
8529
  }
8267
8530
  yield* Result.await(createProject(config, {
8268
8531
  manualDb: cliInput.manualDb ?? input.manualDb,
8269
- dbSetupOptions: effectiveDbSetupOptions
8532
+ dbSetupOptions: effectiveDbSetupOptions,
8533
+ packageManagerVersion: localRequirements.packageManagerVersion
8270
8534
  }));
8271
8535
  await trackProjectCreation(config, input.disableAnalytics);
8272
8536
  const historyResult = await addToHistory(config, reproducibleCommand);
@@ -8322,12 +8586,12 @@ async function handleDirectoryConflictResult(currentPathInput, strategy) {
8322
8586
  if (strategy) return handleDirectoryConflictProgrammatically(currentPathInput, strategy);
8323
8587
  return Result.tryPromise({
8324
8588
  try: async () => handleDirectoryConflict(currentPathInput),
8325
- catch: (e) => {
8326
- if (e instanceof UserCancelledError) return e;
8327
- if (e instanceof CLIError) return e;
8589
+ catch: (cause) => {
8590
+ if (cause instanceof UserCancelledError) return cause;
8591
+ if (cause instanceof CLIError) return cause;
8328
8592
  return new CLIError({
8329
- message: e instanceof Error ? e.message : String(e),
8330
- cause: e
8593
+ message: cause instanceof Error ? cause.message : String(cause),
8594
+ cause
8331
8595
  });
8332
8596
  }
8333
8597
  });
@@ -8532,10 +8796,10 @@ function formatInputValidationError(label, error) {
8532
8796
  * ```
8533
8797
  */
8534
8798
  async function create(projectName, options) {
8535
- const rawInput = options === void 0 || typeof options === "object" && options !== null ? {
8799
+ const rawInput = {
8536
8800
  ...options,
8537
8801
  projectName
8538
- } : options;
8802
+ };
8539
8803
  const parsedInput = types_exports.CreateInputSchema.safeParse(rawInput);
8540
8804
  if (!parsedInput.success) return Result.err(new CLIError({
8541
8805
  message: formatInputValidationError("create", parsedInput.error),
@@ -8554,14 +8818,14 @@ async function create(projectName, options) {
8554
8818
  if (result.isErr()) throw result.error;
8555
8819
  return result.value;
8556
8820
  },
8557
- catch: (e) => {
8558
- if (UserCancelledError.is(e)) return e;
8559
- if (CLIError.is(e)) return e;
8560
- if (DirectoryConflictError.is(e)) return e;
8561
- if (ProjectCreationError.is(e)) return e;
8821
+ catch: (cause) => {
8822
+ if (UserCancelledError.is(cause)) return cause;
8823
+ if (CLIError.is(cause)) return cause;
8824
+ if (DirectoryConflictError.is(cause)) return cause;
8825
+ if (ProjectCreationError.is(cause)) return cause;
8562
8826
  return new CLIError({
8563
- message: e instanceof Error ? e.message : String(e),
8564
- cause: e
8827
+ message: cause instanceof Error ? cause.message : String(cause),
8828
+ cause
8565
8829
  });
8566
8830
  }
8567
8831
  });
@@ -8629,21 +8893,7 @@ async function createVirtual(options) {
8629
8893
  webDeploy: virtualOptions.webDeploy || "none",
8630
8894
  serverDeploy: virtualOptions.serverDeploy || "none"
8631
8895
  };
8632
- const validationResult = validateConfigCompatibility(config, /* @__PURE__ */ new Set([
8633
- "database",
8634
- "orm",
8635
- "backend",
8636
- "runtime",
8637
- "frontend",
8638
- "addons",
8639
- "examples",
8640
- "auth",
8641
- "dbSetup",
8642
- "payments",
8643
- "api",
8644
- "webDeploy",
8645
- "serverDeploy"
8646
- ]), config);
8896
+ const validationResult = validateResolvedConfigCompatibility(config);
8647
8897
  if (validationResult.isErr()) return Result.err(new GeneratorError({
8648
8898
  message: validationResult.error.message,
8649
8899
  phase: "validation",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.38.2",
3
+ "version": "3.39.2",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -69,8 +69,8 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.38.2",
73
- "@better-t-stack/types": "^3.38.2",
72
+ "@better-t-stack/template-generator": "^3.39.2",
73
+ "@better-t-stack/types": "^3.39.2",
74
74
  "@clack/core": "^1.4.3",
75
75
  "@clack/prompts": "^1.7.0",
76
76
  "@modelcontextprotocol/sdk": "1.30.0",
@@ -83,8 +83,9 @@
83
83
  "gradient-string": "^3.0.0",
84
84
  "handlebars": "^4.7.9",
85
85
  "jsonc-parser": "^3.3.1",
86
- "oxfmt": "^0.61.0",
86
+ "oxfmt": "^0.63.0",
87
87
  "picocolors": "^1.1.1",
88
+ "semver": "^7.8.5",
88
89
  "tinyglobby": "^0.2.17",
89
90
  "trpc-cli": "^0.16.0",
90
91
  "ts-morph": "^28.0.0",
@@ -95,8 +96,12 @@
95
96
  "@types/bun": "^1.3.14",
96
97
  "@types/fs-extra": "^11.0.4",
97
98
  "@types/node": "^26.1.2",
99
+ "@types/semver": "^7.8.0",
98
100
  "publint": "^0.3.22",
99
101
  "tsdown": "^0.22.14",
100
102
  "typescript": "^6.0.3"
103
+ },
104
+ "engines": {
105
+ "node": ">=22.0.0"
101
106
  }
102
107
  }