create-better-t-stack 3.31.0 → 3.32.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
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-BZV4gWGl.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;
@@ -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;
@@ -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";
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-BZV4gWGl.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",
@@ -887,12 +893,13 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
887
893
  return allAddons.filter((addon) => {
888
894
  if (existingAddons.includes(addon)) return false;
889
895
  if (addon === "none") return false;
896
+ if (TASK_RUNNER_ADDONS$1.includes(addon) && existingAddons.some((existingAddon) => TASK_RUNNER_ADDONS$1.includes(existingAddon))) return false;
890
897
  const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
891
898
  return isCompatible;
892
899
  });
893
900
  }
894
901
  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.");
902
+ 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
903
  for (const addon of addons) {
897
904
  if (addon === "none") continue;
898
905
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -1178,6 +1185,10 @@ function getAddonDisplay(addon) {
1178
1185
  label = "Nx";
1179
1186
  hint = "Smart monorepo orchestration and task graph";
1180
1187
  break;
1188
+ case "vite-plus":
1189
+ label = "Vite+";
1190
+ hint = "Unified Vite toolchain and workspace task runner";
1191
+ break;
1181
1192
  case "pwa":
1182
1193
  label = "PWA";
1183
1194
  hint = "Make your app installable and work offline";
@@ -1248,7 +1259,11 @@ function getAddonDisplay(addon) {
1248
1259
  };
1249
1260
  }
1250
1261
  const ADDON_GROUPS = {
1251
- "Monorepo & Tasks": ["turborepo", "nx"],
1262
+ "Monorepo & Tasks": [
1263
+ "turborepo",
1264
+ "nx",
1265
+ "vite-plus"
1266
+ ],
1252
1267
  "Code Quality": [
1253
1268
  "biome",
1254
1269
  "oxlint",
@@ -1289,7 +1304,11 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1289
1304
  });
1290
1305
  }
1291
1306
  function validateAddonSelection(selected) {
1292
- if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
1307
+ if ((selected?.filter((addon) => [
1308
+ "turborepo",
1309
+ "nx",
1310
+ "vite-plus"
1311
+ ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1293
1312
  }
1294
1313
  async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1295
1314
  if (addons !== void 0) return addons;
@@ -2142,6 +2161,14 @@ const DEFAULT_DEV_PORT$1 = 4e3;
2142
2161
  function aiChatDisabledForTemplate(template) {
2143
2162
  return template === "next-mdx-static" || template.endsWith("-spa");
2144
2163
  }
2164
+ function getFumadocsLinter(addons) {
2165
+ if (addons.includes("oxlint")) return "oxlint";
2166
+ if (addons.includes("biome") || addons.includes("ultracite")) return "biome";
2167
+ if (addons.includes("vite-plus")) return "oxlint";
2168
+ }
2169
+ function getFumadocsAddonContext(currentAddons, persistedAddons) {
2170
+ return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
2171
+ }
2145
2172
  async function setupFumadocs(config) {
2146
2173
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2147
2174
  const { packageManager, projectDir } = config;
@@ -2215,6 +2242,10 @@ async function setupFumadocs(config) {
2215
2242
  label: "AI SDK",
2216
2243
  hint: "default to OpenRouter"
2217
2244
  },
2245
+ {
2246
+ value: "llmgateway",
2247
+ label: "LLM Gateway"
2248
+ },
2218
2249
  {
2219
2250
  value: "inkeep",
2220
2251
  label: "Inkeep AI",
@@ -2251,7 +2282,9 @@ async function setupFumadocs(config) {
2251
2282
  "--no-git"
2252
2283
  ];
2253
2284
  if (isNextTemplate) options.push("--src");
2254
- if (config.addons.includes("biome") || config.addons.includes("ultracite")) options.push("--linter biome");
2285
+ const persistedConfig = await readBtsConfig(projectDir);
2286
+ const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
2287
+ if (linter) options.push(`--linter ${linter}`);
2255
2288
  if (search) options.push(`--search ${search}`);
2256
2289
  if (ogImage) options.push(`--og-image ${ogImage}`);
2257
2290
  if (aiChat) options.push(`--ai-chat ${aiChat}`);
@@ -3630,6 +3663,7 @@ async function setupAddons(config) {
3630
3663
  const hasHusky = addons.includes("husky");
3631
3664
  const hasLefthook = addons.includes("lefthook");
3632
3665
  const hasOxlint = addons.includes("oxlint");
3666
+ const hasVitePlus = addons.includes("vite-plus");
3633
3667
  if (hasUltracite) {
3634
3668
  const gitHooks = [];
3635
3669
  if (hasHusky) gitHooks.push("husky");
@@ -3642,6 +3676,7 @@ async function setupAddons(config) {
3642
3676
  let linter;
3643
3677
  if (hasOxlint) linter = "oxlint";
3644
3678
  else if (hasBiome) linter = "biome";
3679
+ else if (hasVitePlus) linter = "vite-plus";
3645
3680
  if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
3646
3681
  if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3647
3682
  }
@@ -3683,6 +3718,7 @@ async function setupHusky(projectDir, linter) {
3683
3718
  };
3684
3719
  if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
3685
3720
  else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
3721
+ else if (linter === "vite-plus") packageJson["lint-staged"] = { "*.{js,ts,jsx,tsx,vue,svelte,json,jsonc,css,md}": ["vp check --fix"] };
3686
3722
  else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
3687
3723
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3688
3724
  }
@@ -3750,6 +3786,35 @@ async function installDependencies({ projectDir, packageManager }) {
3750
3786
  }
3751
3787
  //#endregion
3752
3788
  //#region src/helpers/core/add-handler.ts
3789
+ const ADD_PACKAGE_JSON_PATHS = [
3790
+ "package.json",
3791
+ "apps/server/package.json",
3792
+ "apps/web/package.json",
3793
+ "apps/native/package.json",
3794
+ "apps/desktop/package.json",
3795
+ "apps/fumadocs/package.json",
3796
+ "apps/docs/package.json",
3797
+ "packages/api/package.json",
3798
+ "packages/db/package.json",
3799
+ "packages/auth/package.json",
3800
+ "packages/backend/package.json",
3801
+ "packages/config/package.json",
3802
+ "packages/env/package.json",
3803
+ "packages/infra/package.json",
3804
+ "packages/ui/package.json"
3805
+ ];
3806
+ const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
3807
+ const HOOK_ADDONS = ["husky", "lefthook"];
3808
+ const HOOK_LINTER_ADDONS = [
3809
+ "biome",
3810
+ "oxlint",
3811
+ "vite-plus"
3812
+ ];
3813
+ const TASK_RUNNER_ADDONS = [
3814
+ "turborepo",
3815
+ "nx",
3816
+ "vite-plus"
3817
+ ];
3753
3818
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3754
3819
  if (!existingAddonOptions && !nextAddonOptions) return;
3755
3820
  const mergedAddonOptions = { ...existingAddonOptions };
@@ -3763,6 +3828,29 @@ function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3763
3828
  }
3764
3829
  return Object.keys(mergedAddonOptions).length > 0 ? mergedAddonOptions : void 0;
3765
3830
  }
3831
+ function getSetupAddons(addonsToAdd, updatedAddons) {
3832
+ const setupAddons = new Set(addonsToAdd);
3833
+ if (addonsToAdd.some((addon) => HOOK_ADDONS.includes(addon) || HOOK_LINTER_ADDONS.includes(addon))) {
3834
+ for (const addon of [...HOOK_ADDONS, ...HOOK_LINTER_ADDONS]) if (updatedAddons.includes(addon)) setupAddons.add(addon);
3835
+ }
3836
+ return [...setupAddons];
3837
+ }
3838
+ function shouldRefreshLefthook(addonsToAdd, updatedAddons) {
3839
+ return updatedAddons.includes("lefthook") && addonsToAdd.some((addon) => addon === "lefthook" || HOOK_LINTER_ADDONS.includes(addon));
3840
+ }
3841
+ function refreshLefthookTemplate(vfs, config) {
3842
+ const template = EMBEDDED_TEMPLATES.get("addons/lefthook/lefthook.yml.hbs");
3843
+ if (!template) return;
3844
+ vfs.writeFile("lefthook.yml", processTemplateString(template, config));
3845
+ }
3846
+ function updateViteConfigImportsForVitePlus(vfs) {
3847
+ for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
3848
+ const content = vfs.readFile(viteConfigPath);
3849
+ if (!content) continue;
3850
+ const updatedContent = content.replaceAll("from \"vite\";", "from \"vite-plus\";").replaceAll("from 'vite';", "from 'vite-plus';");
3851
+ if (updatedContent !== content) vfs.writeFile(viteConfigPath, updatedContent);
3852
+ }
3853
+ }
3766
3854
  async function addHandler(input, options = {}) {
3767
3855
  const { silent = false } = options;
3768
3856
  return runWithContextAsync({ silent }, async () => {
@@ -3840,10 +3928,10 @@ async function addHandlerInternal(input) {
3840
3928
  }
3841
3929
  addonsToAdd = selectedAddons;
3842
3930
  }
3843
- const addonsValidationResult = validateAddonsAgainstConfig(addonsToAdd, existingConfig);
3931
+ const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3932
+ const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3844
3933
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3845
3934
  if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
3846
- const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3847
3935
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
3848
3936
  const config = {
3849
3937
  projectName: existingConfig.projectName,
@@ -3867,27 +3955,39 @@ async function addHandlerInternal(input) {
3867
3955
  webDeploy: existingConfig.webDeploy,
3868
3956
  serverDeploy: existingConfig.serverDeploy
3869
3957
  };
3958
+ const updatedConfig = {
3959
+ ...config,
3960
+ addons: updatedAddons
3961
+ };
3870
3962
  if (!isSilent()) log.info(pc.dim("Installing addon files..."));
3871
3963
  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
- ]) {
3964
+ for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
3878
3965
  const fullPath = path.join(projectDir, pkgPath);
3879
3966
  if (await fs.pathExists(fullPath)) {
3880
3967
  const content = await fs.readFile(fullPath, "utf-8");
3881
3968
  vfs.writeFile(pkgPath, content);
3882
3969
  }
3883
3970
  }
3971
+ for (const filePath of ADD_TEXT_FILE_PATHS) {
3972
+ const fullPath = path.join(projectDir, filePath);
3973
+ if (await fs.pathExists(fullPath)) {
3974
+ const content = await fs.readFile(fullPath, "utf-8");
3975
+ vfs.writeFile(filePath, content);
3976
+ }
3977
+ }
3884
3978
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
3885
3979
  processAddonsDeps(vfs, config);
3980
+ if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
3981
+ if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
3982
+ if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
3983
+ if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
3984
+ if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
3985
+ if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
3886
3986
  const tree = {
3887
3987
  root: vfs.toTree(""),
3888
3988
  fileCount: vfs.getFileCount(),
3889
3989
  directoryCount: vfs.getDirectoryCount(),
3890
- config
3990
+ config: updatedConfig
3891
3991
  };
3892
3992
  if (input.dryRun) {
3893
3993
  if (!isSilent()) {
@@ -3907,7 +4007,10 @@ async function addHandlerInternal(input) {
3907
4007
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
3908
4008
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
3909
4009
  const setupResult = await Result.tryPromise({
3910
- try: () => setupAddons(config),
4010
+ try: () => setupAddons({
4011
+ ...config,
4012
+ addons: getSetupAddons(addonsToAdd, updatedAddons)
4013
+ }),
3911
4014
  catch: (e) => {
3912
4015
  if (UserCancelledError.is(e)) return e;
3913
4016
  return new CLIError({
@@ -3919,7 +4022,7 @@ async function addHandlerInternal(input) {
3919
4022
  if (setupResult.isErr()) return Result.err(setupResult.error);
3920
4023
  await updateBtsConfig(projectDir, {
3921
4024
  addons: updatedAddons,
3922
- addonOptions: config.addonOptions
4025
+ addonOptions: updatedConfig.addonOptions
3923
4026
  });
3924
4027
  if (input.install) {
3925
4028
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
@@ -6713,12 +6816,15 @@ async function displayPostInstallInstructions(config) {
6713
6816
  const cdCmd = `cd ${relativePath}`;
6714
6817
  const hasHusky = addons?.includes("husky");
6715
6818
  const hasLefthook = addons?.includes("lefthook");
6716
- const hasGitHooksOrLinting = addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook") || addons?.includes("oxlint");
6819
+ const hasVitePlus = addons?.includes("vite-plus");
6820
+ const hasVitePlusNativeHooks = hasVitePlus && !hasHusky && !hasLefthook;
6821
+ const hasGitHooksOrLinting = addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook") || addons?.includes("oxlint") || hasVitePlus;
6717
6822
  const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, webDeploy, serverDeploy, backend) : "";
6718
6823
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd, frontend) : "";
6719
6824
  const electrobunInstructions = addons?.includes("electrobun") ? getElectrobunInstructions(runCmd, frontend) : "";
6720
6825
  const huskyInstructions = hasHusky ? getHuskyInstructions(runCmd) : "";
6721
6826
  const lefthookInstructions = hasLefthook ? getLefthookInstructions(packageManager) : "";
6827
+ const vitePlusNativeHooksInstructions = hasVitePlusNativeHooks ? getVitePlusNativeHooksInstructions(runCmd) : "";
6722
6828
  const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
6723
6829
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
6724
6830
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
@@ -6774,6 +6880,7 @@ async function displayPostInstallInstructions(config) {
6774
6880
  if (electrobunInstructions) output += `\n${electrobunInstructions.trim()}\n`;
6775
6881
  if (huskyInstructions) output += `\n${huskyInstructions.trim()}\n`;
6776
6882
  if (lefthookInstructions) output += `\n${lefthookInstructions.trim()}\n`;
6883
+ if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
6777
6884
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
6778
6885
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
6779
6886
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
@@ -6805,12 +6912,15 @@ function getHuskyInstructions(runCmd) {
6805
6912
  return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
6806
6913
  }
6807
6914
  function getLintingInstructions(runCmd) {
6808
- return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
6915
+ return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
6809
6916
  }
6810
6917
  function getLefthookInstructions(packageManager) {
6811
6918
  const cmd = packageManager === "npm" ? "npx" : packageManager;
6812
6919
  return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
6813
6920
  }
6921
+ function getVitePlusNativeHooksInstructions(runCmd) {
6922
+ 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`;
6923
+ }
6814
6924
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
6815
6925
  const instructions = [];
6816
6926
  const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.31.0",
3
+ "version": "3.32.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,8 +69,8 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.31.0",
73
- "@better-t-stack/types": "^3.31.0",
72
+ "@better-t-stack/template-generator": "^3.32.0",
73
+ "@better-t-stack/types": "^3.32.0",
74
74
  "@clack/core": "^1.3.1",
75
75
  "@clack/prompts": "^1.4.0",
76
76
  "@modelcontextprotocol/sdk": "1.29.0",