create-better-t-stack 3.31.1 → 3.33.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 CHANGED
@@ -29,21 +29,21 @@ Follow the prompts to configure your project or use the `--yes` flag for default
29
29
 
30
30
  ## Features
31
31
 
32
- | Category | Options |
33
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
- | **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 |
36
- | **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
37
- | **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
38
- | **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
39
- | **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
40
- | **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
41
- | **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
42
- | **Authentication** | • Better Auth<br>• Clerk |
43
- | **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
44
- | **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
45
- | **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
46
- | **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
32
+ | Category | Options |
33
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | **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 |
36
+ | **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
37
+ | **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
38
+ | **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
39
+ | **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
40
+ | **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
41
+ | **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
42
+ | **Authentication** | • Better Auth<br>• Clerk |
43
+ | **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
44
+ | **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite, or Vite+ (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo, Nx, or Vite+ (monorepo orchestration) |
45
+ | **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
46
+ | **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
47
47
 
48
48
  ## Usage
49
49
 
@@ -60,7 +60,7 @@ Options:
60
60
  --auth <provider> Authentication (better-auth, clerk, none)
61
61
  --payments <provider> Payments provider (polar, none)
62
62
  --frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, astro, native-bare, native-uniwind, native-unistyles, none)
63
- --addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
63
+ --addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, vite-plus, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
64
64
  --examples <types...> Examples to include (todo, ai, none)
65
65
  --git Initialize git repository
66
66
  --no-git Skip git initialization
@@ -68,8 +68,8 @@ Options:
68
68
  --install Install dependencies
69
69
  --no-install Skip installing dependencies
70
70
  --db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, planetscale, mongodb-atlas, docker, none)
71
- --web-deploy <setup> Web deployment (cloudflare, none)
72
- --server-deploy <setup> Server deployment (cloudflare, none)
71
+ --web-deploy <setup> Web deployment (cloudflare, docker, none)
72
+ --server-deploy <setup> Server deployment (cloudflare, docker, none)
73
73
  --backend <framework> Backend framework (hono, express, fastify, elysia, convex, self, none)
74
74
  --runtime <runtime> Runtime (bun, node, workers, none)
75
75
  --api <type> API type (trpc, orpc, none)
@@ -223,6 +223,12 @@ Create a self-hosted fullstack project on Cloudflare with D1:
223
223
  npx create-better-t-stack --backend self --frontend next --api trpc --database sqlite --orm drizzle --db-setup d1 --web-deploy cloudflare
224
224
  ```
225
225
 
226
+ Create a self-hosted project that ships as Docker containers (web + server + database via Docker Compose):
227
+
228
+ ```bash
229
+ npx create-better-t-stack --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --db-setup docker --web-deploy docker --server-deploy docker
230
+ ```
231
+
226
232
  Create a minimal API-only project:
227
233
 
228
234
  ```bash
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-ByCnjYDk.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-_FzI_58B.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";
package/dist/index.d.mts CHANGED
@@ -175,7 +175,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
175
175
  auth?: "none" | "better-auth" | "clerk" | undefined;
176
176
  payments?: "none" | "polar" | undefined;
177
177
  frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
178
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
178
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
179
179
  examples?: ("none" | "todo" | "ai")[] | undefined;
180
180
  git?: boolean | undefined;
181
181
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
@@ -184,8 +184,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
184
184
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
185
185
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
186
186
  api?: "none" | "trpc" | "orpc" | undefined;
187
- webDeploy?: "none" | "cloudflare" | undefined;
188
- serverDeploy?: "none" | "cloudflare" | undefined;
187
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
188
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
189
189
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
190
190
  renderTitle?: boolean | undefined;
191
191
  disableAnalytics?: boolean | undefined;
@@ -228,7 +228,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
228
228
  devPort?: number | undefined;
229
229
  search?: "orama" | "orama-cloud" | undefined;
230
230
  ogImage?: "next-og" | "takumi" | undefined;
231
- aiChat?: "openrouter" | "inkeep" | undefined;
231
+ aiChat?: "openrouter" | "llmgateway" | "inkeep" | undefined;
232
232
  } | undefined;
233
233
  opentui?: {
234
234
  template: "solid" | "react" | "core";
@@ -274,7 +274,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
274
274
  auth?: "none" | "better-auth" | "clerk" | undefined;
275
275
  payments?: "none" | "polar" | undefined;
276
276
  frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
277
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
277
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
278
278
  examples?: ("none" | "todo" | "ai")[] | undefined;
279
279
  git?: boolean | undefined;
280
280
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
@@ -283,8 +283,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
283
283
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
284
284
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
285
285
  api?: "none" | "trpc" | "orpc" | undefined;
286
- webDeploy?: "none" | "cloudflare" | undefined;
287
- serverDeploy?: "none" | "cloudflare" | undefined;
286
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
287
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
288
288
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
289
289
  renderTitle?: boolean | undefined;
290
290
  disableAnalytics?: boolean | undefined;
@@ -317,7 +317,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
317
317
  }>;
318
318
  add: _$_trpc_server0.TRPCMutationProcedure<{
319
319
  input: {
320
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
320
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
321
321
  install?: boolean | undefined;
322
322
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
323
323
  projectDir?: string | undefined;
@@ -327,7 +327,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
327
327
  }>;
328
328
  addJson: _$_trpc_server0.TRPCMutationProcedure<{
329
329
  input: {
330
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
330
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
331
331
  addonOptions?: {
332
332
  wxt?: {
333
333
  template: "svelte" | "solid" | "vanilla" | "vue" | "react";
@@ -338,7 +338,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
338
338
  devPort?: number | undefined;
339
339
  search?: "orama" | "orama-cloud" | undefined;
340
340
  ogImage?: "next-og" | "takumi" | undefined;
341
- aiChat?: "openrouter" | "inkeep" | undefined;
341
+ aiChat?: "openrouter" | "llmgateway" | "inkeep" | undefined;
342
342
  } | undefined;
343
343
  opentui?: {
344
344
  template: "solid" | "react" | "core";
@@ -363,8 +363,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
363
363
  hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
364
364
  } | undefined;
365
365
  } | undefined;
366
- webDeploy?: "none" | "cloudflare" | undefined;
367
- serverDeploy?: "none" | "cloudflare" | undefined;
366
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
367
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
368
368
  projectDir?: string | undefined;
369
369
  install?: boolean | undefined;
370
370
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
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-ByCnjYDk.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-_FzI_58B.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 };
@@ -12,7 +12,7 @@ import envPaths from "env-paths";
12
12
  import fs from "fs-extra";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { desktopWebFrontends as desktopWebFrontends$3 } from "@better-t-stack/types";
15
- import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
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";
18
18
  import gradient from "gradient-string";
@@ -78,6 +78,7 @@ const ADDON_COMPATIBILITY = {
78
78
  lefthook: [],
79
79
  turborepo: [],
80
80
  nx: [],
81
+ "vite-plus": [],
81
82
  starlight: [],
82
83
  ultracite: [],
83
84
  mcp: [],
@@ -729,6 +730,11 @@ const WEB_FRAMEWORKS = [
729
730
  ];
730
731
  //#endregion
731
732
  //#region src/utils/compatibility-rules.ts
733
+ const TASK_RUNNER_ADDONS$1 = [
734
+ "turborepo",
735
+ "nx",
736
+ "vite-plus"
737
+ ];
732
738
  const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
733
739
  "nuxt",
734
740
  "svelte",
@@ -869,6 +875,12 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
869
875
  if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
870
876
  return Result.ok(void 0);
871
877
  }
878
+ function validateDockerServerDeploy(serverDeploy, backend, runtime) {
879
+ if (serverDeploy !== "docker") return Result.ok(void 0);
880
+ 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.");
881
+ 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'.");
882
+ return Result.ok(void 0);
883
+ }
872
884
  function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
873
885
  if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
874
886
  isCompatible: false,
@@ -887,12 +899,13 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
887
899
  return allAddons.filter((addon) => {
888
900
  if (existingAddons.includes(addon)) return false;
889
901
  if (addon === "none") return false;
902
+ if (TASK_RUNNER_ADDONS$1.includes(addon) && existingAddons.some((existingAddon) => TASK_RUNNER_ADDONS$1.includes(existingAddon))) return false;
890
903
  const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
891
904
  return isCompatible;
892
905
  });
893
906
  }
894
907
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
895
- if (addons.includes("turborepo") && addons.includes("nx")) return validationErr$1("Cannot combine 'turborepo' and 'nx' addons. Choose one monorepo tool.");
908
+ if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
896
909
  for (const addon of addons) {
897
910
  if (addon === "none") continue;
898
911
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -978,7 +991,7 @@ function normalizeValidationMessage(validationMessage) {
978
991
  async function runWithNavigation(prompt) {
979
992
  let goBack = false;
980
993
  prompt.on("key", (char) => {
981
- if (char === "b" && !isFirstPrompt()) {
994
+ if ((char === "b" || char === "B") && !isFirstPrompt()) {
982
995
  goBack = true;
983
996
  prompt.state = "cancel";
984
997
  }
@@ -1164,6 +1177,10 @@ async function navigableGroupMultiselect(opts) {
1164
1177
  }
1165
1178
  }));
1166
1179
  }
1180
+ /** Use the remembered answer as the initial value only while it is still selectable. */
1181
+ function preferValidInitial(options, previous, fallback) {
1182
+ return previous !== void 0 && options.some((o) => o.value === previous) ? previous : fallback;
1183
+ }
1167
1184
  //#endregion
1168
1185
  //#region src/prompts/addons.ts
1169
1186
  function getAddonDisplay(addon) {
@@ -1178,6 +1195,10 @@ function getAddonDisplay(addon) {
1178
1195
  label = "Nx";
1179
1196
  hint = "Smart monorepo orchestration and task graph";
1180
1197
  break;
1198
+ case "vite-plus":
1199
+ label = "Vite+";
1200
+ hint = "Unified Vite toolchain and workspace task runner";
1201
+ break;
1181
1202
  case "pwa":
1182
1203
  label = "PWA";
1183
1204
  hint = "Make your app installable and work offline";
@@ -1248,7 +1269,11 @@ function getAddonDisplay(addon) {
1248
1269
  };
1249
1270
  }
1250
1271
  const ADDON_GROUPS = {
1251
- "Monorepo & Tasks": ["turborepo", "nx"],
1272
+ "Monorepo & Tasks": [
1273
+ "turborepo",
1274
+ "nx",
1275
+ "vite-plus"
1276
+ ],
1252
1277
  "Code Quality": [
1253
1278
  "biome",
1254
1279
  "oxlint",
@@ -1289,9 +1314,13 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1289
1314
  });
1290
1315
  }
1291
1316
  function validateAddonSelection(selected) {
1292
- if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
1317
+ if ((selected?.filter((addon) => [
1318
+ "turborepo",
1319
+ "nx",
1320
+ "vite-plus"
1321
+ ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1293
1322
  }
1294
- async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1323
+ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1295
1324
  if (addons !== void 0) return addons;
1296
1325
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
1297
1326
  const groupedOptions = createGroupedOptions();
@@ -1310,7 +1339,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1310
1339
  const response = await navigableGroupMultiselect({
1311
1340
  message: "Select addons",
1312
1341
  options: groupedOptions,
1313
- initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1342
+ initialValues: (previousValue ?? DEFAULT_CONFIG.addons).filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1314
1343
  required: false,
1315
1344
  validate: validateAddonSelection
1316
1345
  });
@@ -1955,26 +1984,36 @@ async function setupEvlog(config) {
1955
1984
  /**
1956
1985
  * Define a group of prompts that supports going back to previous prompts.
1957
1986
  * Returns a result object with all the values, or handles cancel/go-back navigation.
1987
+ * When navigating back, the discarded answers are replayed to the prompts as
1988
+ * `previousAnswer` so they can preselect what the user chose before.
1958
1989
  */
1959
1990
  async function navigableGroup(prompts, opts) {
1960
1991
  const results = {};
1992
+ const previousAnswers = {};
1961
1993
  const promptNames = Object.keys(prompts);
1962
1994
  let currentIndex = 0;
1963
1995
  let goingBack = false;
1996
+ const stepBack = () => {
1997
+ const prevName = promptNames[currentIndex - 1];
1998
+ previousAnswers[prevName] = results[prevName];
1999
+ delete results[prevName];
2000
+ currentIndex--;
2001
+ };
1964
2002
  while (currentIndex < promptNames.length) {
1965
2003
  const name = promptNames[currentIndex];
1966
2004
  const prompt = prompts[name];
1967
2005
  setIsFirstPrompt$1(currentIndex === 0);
1968
2006
  setLastPromptShownUI(false);
1969
- const result = await prompt({ results })?.catch((e) => {
2007
+ const result = await prompt({
2008
+ results,
2009
+ previousAnswer: previousAnswers[name]
2010
+ })?.catch((e) => {
1970
2011
  throw e;
1971
2012
  });
1972
2013
  if (isGoBack(result)) {
1973
2014
  goingBack = true;
1974
2015
  if (currentIndex > 0) {
1975
- const prevName = promptNames[currentIndex - 1];
1976
- delete results[prevName];
1977
- currentIndex--;
2016
+ stepBack();
1978
2017
  continue;
1979
2018
  }
1980
2019
  goingBack = false;
@@ -1990,9 +2029,7 @@ async function navigableGroup(prompts, opts) {
1990
2029
  }
1991
2030
  if (goingBack && !didLastPromptShowUI()) {
1992
2031
  if (currentIndex > 0) {
1993
- const prevName = promptNames[currentIndex - 1];
1994
- delete results[prevName];
1995
- currentIndex--;
2032
+ stepBack();
1996
2033
  continue;
1997
2034
  }
1998
2035
  }
@@ -2142,6 +2179,14 @@ const DEFAULT_DEV_PORT$1 = 4e3;
2142
2179
  function aiChatDisabledForTemplate(template) {
2143
2180
  return template === "next-mdx-static" || template.endsWith("-spa");
2144
2181
  }
2182
+ function getFumadocsLinter(addons) {
2183
+ if (addons.includes("oxlint")) return "oxlint";
2184
+ if (addons.includes("biome") || addons.includes("ultracite")) return "biome";
2185
+ if (addons.includes("vite-plus")) return "oxlint";
2186
+ }
2187
+ function getFumadocsAddonContext(currentAddons, persistedAddons) {
2188
+ return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
2189
+ }
2145
2190
  async function setupFumadocs(config) {
2146
2191
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2147
2192
  const { packageManager, projectDir } = config;
@@ -2215,6 +2260,10 @@ async function setupFumadocs(config) {
2215
2260
  label: "AI SDK",
2216
2261
  hint: "default to OpenRouter"
2217
2262
  },
2263
+ {
2264
+ value: "llmgateway",
2265
+ label: "LLM Gateway"
2266
+ },
2218
2267
  {
2219
2268
  value: "inkeep",
2220
2269
  label: "Inkeep AI",
@@ -2251,7 +2300,9 @@ async function setupFumadocs(config) {
2251
2300
  "--no-git"
2252
2301
  ];
2253
2302
  if (isNextTemplate) options.push("--src");
2254
- if (config.addons.includes("biome") || config.addons.includes("ultracite")) options.push("--linter biome");
2303
+ const persistedConfig = await readBtsConfig(projectDir);
2304
+ const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
2305
+ if (linter) options.push(`--linter ${linter}`);
2255
2306
  if (search) options.push(`--search ${search}`);
2256
2307
  if (ogImage) options.push(`--og-image ${ogImage}`);
2257
2308
  if (aiChat) options.push(`--ai-chat ${aiChat}`);
@@ -3630,6 +3681,7 @@ async function setupAddons(config) {
3630
3681
  const hasHusky = addons.includes("husky");
3631
3682
  const hasLefthook = addons.includes("lefthook");
3632
3683
  const hasOxlint = addons.includes("oxlint");
3684
+ const hasVitePlus = addons.includes("vite-plus");
3633
3685
  if (hasUltracite) {
3634
3686
  const gitHooks = [];
3635
3687
  if (hasHusky) gitHooks.push("husky");
@@ -3642,6 +3694,7 @@ async function setupAddons(config) {
3642
3694
  let linter;
3643
3695
  if (hasOxlint) linter = "oxlint";
3644
3696
  else if (hasBiome) linter = "biome";
3697
+ else if (hasVitePlus) linter = "vite-plus";
3645
3698
  if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
3646
3699
  if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3647
3700
  }
@@ -3683,6 +3736,7 @@ async function setupHusky(projectDir, linter) {
3683
3736
  };
3684
3737
  if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
3685
3738
  else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
3739
+ else if (linter === "vite-plus") packageJson["lint-staged"] = { "*.{js,ts,jsx,tsx,vue,svelte,json,jsonc,css,md}": ["vp check --fix"] };
3686
3740
  else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
3687
3741
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3688
3742
  }
@@ -3750,6 +3804,35 @@ async function installDependencies({ projectDir, packageManager }) {
3750
3804
  }
3751
3805
  //#endregion
3752
3806
  //#region src/helpers/core/add-handler.ts
3807
+ const ADD_PACKAGE_JSON_PATHS = [
3808
+ "package.json",
3809
+ "apps/server/package.json",
3810
+ "apps/web/package.json",
3811
+ "apps/native/package.json",
3812
+ "apps/desktop/package.json",
3813
+ "apps/fumadocs/package.json",
3814
+ "apps/docs/package.json",
3815
+ "packages/api/package.json",
3816
+ "packages/db/package.json",
3817
+ "packages/auth/package.json",
3818
+ "packages/backend/package.json",
3819
+ "packages/config/package.json",
3820
+ "packages/env/package.json",
3821
+ "packages/infra/package.json",
3822
+ "packages/ui/package.json"
3823
+ ];
3824
+ const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
3825
+ const HOOK_ADDONS = ["husky", "lefthook"];
3826
+ const HOOK_LINTER_ADDONS = [
3827
+ "biome",
3828
+ "oxlint",
3829
+ "vite-plus"
3830
+ ];
3831
+ const TASK_RUNNER_ADDONS = [
3832
+ "turborepo",
3833
+ "nx",
3834
+ "vite-plus"
3835
+ ];
3753
3836
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3754
3837
  if (!existingAddonOptions && !nextAddonOptions) return;
3755
3838
  const mergedAddonOptions = { ...existingAddonOptions };
@@ -3763,6 +3846,29 @@ function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3763
3846
  }
3764
3847
  return Object.keys(mergedAddonOptions).length > 0 ? mergedAddonOptions : void 0;
3765
3848
  }
3849
+ function getSetupAddons(addonsToAdd, updatedAddons) {
3850
+ const setupAddons = new Set(addonsToAdd);
3851
+ if (addonsToAdd.some((addon) => HOOK_ADDONS.includes(addon) || HOOK_LINTER_ADDONS.includes(addon))) {
3852
+ for (const addon of [...HOOK_ADDONS, ...HOOK_LINTER_ADDONS]) if (updatedAddons.includes(addon)) setupAddons.add(addon);
3853
+ }
3854
+ return [...setupAddons];
3855
+ }
3856
+ function shouldRefreshLefthook(addonsToAdd, updatedAddons) {
3857
+ return updatedAddons.includes("lefthook") && addonsToAdd.some((addon) => addon === "lefthook" || HOOK_LINTER_ADDONS.includes(addon));
3858
+ }
3859
+ function refreshLefthookTemplate(vfs, config) {
3860
+ const template = EMBEDDED_TEMPLATES.get("addons/lefthook/lefthook.yml.hbs");
3861
+ if (!template) return;
3862
+ vfs.writeFile("lefthook.yml", processTemplateString(template, config));
3863
+ }
3864
+ function updateViteConfigImportsForVitePlus(vfs) {
3865
+ for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
3866
+ const content = vfs.readFile(viteConfigPath);
3867
+ if (!content) continue;
3868
+ const updatedContent = content.replaceAll("from \"vite\";", "from \"vite-plus\";").replaceAll("from 'vite';", "from 'vite-plus';");
3869
+ if (updatedContent !== content) vfs.writeFile(viteConfigPath, updatedContent);
3870
+ }
3871
+ }
3766
3872
  async function addHandler(input, options = {}) {
3767
3873
  const { silent = false } = options;
3768
3874
  return runWithContextAsync({ silent }, async () => {
@@ -3840,10 +3946,10 @@ async function addHandlerInternal(input) {
3840
3946
  }
3841
3947
  addonsToAdd = selectedAddons;
3842
3948
  }
3843
- const addonsValidationResult = validateAddonsAgainstConfig(addonsToAdd, existingConfig);
3949
+ const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3950
+ const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3844
3951
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3845
3952
  if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
3846
- const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3847
3953
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
3848
3954
  const config = {
3849
3955
  projectName: existingConfig.projectName,
@@ -3867,27 +3973,39 @@ async function addHandlerInternal(input) {
3867
3973
  webDeploy: existingConfig.webDeploy,
3868
3974
  serverDeploy: existingConfig.serverDeploy
3869
3975
  };
3976
+ const updatedConfig = {
3977
+ ...config,
3978
+ addons: updatedAddons
3979
+ };
3870
3980
  if (!isSilent()) log.info(pc.dim("Installing addon files..."));
3871
3981
  const vfs = new VirtualFileSystem();
3872
- for (const pkgPath of [
3873
- "package.json",
3874
- "apps/web/package.json",
3875
- "apps/server/package.json",
3876
- "apps/native/package.json"
3877
- ]) {
3982
+ for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
3878
3983
  const fullPath = path.join(projectDir, pkgPath);
3879
3984
  if (await fs.pathExists(fullPath)) {
3880
3985
  const content = await fs.readFile(fullPath, "utf-8");
3881
3986
  vfs.writeFile(pkgPath, content);
3882
3987
  }
3883
3988
  }
3989
+ for (const filePath of ADD_TEXT_FILE_PATHS) {
3990
+ const fullPath = path.join(projectDir, filePath);
3991
+ if (await fs.pathExists(fullPath)) {
3992
+ const content = await fs.readFile(fullPath, "utf-8");
3993
+ vfs.writeFile(filePath, content);
3994
+ }
3995
+ }
3884
3996
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
3885
3997
  processAddonsDeps(vfs, config);
3998
+ if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
3999
+ if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
4000
+ if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
4001
+ if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
4002
+ if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
4003
+ if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
3886
4004
  const tree = {
3887
4005
  root: vfs.toTree(""),
3888
4006
  fileCount: vfs.getFileCount(),
3889
4007
  directoryCount: vfs.getDirectoryCount(),
3890
- config
4008
+ config: updatedConfig
3891
4009
  };
3892
4010
  if (input.dryRun) {
3893
4011
  if (!isSilent()) {
@@ -3907,7 +4025,10 @@ async function addHandlerInternal(input) {
3907
4025
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
3908
4026
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
3909
4027
  const setupResult = await Result.tryPromise({
3910
- try: () => setupAddons(config),
4028
+ try: () => setupAddons({
4029
+ ...config,
4030
+ addons: getSetupAddons(addonsToAdd, updatedAddons)
4031
+ }),
3911
4032
  catch: (e) => {
3912
4033
  if (UserCancelledError.is(e)) return e;
3913
4034
  return new CLIError({
@@ -3919,7 +4040,7 @@ async function addHandlerInternal(input) {
3919
4040
  if (setupResult.isErr()) return Result.err(setupResult.error);
3920
4041
  await updateBtsConfig(projectDir, {
3921
4042
  addons: updatedAddons,
3922
- addonOptions: config.addonOptions
4043
+ addonOptions: updatedConfig.addonOptions
3923
4044
  });
3924
4045
  if (input.install) {
3925
4046
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
@@ -3942,7 +4063,7 @@ async function addHandlerInternal(input) {
3942
4063
  }
3943
4064
  //#endregion
3944
4065
  //#region src/prompts/api.ts
3945
- async function getApiChoice(Api, frontend, backend) {
4066
+ async function getApiChoice(Api, frontend, backend, previousValue) {
3946
4067
  if (backend === "convex" || backend === "none") return "none";
3947
4068
  const allowed = allowedApisForFrontends(frontend ?? []);
3948
4069
  if (Api) {
@@ -3966,7 +4087,7 @@ async function getApiChoice(Api, frontend, backend) {
3966
4087
  const apiType = await navigableSelect({
3967
4088
  message: "Select API type",
3968
4089
  options: apiOptions,
3969
- initialValue: apiOptions[0].value
4090
+ initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
3970
4091
  });
3971
4092
  if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
3972
4093
  return apiType;
@@ -3992,7 +4113,7 @@ function getAvailableAuthProviders(backend, frontend = []) {
3992
4113
  if (options.length === 0) return ["none"];
3993
4114
  return [...options, "none"];
3994
4115
  }
3995
- async function getAuthChoice(auth, backend, frontend = []) {
4116
+ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
3996
4117
  if (auth !== void 0) return auth;
3997
4118
  const availableProviders = getAvailableAuthProviders(backend, frontend);
3998
4119
  if (availableProviders.length === 1 && availableProviders[0] === "none") return "none";
@@ -4018,7 +4139,7 @@ async function getAuthChoice(auth, backend, frontend = []) {
4018
4139
  const response = await navigableSelect({
4019
4140
  message: "Select authentication provider",
4020
4141
  options,
4021
- initialValue: options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none"
4142
+ initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4022
4143
  });
4023
4144
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4024
4145
  return response;
@@ -4032,7 +4153,7 @@ const FULLSTACK_FRONTENDS = [
4032
4153
  "svelte",
4033
4154
  "astro"
4034
4155
  ];
4035
- async function getBackendFrameworkChoice(backendFramework, frontends) {
4156
+ async function getBackendFrameworkChoice(backendFramework, frontends, previousValue) {
4036
4157
  if (backendFramework !== void 0) return backendFramework;
4037
4158
  const hasIncompatibleFrontend = frontends?.some((f) => f === "solid" || f === "astro");
4038
4159
  const hasFullstackFrontend = frontends?.some((f) => FULLSTACK_FRONTENDS.includes(f));
@@ -4072,14 +4193,14 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
4072
4193
  const response = await navigableSelect({
4073
4194
  message: "Select backend",
4074
4195
  options: backendOptions,
4075
- initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
4196
+ initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4076
4197
  });
4077
4198
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4078
4199
  return response;
4079
4200
  }
4080
4201
  //#endregion
4081
4202
  //#region src/prompts/database.ts
4082
- async function getDatabaseChoice(database, backend, runtime) {
4203
+ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4083
4204
  if (backend === "convex" || backend === "none") return "none";
4084
4205
  if (database !== void 0) return database;
4085
4206
  const databaseOptions = [
@@ -4112,14 +4233,14 @@ async function getDatabaseChoice(database, backend, runtime) {
4112
4233
  const response = await navigableSelect({
4113
4234
  message: "Select database",
4114
4235
  options: databaseOptions,
4115
- initialValue: DEFAULT_CONFIG.database
4236
+ initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4116
4237
  });
4117
4238
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4118
4239
  return response;
4119
4240
  }
4120
4241
  //#endregion
4121
4242
  //#region src/prompts/database-setup.ts
4122
- async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
4243
+ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, previousValue) {
4123
4244
  if (backend === "convex") return "none";
4124
4245
  if (dbSetup !== void 0) return dbSetup;
4125
4246
  if (databaseType === "none") return "none";
@@ -4211,14 +4332,14 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
4211
4332
  const response = await navigableSelect({
4212
4333
  message: `Select ${databaseType} setup option`,
4213
4334
  options,
4214
- initialValue: "none"
4335
+ initialValue: preferValidInitial(options, previousValue, "none")
4215
4336
  });
4216
4337
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4217
4338
  return response;
4218
4339
  }
4219
4340
  //#endregion
4220
4341
  //#region src/prompts/examples.ts
4221
- async function getExamplesChoice(examples, database, frontends, backend, api) {
4342
+ async function getExamplesChoice(examples, database, frontends, backend, api, previousValue) {
4222
4343
  if (examples !== void 0) return examples;
4223
4344
  if (backend === "none") return [];
4224
4345
  let response = [];
@@ -4238,15 +4359,27 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
4238
4359
  message: "Include examples",
4239
4360
  options,
4240
4361
  required: false,
4241
- initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
4362
+ initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
4242
4363
  });
4243
4364
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4244
4365
  return response;
4245
4366
  }
4246
4367
  //#endregion
4247
4368
  //#region src/prompts/frontend.ts
4248
- async function getFrontendChoice(frontendOptions, backend, auth) {
4369
+ const WEB_FRONTEND_VALUES = [
4370
+ "tanstack-router",
4371
+ "react-router",
4372
+ "next",
4373
+ "nuxt",
4374
+ "svelte",
4375
+ "solid",
4376
+ "astro",
4377
+ "tanstack-start"
4378
+ ];
4379
+ async function getFrontendChoice(frontendOptions, backend, auth, previousValue) {
4249
4380
  if (frontendOptions !== void 0) return frontendOptions;
4381
+ const previousWeb = previousValue?.find((f) => WEB_FRONTEND_VALUES.includes(f));
4382
+ const previousNative = previousValue?.find((f) => f.startsWith("native-"));
4250
4383
  while (true) {
4251
4384
  const wasFirstPrompt = isFirstPrompt();
4252
4385
  const frontendTypes = await navigableMultiselect({
@@ -4261,7 +4394,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4261
4394
  hint: "Create a React Native/Expo app"
4262
4395
  }],
4263
4396
  required: false,
4264
- initialValues: ["web"]
4397
+ initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
4265
4398
  });
4266
4399
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
4267
4400
  if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -4269,51 +4402,52 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4269
4402
  const result = [];
4270
4403
  let shouldRestart = false;
4271
4404
  if (frontendTypes.includes("web")) {
4405
+ const webOptions = [
4406
+ {
4407
+ value: "tanstack-router",
4408
+ label: "TanStack Router",
4409
+ hint: "Modern and scalable routing for React Applications"
4410
+ },
4411
+ {
4412
+ value: "react-router",
4413
+ label: "React Router",
4414
+ hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
4415
+ },
4416
+ {
4417
+ value: "next",
4418
+ label: "Next.js",
4419
+ hint: "The React Framework for the Web"
4420
+ },
4421
+ {
4422
+ value: "nuxt",
4423
+ label: "Nuxt",
4424
+ hint: "The Progressive Web Framework for Vue.js"
4425
+ },
4426
+ {
4427
+ value: "svelte",
4428
+ label: "Svelte",
4429
+ hint: "web development for the rest of us"
4430
+ },
4431
+ {
4432
+ value: "solid",
4433
+ label: "Solid",
4434
+ hint: "Simple and performant reactivity for building user interfaces"
4435
+ },
4436
+ {
4437
+ value: "astro",
4438
+ label: "Astro",
4439
+ hint: "The web framework for content-driven websites"
4440
+ },
4441
+ {
4442
+ value: "tanstack-start",
4443
+ label: "TanStack Start",
4444
+ hint: "SSR, Server Functions, API Routes and more with TanStack Router"
4445
+ }
4446
+ ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
4272
4447
  const webFramework = await navigableSelect({
4273
4448
  message: "Choose web",
4274
- options: [
4275
- {
4276
- value: "tanstack-router",
4277
- label: "TanStack Router",
4278
- hint: "Modern and scalable routing for React Applications"
4279
- },
4280
- {
4281
- value: "react-router",
4282
- label: "React Router",
4283
- hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
4284
- },
4285
- {
4286
- value: "next",
4287
- label: "Next.js",
4288
- hint: "The React Framework for the Web"
4289
- },
4290
- {
4291
- value: "nuxt",
4292
- label: "Nuxt",
4293
- hint: "The Progressive Web Framework for Vue.js"
4294
- },
4295
- {
4296
- value: "svelte",
4297
- label: "Svelte",
4298
- hint: "web development for the rest of us"
4299
- },
4300
- {
4301
- value: "solid",
4302
- label: "Solid",
4303
- hint: "Simple and performant reactivity for building user interfaces"
4304
- },
4305
- {
4306
- value: "astro",
4307
- label: "Astro",
4308
- hint: "The web framework for content-driven websites"
4309
- },
4310
- {
4311
- value: "tanstack-start",
4312
- label: "TanStack Start",
4313
- hint: "SSR, Server Functions, API Routes and more with TanStack Router"
4314
- }
4315
- ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
4316
- initialValue: DEFAULT_CONFIG.frontend[0]
4449
+ options: webOptions,
4450
+ initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4317
4451
  });
4318
4452
  if (isGoBack(webFramework)) shouldRestart = true;
4319
4453
  else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -4343,7 +4477,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4343
4477
  hint: "Consistent styling for React Native"
4344
4478
  }
4345
4479
  ],
4346
- initialValue: "native-bare"
4480
+ initialValue: previousNative ?? "native-bare"
4347
4481
  });
4348
4482
  if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
4349
4483
  else {
@@ -4362,22 +4496,22 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4362
4496
  }
4363
4497
  //#endregion
4364
4498
  //#region src/prompts/git.ts
4365
- async function getGitChoice(git) {
4499
+ async function getGitChoice(git, previousValue) {
4366
4500
  if (git !== void 0) return git;
4367
4501
  const response = await navigableConfirm({
4368
4502
  message: "Initialize git repository?",
4369
- initialValue: DEFAULT_CONFIG.git
4503
+ initialValue: previousValue ?? DEFAULT_CONFIG.git
4370
4504
  });
4371
4505
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4372
4506
  return response;
4373
4507
  }
4374
4508
  //#endregion
4375
4509
  //#region src/prompts/install.ts
4376
- async function getinstallChoice(install) {
4510
+ async function getinstallChoice(install, previousValue) {
4377
4511
  if (install !== void 0) return install;
4378
4512
  const response = await navigableConfirm({
4379
4513
  message: "Install dependencies?",
4380
- initialValue: DEFAULT_CONFIG.install
4514
+ initialValue: previousValue ?? DEFAULT_CONFIG.install
4381
4515
  });
4382
4516
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4383
4517
  return response;
@@ -4561,6 +4695,7 @@ function validateFullConfig(config, providedFlags, options) {
4561
4695
  yield* validateFrontendConstraints(config, providedFlags);
4562
4696
  yield* validateApiConstraints(config, options);
4563
4697
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
4698
+ yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
4564
4699
  yield* validateSelfBackendCompatibility(providedFlags, options, config);
4565
4700
  yield* validateWorkersCompatibility(providedFlags, options, config);
4566
4701
  if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
@@ -4604,7 +4739,7 @@ const ormOptions = {
4604
4739
  hint: "Lightweight and performant TypeScript ORM"
4605
4740
  }
4606
4741
  };
4607
- async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
4742
+ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previousValue) {
4608
4743
  if (backend === "convex") return "none";
4609
4744
  if (!hasDatabase) return "none";
4610
4745
  if (orm !== void 0) {
@@ -4612,18 +4747,20 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
4612
4747
  if (compat.isErr()) throw compat.error;
4613
4748
  return orm;
4614
4749
  }
4750
+ const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
4615
4751
  const response = await navigableSelect({
4616
4752
  message: "Select ORM",
4617
- options: database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma],
4618
- initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
4753
+ options,
4754
+ initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4619
4755
  });
4620
4756
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4621
4757
  return response;
4622
4758
  }
4623
4759
  //#endregion
4624
4760
  //#region src/prompts/package-manager.ts
4625
- async function getPackageManagerChoice(packageManager) {
4761
+ async function getPackageManagerChoice(packageManager, previousValue) {
4626
4762
  if (packageManager !== void 0) return packageManager;
4763
+ const detectedPackageManager = getUserPkgManager();
4627
4764
  const response = await navigableSelect({
4628
4765
  message: "Choose package manager",
4629
4766
  options: [
@@ -4643,36 +4780,37 @@ async function getPackageManagerChoice(packageManager) {
4643
4780
  hint: "All-in-one JavaScript runtime & toolkit"
4644
4781
  }
4645
4782
  ],
4646
- initialValue: getUserPkgManager()
4783
+ initialValue: previousValue ?? detectedPackageManager
4647
4784
  });
4648
4785
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4649
4786
  return response;
4650
4787
  }
4651
4788
  //#endregion
4652
4789
  //#region src/prompts/payments.ts
4653
- async function getPaymentsChoice(payments, auth, backend, _frontends) {
4790
+ async function getPaymentsChoice(payments, auth, backend, _frontends, previousValue) {
4654
4791
  if (payments !== void 0) return payments;
4655
4792
  if (backend === "none") return "none";
4656
4793
  if (!(auth === "better-auth")) return "none";
4794
+ const options = [{
4795
+ value: "polar",
4796
+ label: "Polar",
4797
+ hint: "Turn your software into a business. 6 lines of code."
4798
+ }, {
4799
+ value: "none",
4800
+ label: "None",
4801
+ hint: "No payments integration"
4802
+ }];
4657
4803
  const response = await navigableSelect({
4658
4804
  message: "Select payments provider",
4659
- options: [{
4660
- value: "polar",
4661
- label: "Polar",
4662
- hint: "Turn your software into a business. 6 lines of code."
4663
- }, {
4664
- value: "none",
4665
- label: "None",
4666
- hint: "No payments integration"
4667
- }],
4668
- initialValue: DEFAULT_CONFIG.payments
4805
+ options,
4806
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
4669
4807
  });
4670
4808
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4671
4809
  return response;
4672
4810
  }
4673
4811
  //#endregion
4674
4812
  //#region src/prompts/runtime.ts
4675
- async function getRuntimeChoice(runtime, backend) {
4813
+ async function getRuntimeChoice(runtime, backend, previousValue) {
4676
4814
  if (backend === "convex" || backend === "none" || backend === "self") return "none";
4677
4815
  if (runtime !== void 0) return runtime;
4678
4816
  const runtimeOptions = [{
@@ -4692,19 +4830,56 @@ async function getRuntimeChoice(runtime, backend) {
4692
4830
  const response = await navigableSelect({
4693
4831
  message: "Select runtime",
4694
4832
  options: runtimeOptions,
4695
- initialValue: DEFAULT_CONFIG.runtime
4833
+ initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4696
4834
  });
4697
4835
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4698
4836
  return response;
4699
4837
  }
4700
4838
  //#endregion
4701
4839
  //#region src/prompts/server-deploy.ts
4702
- async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
4840
+ const SERVER_APP_BACKENDS = [
4841
+ "hono",
4842
+ "express",
4843
+ "fastify",
4844
+ "elysia"
4845
+ ];
4846
+ function getDeploymentDisplay$1(deployment) {
4847
+ if (deployment === "cloudflare") return {
4848
+ label: "Cloudflare",
4849
+ hint: "Deploy to Cloudflare Workers using Alchemy"
4850
+ };
4851
+ if (deployment === "docker") return {
4852
+ label: "Docker",
4853
+ hint: "Self-host with a Dockerfile and docker-compose.yml"
4854
+ };
4855
+ return {
4856
+ label: deployment,
4857
+ hint: `Add ${deployment} deployment`
4858
+ };
4859
+ }
4860
+ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy, previousValue) {
4703
4861
  if (deployment !== void 0) return deployment;
4704
- if (backend === "none" || backend === "convex") return "none";
4705
- if (backend !== "hono") return "none";
4862
+ if (!backend || !SERVER_APP_BACKENDS.includes(backend)) return "none";
4706
4863
  if (runtime === "workers") return "cloudflare";
4707
- return "none";
4864
+ if (runtime !== "bun" && runtime !== "node") return "none";
4865
+ const options = ["docker", "none"].map((deploy) => {
4866
+ const { label, hint } = deploy === "none" ? {
4867
+ label: "None",
4868
+ hint: "Skip deployment setup"
4869
+ } : getDeploymentDisplay$1(deploy);
4870
+ return {
4871
+ value: deploy,
4872
+ label,
4873
+ hint
4874
+ };
4875
+ });
4876
+ const response = await navigableSelect({
4877
+ message: "Select server deployment",
4878
+ options,
4879
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
4880
+ });
4881
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4882
+ return response;
4708
4883
  }
4709
4884
  //#endregion
4710
4885
  //#region src/prompts/web-deploy.ts
@@ -4716,26 +4891,35 @@ function getDeploymentDisplay(deployment) {
4716
4891
  label: "Cloudflare",
4717
4892
  hint: "Deploy to Cloudflare Workers using Alchemy"
4718
4893
  };
4894
+ if (deployment === "docker") return {
4895
+ label: "Docker",
4896
+ hint: "Self-host with a Dockerfile and docker-compose.yml"
4897
+ };
4719
4898
  return {
4720
4899
  label: deployment,
4721
4900
  hint: `Add ${deployment} deployment`
4722
4901
  };
4723
4902
  }
4724
- async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup) {
4903
+ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, previousValue) {
4725
4904
  if (deployment !== void 0) return deployment;
4726
4905
  if (!hasWebFrontend(frontend)) return "none";
4727
4906
  if (backend === "self" && dbSetup === "d1") return "cloudflare";
4907
+ const options = [
4908
+ "cloudflare",
4909
+ "docker",
4910
+ "none"
4911
+ ].map((deploy) => {
4912
+ const { label, hint } = getDeploymentDisplay(deploy);
4913
+ return {
4914
+ value: deploy,
4915
+ label,
4916
+ hint
4917
+ };
4918
+ });
4728
4919
  const response = await navigableSelect({
4729
4920
  message: "Select web deployment",
4730
- options: ["cloudflare", "none"].map((deploy) => {
4731
- const { label, hint } = getDeploymentDisplay(deploy);
4732
- return {
4733
- value: deploy,
4734
- label,
4735
- hint
4736
- };
4737
- }),
4738
- initialValue: DEFAULT_CONFIG.webDeploy
4921
+ options,
4922
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
4739
4923
  });
4740
4924
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4741
4925
  return response;
@@ -4767,22 +4951,22 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
4767
4951
  serverDeploy: flags.serverDeploy ?? DEFAULT_CONFIG.serverDeploy
4768
4952
  };
4769
4953
  const result = await navigableGroup({
4770
- frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
4771
- backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
4772
- runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
4773
- database: ({ results }) => getDatabaseChoice(flags.database, results.backend, results.runtime),
4774
- orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
4775
- api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
4776
- auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
4777
- payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
4778
- addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
4779
- examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
4780
- dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
4781
- webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
4782
- serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
4783
- git: () => getGitChoice(flags.git),
4784
- packageManager: () => getPackageManagerChoice(flags.packageManager),
4785
- install: () => getinstallChoice(flags.install)
4954
+ frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
4955
+ backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
4956
+ runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
4957
+ database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
4958
+ orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
4959
+ api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
4960
+ auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
4961
+ payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
4962
+ addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
4963
+ examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
4964
+ dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
4965
+ webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
4966
+ serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
4967
+ git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
4968
+ packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
4969
+ install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
4786
4970
  }, { onCancel: () => {
4787
4971
  throw new UserCancelledError({ message: "Operation cancelled" });
4788
4972
  } });
@@ -6713,12 +6897,15 @@ async function displayPostInstallInstructions(config) {
6713
6897
  const cdCmd = `cd ${relativePath}`;
6714
6898
  const hasHusky = addons?.includes("husky");
6715
6899
  const hasLefthook = addons?.includes("lefthook");
6716
- const hasGitHooksOrLinting = addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook") || addons?.includes("oxlint");
6900
+ const hasVitePlus = addons?.includes("vite-plus");
6901
+ const hasVitePlusNativeHooks = hasVitePlus && !hasHusky && !hasLefthook;
6902
+ const hasGitHooksOrLinting = addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook") || addons?.includes("oxlint") || hasVitePlus;
6717
6903
  const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, webDeploy, serverDeploy, backend) : "";
6718
6904
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd, frontend) : "";
6719
6905
  const electrobunInstructions = addons?.includes("electrobun") ? getElectrobunInstructions(runCmd, frontend) : "";
6720
6906
  const huskyInstructions = hasHusky ? getHuskyInstructions(runCmd) : "";
6721
6907
  const lefthookInstructions = hasLefthook ? getLefthookInstructions(packageManager) : "";
6908
+ const vitePlusNativeHooksInstructions = hasVitePlusNativeHooks ? getVitePlusNativeHooksInstructions(runCmd) : "";
6722
6909
  const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
6723
6910
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
6724
6911
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
@@ -6774,6 +6961,7 @@ async function displayPostInstallInstructions(config) {
6774
6961
  if (electrobunInstructions) output += `\n${electrobunInstructions.trim()}\n`;
6775
6962
  if (huskyInstructions) output += `\n${huskyInstructions.trim()}\n`;
6776
6963
  if (lefthookInstructions) output += `\n${lefthookInstructions.trim()}\n`;
6964
+ if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
6777
6965
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
6778
6966
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
6779
6967
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
@@ -6805,12 +6993,15 @@ function getHuskyInstructions(runCmd) {
6805
6993
  return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
6806
6994
  }
6807
6995
  function getLintingInstructions(runCmd) {
6808
- return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
6996
+ return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
6809
6997
  }
6810
6998
  function getLefthookInstructions(packageManager) {
6811
6999
  const cmd = packageManager === "npm" ? "npx" : packageManager;
6812
7000
  return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
6813
7001
  }
7002
+ function getVitePlusNativeHooksInstructions(runCmd) {
7003
+ return `${pc.bold("Vite+ native Git hooks:")}\n${pc.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
7004
+ }
6814
7005
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
6815
7006
  const instructions = [];
6816
7007
  const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
@@ -6947,6 +7138,10 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
6947
7138
  if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6948
7139
  else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6949
7140
  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`}`);
7141
+ if (webDeploy === "docker" || serverDeploy === "docker") {
7142
+ const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
7143
+ 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`);
7144
+ }
6950
7145
  return instructions.length ? `\n${instructions.join("\n")}` : "";
6951
7146
  }
6952
7147
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.31.1",
3
+ "version": "3.33.0",
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,10 +69,10 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.31.1",
73
- "@better-t-stack/types": "^3.31.1",
74
- "@clack/core": "^1.3.1",
75
- "@clack/prompts": "^1.4.0",
72
+ "@better-t-stack/template-generator": "^3.33.0",
73
+ "@better-t-stack/types": "^3.33.0",
74
+ "@clack/core": "^1.4.1",
75
+ "@clack/prompts": "^1.5.1",
76
76
  "@modelcontextprotocol/sdk": "1.29.0",
77
77
  "@trpc/server": "^11.17.0",
78
78
  "better-result": "^2.9.2",