create-better-t-stack 2.29.4 → 2.30.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/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
3
- import consola, { consola as consola$1 } from "consola";
4
3
  import pc from "picocolors";
5
4
  import { createCli, trpcServer } from "trpc-cli";
6
5
  import z from "zod";
7
6
  import path from "node:path";
8
7
  import fs from "fs-extra";
9
8
  import { fileURLToPath } from "node:url";
9
+ import consola, { consola as consola$1 } from "consola";
10
10
  import gradient from "gradient-string";
11
11
  import * as JSONC from "jsonc-parser";
12
12
  import { $, execa } from "execa";
13
+ import handlebars from "handlebars";
13
14
  import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
14
15
  import { globby } from "globby";
15
- import handlebars from "handlebars";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region src/utils/get-package-manager.ts
@@ -136,6 +136,7 @@ const ADDON_COMPATIBILITY = {
136
136
  turborepo: [],
137
137
  starlight: [],
138
138
  ultracite: [],
139
+ "vibe-rules": [],
139
140
  oxlint: [],
140
141
  fumadocs: [],
141
142
  none: []
@@ -189,6 +190,7 @@ const AddonsSchema = z.enum([
189
190
  "starlight",
190
191
  "biome",
191
192
  "husky",
193
+ "vibe-rules",
192
194
  "turborepo",
193
195
  "fumadocs",
194
196
  "ultracite",
@@ -258,6 +260,22 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
258
260
  });
259
261
  }
260
262
 
263
+ //#endregion
264
+ //#region src/utils/errors.ts
265
+ function exitWithError(message) {
266
+ consola$1.error(pc.red(message));
267
+ process.exit(1);
268
+ }
269
+ function exitCancelled(message = "Operation cancelled") {
270
+ cancel(pc.red(message));
271
+ process.exit(0);
272
+ }
273
+ function handleError(error, fallbackMessage) {
274
+ const message = error instanceof Error ? error.message : fallbackMessage || String(error);
275
+ consola$1.error(pc.red(message));
276
+ process.exit(1);
277
+ }
278
+
261
279
  //#endregion
262
280
  //#region src/prompts/addons.ts
263
281
  function getAddonDisplay(addon) {
@@ -288,6 +306,10 @@ function getAddonDisplay(addon) {
288
306
  label = "Ultracite";
289
307
  hint = "Zero-config Biome preset with AI integration";
290
308
  break;
309
+ case "vibe-rules":
310
+ label = "vibe-rules";
311
+ hint = "Install and apply BTS rules to editors";
312
+ break;
291
313
  case "husky":
292
314
  label = "Husky";
293
315
  hint = "Modern native Git hooks made easy";
@@ -317,6 +339,7 @@ const ADDON_GROUPS = {
317
339
  "ultracite"
318
340
  ],
319
341
  Other: [
342
+ "vibe-rules",
320
343
  "turborepo",
321
344
  "pwa",
322
345
  "tauri",
@@ -356,10 +379,7 @@ async function getAddonsChoice(addons, frontends) {
356
379
  required: false,
357
380
  selectableGroups: false
358
381
  });
359
- if (isCancel(response)) {
360
- cancel(pc.red("Operation cancelled"));
361
- process.exit(0);
362
- }
382
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
363
383
  return response;
364
384
  }
365
385
  async function getAddonsToAdd(frontend, existingAddons = []) {
@@ -391,10 +411,7 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
391
411
  required: false,
392
412
  selectableGroups: false
393
413
  });
394
- if (isCancel(response)) {
395
- cancel(pc.red("Operation cancelled"));
396
- process.exit(0);
397
- }
414
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
398
415
  return response;
399
416
  }
400
417
 
@@ -425,48 +442,18 @@ function splitFrontends(values = []) {
425
442
  }
426
443
  function ensureSingleWebAndNative(frontends) {
427
444
  const { web, native } = splitFrontends(frontends);
428
- if (web.length > 1) {
429
- consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
430
- process.exit(1);
431
- }
432
- if (native.length > 1) {
433
- consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
434
- process.exit(1);
435
- }
445
+ if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
446
+ if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
436
447
  }
437
448
  function validateWorkersCompatibility(providedFlags, options, config) {
438
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
439
- consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
440
- process.exit(1);
441
- }
442
- if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
443
- consola$1.fatal(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
444
- process.exit(1);
445
- }
446
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
447
- consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
448
- process.exit(1);
449
- }
450
- if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
451
- consola$1.fatal(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
452
- process.exit(1);
453
- }
454
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
455
- consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
456
- process.exit(1);
457
- }
458
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
459
- consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
460
- process.exit(1);
461
- }
462
- if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
463
- consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
464
- process.exit(1);
465
- }
466
- if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") {
467
- consola$1.fatal("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
468
- process.exit(1);
469
- }
449
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
450
+ if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
451
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
452
+ if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") exitWithError(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
453
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
454
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
455
+ if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
456
+ if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
470
457
  }
471
458
  function coerceBackendPresets(config) {
472
459
  if (config.backend === "convex") {
@@ -516,10 +503,7 @@ function validateApiFrontendCompatibility(api, frontends = []) {
516
503
  const includesNuxt = frontends.includes("nuxt");
517
504
  const includesSvelte = frontends.includes("svelte");
518
505
  const includesSolid = frontends.includes("solid");
519
- if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") {
520
- consola$1.fatal(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
521
- process.exit(1);
522
- }
506
+ if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
523
507
  }
524
508
  function isFrontendAllowedWithBackend(frontend, backend) {
525
509
  if (backend === "convex" && frontend === "solid") return false;
@@ -547,10 +531,7 @@ function isExampleAIAllowed(backend, frontends = []) {
547
531
  return true;
548
532
  }
549
533
  function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
550
- if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) {
551
- consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
552
- process.exit(1);
553
- }
534
+ if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
554
535
  }
555
536
 
556
537
  //#endregion
@@ -577,10 +558,7 @@ async function getApiChoice(Api, frontend, backend) {
577
558
  options: apiOptions,
578
559
  initialValue: apiOptions[0].value
579
560
  });
580
- if (isCancel(apiType)) {
581
- cancel(pc.red("Operation cancelled"));
582
- process.exit(0);
583
- }
561
+ if (isCancel(apiType)) return exitCancelled("Operation cancelled");
584
562
  return apiType;
585
563
  }
586
564
 
@@ -594,10 +572,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
594
572
  message: "Add authentication with Better-Auth?",
595
573
  initialValue: DEFAULT_CONFIG.auth
596
574
  });
597
- if (isCancel(response)) {
598
- cancel(pc.red("Operation cancelled"));
599
- process.exit(0);
600
- }
575
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
601
576
  return response;
602
577
  }
603
578
 
@@ -648,10 +623,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
648
623
  options: backendOptions,
649
624
  initialValue: DEFAULT_CONFIG.backend
650
625
  });
651
- if (isCancel(response)) {
652
- cancel(pc.red("Operation cancelled"));
653
- process.exit(0);
654
- }
626
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
655
627
  return response;
656
628
  }
657
629
 
@@ -692,10 +664,7 @@ async function getDatabaseChoice(database, backend, runtime) {
692
664
  options: databaseOptions,
693
665
  initialValue: DEFAULT_CONFIG.database
694
666
  });
695
- if (isCancel(response)) {
696
- cancel(pc.red("Operation cancelled"));
697
- process.exit(0);
698
- }
667
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
699
668
  return response;
700
669
  }
701
670
 
@@ -783,10 +752,7 @@ async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
783
752
  options,
784
753
  initialValue: "none"
785
754
  });
786
- if (isCancel(response)) {
787
- cancel(pc.red("Operation cancelled"));
788
- process.exit(0);
789
- }
755
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
790
756
  return response;
791
757
  }
792
758
 
@@ -819,10 +785,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
819
785
  required: false,
820
786
  initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
821
787
  });
822
- if (isCancel(response)) {
823
- cancel(pc.red("Operation cancelled"));
824
- process.exit(0);
825
- }
788
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
826
789
  return response;
827
790
  }
828
791
 
@@ -844,10 +807,7 @@ async function getFrontendChoice(frontendOptions, backend) {
844
807
  required: false,
845
808
  initialValues: ["web"]
846
809
  });
847
- if (isCancel(frontendTypes)) {
848
- cancel(pc.red("Operation cancelled"));
849
- process.exit(0);
850
- }
810
+ if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
851
811
  const result = [];
852
812
  if (frontendTypes.includes("web")) {
853
813
  const allWebOptions = [
@@ -883,7 +843,7 @@ async function getFrontendChoice(frontendOptions, backend) {
883
843
  },
884
844
  {
885
845
  value: "tanstack-start",
886
- label: "TanStack Start (vite)",
846
+ label: "TanStack Start",
887
847
  hint: "SSR, Server Functions, API Routes and more with TanStack Router"
888
848
  }
889
849
  ];
@@ -893,10 +853,7 @@ async function getFrontendChoice(frontendOptions, backend) {
893
853
  options: webOptions,
894
854
  initialValue: DEFAULT_CONFIG.frontend[0]
895
855
  });
896
- if (isCancel(webFramework)) {
897
- cancel(pc.red("Operation cancelled"));
898
- process.exit(0);
899
- }
856
+ if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
900
857
  result.push(webFramework);
901
858
  }
902
859
  if (frontendTypes.includes("native")) {
@@ -913,10 +870,7 @@ async function getFrontendChoice(frontendOptions, backend) {
913
870
  }],
914
871
  initialValue: "native-nativewind"
915
872
  });
916
- if (isCancel(nativeFramework)) {
917
- cancel(pc.red("Operation cancelled"));
918
- process.exit(0);
919
- }
873
+ if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
920
874
  result.push(nativeFramework);
921
875
  }
922
876
  return result;
@@ -930,10 +884,7 @@ async function getGitChoice(git) {
930
884
  message: "Initialize git repository?",
931
885
  initialValue: DEFAULT_CONFIG.git
932
886
  });
933
- if (isCancel(response)) {
934
- cancel(pc.red("Operation cancelled"));
935
- process.exit(0);
936
- }
887
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
937
888
  return response;
938
889
  }
939
890
 
@@ -945,10 +896,7 @@ async function getinstallChoice(install) {
945
896
  message: "Install dependencies?",
946
897
  initialValue: DEFAULT_CONFIG.install
947
898
  });
948
- if (isCancel(response)) {
949
- cancel(pc.red("Operation cancelled"));
950
- process.exit(0);
951
- }
899
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
952
900
  return response;
953
901
  }
954
902
 
@@ -982,10 +930,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
982
930
  options,
983
931
  initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
984
932
  });
985
- if (isCancel(response)) {
986
- cancel(pc.red("Operation cancelled"));
987
- process.exit(0);
988
- }
933
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
989
934
  return response;
990
935
  }
991
936
 
@@ -1015,10 +960,7 @@ async function getPackageManagerChoice(packageManager) {
1015
960
  ],
1016
961
  initialValue: detectedPackageManager
1017
962
  });
1018
- if (isCancel(response)) {
1019
- cancel(pc.red("Operation cancelled"));
1020
- process.exit(0);
1021
- }
963
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1022
964
  return response;
1023
965
  }
1024
966
 
@@ -1047,10 +989,7 @@ async function getRuntimeChoice(runtime, backend) {
1047
989
  options: runtimeOptions,
1048
990
  initialValue: DEFAULT_CONFIG.runtime
1049
991
  });
1050
- if (isCancel(response)) {
1051
- cancel(pc.red("Operation cancelled"));
1052
- process.exit(0);
1053
- }
992
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1054
993
  return response;
1055
994
  }
1056
995
 
@@ -1086,10 +1025,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
1086
1025
  options,
1087
1026
  initialValue: DEFAULT_CONFIG.webDeploy
1088
1027
  });
1089
- if (isCancel(response)) {
1090
- cancel(pc.red("Operation cancelled"));
1091
- process.exit(0);
1092
- }
1028
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1093
1029
  return response;
1094
1030
  }
1095
1031
  async function getDeploymentToAdd(frontend, existingDeployment) {
@@ -1115,10 +1051,7 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
1115
1051
  options,
1116
1052
  initialValue: DEFAULT_CONFIG.webDeploy
1117
1053
  });
1118
- if (isCancel(response)) {
1119
- cancel(pc.red("Operation cancelled"));
1120
- process.exit(0);
1121
- }
1054
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1122
1055
  return response;
1123
1056
  }
1124
1057
 
@@ -1140,10 +1073,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1140
1073
  git: () => getGitChoice(flags.git),
1141
1074
  packageManager: () => getPackageManagerChoice(flags.packageManager),
1142
1075
  install: () => getinstallChoice(flags.install)
1143
- }, { onCancel: () => {
1144
- cancel(pc.red("Operation cancelled"));
1145
- process.exit(0);
1146
- } });
1076
+ }, { onCancel: () => exitCancelled("Operation cancelled") });
1147
1077
  if (result.backend === "convex") {
1148
1078
  result.runtime = "none";
1149
1079
  result.database = "none";
@@ -1235,10 +1165,7 @@ async function getProjectName(initialName) {
1235
1165
  return void 0;
1236
1166
  }
1237
1167
  });
1238
- if (isCancel(response)) {
1239
- cancel(pc.red("Operation cancelled."));
1240
- process.exit(0);
1241
- }
1168
+ if (isCancel(response)) return exitCancelled("Operation cancelled.");
1242
1169
  projectPath = response || defaultName;
1243
1170
  isValid = true;
1244
1171
  }
@@ -1410,10 +1337,7 @@ async function handleDirectoryConflict(currentPathInput) {
1410
1337
  ],
1411
1338
  initialValue: "rename"
1412
1339
  });
1413
- if (isCancel(action)) {
1414
- cancel(pc.red("Operation cancelled."));
1415
- process.exit(0);
1416
- }
1340
+ if (isCancel(action)) return exitCancelled("Operation cancelled.");
1417
1341
  switch (action) {
1418
1342
  case "overwrite": return {
1419
1343
  finalPathInput: currentPathInput,
@@ -1430,9 +1354,7 @@ async function handleDirectoryConflict(currentPathInput) {
1430
1354
  const newPathInput = await getProjectName(void 0);
1431
1355
  return await handleDirectoryConflict(newPathInput);
1432
1356
  }
1433
- case "cancel":
1434
- cancel(pc.red("Operation cancelled."));
1435
- process.exit(0);
1357
+ case "cancel": return exitCancelled("Operation cancelled.");
1436
1358
  }
1437
1359
  }
1438
1360
  }
@@ -1454,8 +1376,7 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
1454
1376
  s.stop(`Directory "${finalResolvedPath}" cleared.`);
1455
1377
  } catch (error) {
1456
1378
  s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
1457
- consola$1.error(error);
1458
- process.exit(1);
1379
+ handleError(error);
1459
1380
  }
1460
1381
  } else await fs.ensureDir(finalResolvedPath);
1461
1382
  return {
@@ -1515,18 +1436,12 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1515
1436
  if (options.api) {
1516
1437
  config.api = options.api;
1517
1438
  if (options.api === "none") {
1518
- if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") {
1519
- consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1520
- process.exit(1);
1521
- }
1439
+ if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1522
1440
  }
1523
1441
  }
1524
1442
  if (options.backend) config.backend = options.backend;
1525
1443
  if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
1526
- if (providedFlags.has("runtime") && options.runtime === "none") {
1527
- consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1528
- process.exit(1);
1529
- }
1444
+ if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1530
1445
  }
1531
1446
  if (options.database) config.database = options.database;
1532
1447
  if (options.orm) config.orm = options.orm;
@@ -1539,25 +1454,16 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1539
1454
  if (options.webDeploy) config.webDeploy = options.webDeploy;
1540
1455
  if (projectName) {
1541
1456
  const result = ProjectNameSchema.safeParse(path.basename(projectName));
1542
- if (!result.success) {
1543
- consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1544
- process.exit(1);
1545
- }
1457
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1546
1458
  config.projectName = projectName;
1547
1459
  } else if (options.projectDirectory) {
1548
1460
  const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
1549
1461
  const result = ProjectNameSchema.safeParse(baseName);
1550
- if (!result.success) {
1551
- consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1552
- process.exit(1);
1553
- }
1462
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1554
1463
  config.projectName = baseName;
1555
1464
  }
1556
1465
  if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
1557
- if (options.frontend.length > 1) {
1558
- consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
1559
- process.exit(1);
1560
- }
1466
+ if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
1561
1467
  config.frontend = [];
1562
1468
  } else {
1563
1469
  const validOptions = options.frontend.filter((f) => f !== "none");
@@ -1566,17 +1472,11 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1566
1472
  }
1567
1473
  if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
1568
1474
  if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
1569
- if (options.addons.length > 1) {
1570
- consola$1.fatal(`Cannot combine 'none' with other addons.`);
1571
- process.exit(1);
1572
- }
1475
+ if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
1573
1476
  config.addons = [];
1574
1477
  } else config.addons = options.addons.filter((addon) => addon !== "none");
1575
1478
  if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
1576
- if (options.examples.length > 1) {
1577
- consola$1.fatal("Cannot combine 'none' with other examples.");
1578
- process.exit(1);
1579
- }
1479
+ if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
1580
1480
  config.examples = [];
1581
1481
  } else {
1582
1482
  config.examples = options.examples.filter((ex) => ex !== "none");
@@ -1584,89 +1484,35 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1584
1484
  }
1585
1485
  if (config.backend === "convex" || config.backend === "none") {
1586
1486
  const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
1587
- if (incompatibleFlags.length > 0) {
1588
- consola$1.fatal(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1589
- process.exit(1);
1590
- }
1487
+ if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1591
1488
  if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1592
1489
  const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
1593
- if (incompatibleFrontends.length > 0) {
1594
- consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1595
- process.exit(1);
1596
- }
1490
+ if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1597
1491
  }
1598
1492
  coerceBackendPresets(config);
1599
1493
  }
1600
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") {
1601
- consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1602
- process.exit(1);
1603
- }
1604
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
1605
- consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1606
- process.exit(1);
1607
- }
1608
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") {
1609
- consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1610
- process.exit(1);
1611
- }
1612
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") {
1613
- consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1614
- process.exit(1);
1615
- }
1616
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") {
1617
- consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
1618
- process.exit(1);
1619
- }
1620
- if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") {
1621
- consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
1622
- process.exit(1);
1623
- }
1624
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
1625
- consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1626
- process.exit(1);
1627
- }
1628
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") {
1629
- consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1630
- process.exit(1);
1631
- }
1632
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") {
1633
- consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1634
- process.exit(1);
1635
- }
1636
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
1637
- consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1638
- process.exit(1);
1639
- }
1640
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
1641
- consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1642
- process.exit(1);
1643
- }
1644
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") {
1645
- consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1646
- process.exit(1);
1647
- }
1494
+ if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1495
+ if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1496
+ if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1497
+ if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1498
+ if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1499
+ if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1500
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1501
+ if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") exitWithError("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1502
+ if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1503
+ if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1504
+ if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1505
+ if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1648
1506
  if (config.dbSetup === "d1") {
1649
1507
  if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
1650
- if (config.database !== "sqlite") {
1651
- consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1652
- process.exit(1);
1653
- }
1508
+ if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1654
1509
  }
1655
1510
  if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
1656
- if (config.runtime !== "workers") {
1657
- consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1658
- process.exit(1);
1659
- }
1511
+ if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1660
1512
  }
1661
1513
  }
1662
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") {
1663
- consola$1.fatal("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1664
- process.exit(1);
1665
- }
1666
- if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") {
1667
- consola$1.fatal("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1668
- process.exit(1);
1669
- }
1514
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1515
+ if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1670
1516
  validateWorkersCompatibility(providedFlags, options, config);
1671
1517
  const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
1672
1518
  validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
@@ -1840,10 +1686,7 @@ async function setupFumadocs(config) {
1840
1686
  })),
1841
1687
  initialValue: "next-mdx"
1842
1688
  });
1843
- if (isCancel(template)) {
1844
- cancel(pc.red("Operation cancelled"));
1845
- process.exit(0);
1846
- }
1689
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
1847
1690
  const templateArg = TEMPLATES[template].value;
1848
1691
  const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint --no-git`;
1849
1692
  const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
@@ -2007,10 +1850,7 @@ async function setupUltracite(config, hasHusky) {
2007
1850
  })),
2008
1851
  required: false
2009
1852
  });
2010
- if (isCancel(editors)) {
2011
- cancel(pc.red("Operation cancelled"));
2012
- process.exit(0);
2013
- }
1853
+ if (isCancel(editors)) return exitCancelled("Operation cancelled");
2014
1854
  const rules = await multiselect({
2015
1855
  message: "Choose rules",
2016
1856
  options: Object.entries(RULES).map(([key, rule]) => ({
@@ -2020,10 +1860,7 @@ async function setupUltracite(config, hasHusky) {
2020
1860
  })),
2021
1861
  required: false
2022
1862
  });
2023
- if (isCancel(rules)) {
2024
- cancel(pc.red("Operation cancelled"));
2025
- process.exit(0);
2026
- }
1863
+ if (isCancel(rules)) return exitCancelled("Operation cancelled");
2027
1864
  const ultraciteArgs = [
2028
1865
  "init",
2029
1866
  "--pm",
@@ -2055,6 +1892,135 @@ async function setupUltracite(config, hasHusky) {
2055
1892
  }
2056
1893
  }
2057
1894
 
1895
+ //#endregion
1896
+ //#region src/utils/template-processor.ts
1897
+ /**
1898
+ * Processes a Handlebars template file and writes the output to the destination.
1899
+ * @param srcPath Path to the source .hbs template file.
1900
+ * @param destPath Path to write the processed file.
1901
+ * @param context Data to be passed to the Handlebars template.
1902
+ */
1903
+ async function processTemplate(srcPath, destPath, context) {
1904
+ try {
1905
+ const templateContent = await fs.readFile(srcPath, "utf-8");
1906
+ const template = handlebars.compile(templateContent);
1907
+ const processedContent = template(context);
1908
+ await fs.ensureDir(path.dirname(destPath));
1909
+ await fs.writeFile(destPath, processedContent);
1910
+ } catch (error) {
1911
+ consola.error(`Error processing template ${srcPath}:`, error);
1912
+ throw new Error(`Failed to process template ${srcPath}`);
1913
+ }
1914
+ }
1915
+ handlebars.registerHelper("eq", (a, b) => a === b);
1916
+ handlebars.registerHelper("ne", (a, b) => a !== b);
1917
+ handlebars.registerHelper("and", (a, b) => a && b);
1918
+ handlebars.registerHelper("or", (a, b) => a || b);
1919
+ handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
1920
+
1921
+ //#endregion
1922
+ //#region src/helpers/setup/vibe-rules-setup.ts
1923
+ async function setupVibeRules(config) {
1924
+ const { packageManager, projectDir } = config;
1925
+ try {
1926
+ log.info("Setting up vibe-rules...");
1927
+ const rulesDir = path.join(projectDir, ".bts");
1928
+ const ruleFile = path.join(rulesDir, "rules.md");
1929
+ if (!await fs.pathExists(ruleFile)) {
1930
+ const templatePath = path.join(PKG_ROOT, "templates", "addons", "vibe-rules", ".bts", "rules.md.hbs");
1931
+ if (await fs.pathExists(templatePath)) {
1932
+ await fs.ensureDir(rulesDir);
1933
+ await processTemplate(templatePath, ruleFile, config);
1934
+ } else {
1935
+ log.error(pc.red("Rules template not found for vibe-rules addon"));
1936
+ return;
1937
+ }
1938
+ }
1939
+ const EDITORS$1 = {
1940
+ cursor: {
1941
+ label: "Cursor",
1942
+ hint: ".cursor/rules/*.mdc"
1943
+ },
1944
+ windsurf: {
1945
+ label: "Windsurf",
1946
+ hint: ".windsurfrules"
1947
+ },
1948
+ "claude-code": {
1949
+ label: "Claude Code",
1950
+ hint: "CLAUDE.md"
1951
+ },
1952
+ vscode: {
1953
+ label: "VSCode",
1954
+ hint: ".github/instructions/*.instructions.md"
1955
+ },
1956
+ gemini: {
1957
+ label: "Gemini",
1958
+ hint: "GEMINI.md"
1959
+ },
1960
+ codex: {
1961
+ label: "Codex",
1962
+ hint: "AGENTS.md"
1963
+ },
1964
+ clinerules: {
1965
+ label: "Cline/Roo",
1966
+ hint: ".clinerules/*.md"
1967
+ },
1968
+ roo: {
1969
+ label: "Roo",
1970
+ hint: ".clinerules/*.md"
1971
+ },
1972
+ zed: {
1973
+ label: "Zed",
1974
+ hint: ".rules/*.md"
1975
+ },
1976
+ unified: {
1977
+ label: "Unified",
1978
+ hint: ".rules/*.md"
1979
+ }
1980
+ };
1981
+ const selectedEditors = await multiselect({
1982
+ message: "Choose editors to install BTS rule",
1983
+ options: Object.entries(EDITORS$1).map(([key, v]) => ({
1984
+ value: key,
1985
+ label: v.label,
1986
+ hint: v.hint
1987
+ })),
1988
+ required: false
1989
+ });
1990
+ if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
1991
+ const editorsArg = selectedEditors.join(", ");
1992
+ const s = spinner();
1993
+ s.start("Saving and applying BTS rules...");
1994
+ try {
1995
+ const saveCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest save bts -f ${JSON.stringify(path.relative(projectDir, ruleFile))}`);
1996
+ await execa(saveCmd, {
1997
+ cwd: projectDir,
1998
+ env: { CI: "true" },
1999
+ shell: true
2000
+ });
2001
+ for (const editor of selectedEditors) {
2002
+ const loadCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest load bts ${editor}`);
2003
+ await execa(loadCmd, {
2004
+ cwd: projectDir,
2005
+ env: { CI: "true" },
2006
+ shell: true
2007
+ });
2008
+ }
2009
+ s.stop(`Applied BTS rules to: ${editorsArg}`);
2010
+ } catch (error) {
2011
+ s.stop(pc.red("Failed to apply BTS rules"));
2012
+ throw error;
2013
+ }
2014
+ try {
2015
+ await fs.remove(rulesDir);
2016
+ } catch (_) {}
2017
+ log.success("vibe-rules setup successfully!");
2018
+ } catch (error) {
2019
+ log.error(pc.red("Failed to set up vibe-rules"));
2020
+ if (error instanceof Error) console.error(pc.red(error.message));
2021
+ }
2022
+ }
2023
+
2058
2024
  //#endregion
2059
2025
  //#region src/utils/ts-morph.ts
2060
2026
  const tsProject = new Project({
@@ -2146,6 +2112,7 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2146
2112
  }
2147
2113
  if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2148
2114
  if (addons.includes("starlight")) await setupStarlight(config);
2115
+ if (addons.includes("vibe-rules")) await setupVibeRules(config);
2149
2116
  if (addons.includes("fumadocs")) await setupFumadocs(config);
2150
2117
  }
2151
2118
  function getWebAppDir(projectDir, frontends) {
@@ -2290,31 +2257,6 @@ async function installDependencies({ projectDir, packageManager }) {
2290
2257
  }
2291
2258
  }
2292
2259
 
2293
- //#endregion
2294
- //#region src/utils/template-processor.ts
2295
- /**
2296
- * Processes a Handlebars template file and writes the output to the destination.
2297
- * @param srcPath Path to the source .hbs template file.
2298
- * @param destPath Path to write the processed file.
2299
- * @param context Data to be passed to the Handlebars template.
2300
- */
2301
- async function processTemplate(srcPath, destPath, context) {
2302
- try {
2303
- const templateContent = await fs.readFile(srcPath, "utf-8");
2304
- const template = handlebars.compile(templateContent);
2305
- const processedContent = template(context);
2306
- await fs.ensureDir(path.dirname(destPath));
2307
- await fs.writeFile(destPath, processedContent);
2308
- } catch (error) {
2309
- consola.error(`Error processing template ${srcPath}:`, error);
2310
- throw new Error(`Failed to process template ${srcPath}`);
2311
- }
2312
- }
2313
- handlebars.registerHelper("eq", (a, b) => a === b);
2314
- handlebars.registerHelper("and", (a, b) => a && b);
2315
- handlebars.registerHelper("or", (a, b) => a || b);
2316
- handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
2317
-
2318
2260
  //#endregion
2319
2261
  //#region src/helpers/project-generation/template-manager.ts
2320
2262
  async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
@@ -2525,6 +2467,7 @@ async function setupAddonsTemplate(projectDir, context) {
2525
2467
  if (!context.addons || context.addons.length === 0) return;
2526
2468
  for (const addon of context.addons) {
2527
2469
  if (addon === "none") continue;
2470
+ if (addon === "vibe-rules") continue;
2528
2471
  let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
2529
2472
  let addonDestDir = projectDir;
2530
2473
  if (addon === "pwa") {
@@ -2685,17 +2628,13 @@ async function setupDeploymentTemplates(projectDir, context) {
2685
2628
 
2686
2629
  //#endregion
2687
2630
  //#region src/helpers/project-generation/add-addons.ts
2688
- function exitWithError$1(message) {
2689
- cancel(pc.red(message));
2690
- process.exit(1);
2691
- }
2692
2631
  async function addAddonsToProject(input) {
2693
2632
  try {
2694
2633
  const projectDir = input.projectDir || process.cwd();
2695
2634
  const isBetterTStack = await isBetterTStackProject(projectDir);
2696
- if (!isBetterTStack) exitWithError$1("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
2635
+ if (!isBetterTStack) exitWithError("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
2697
2636
  const detectedConfig = await detectProjectConfig(projectDir);
2698
- if (!detectedConfig) exitWithError$1("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
2637
+ if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
2699
2638
  const config = {
2700
2639
  projectName: detectedConfig.projectName || path.basename(projectDir),
2701
2640
  projectDir,
@@ -2717,9 +2656,8 @@ async function addAddonsToProject(input) {
2717
2656
  };
2718
2657
  for (const addon of input.addons) {
2719
2658
  const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
2720
- if (!isCompatible) exitWithError$1(reason || `${addon} addon is not compatible with current frontend configuration`);
2659
+ if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
2721
2660
  }
2722
- log.info(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`);
2723
2661
  await setupAddonsTemplate(projectDir, config);
2724
2662
  await setupAddons(config, true);
2725
2663
  const currentAddons = detectedConfig.addons || [];
@@ -2732,7 +2670,7 @@ async function addAddonsToProject(input) {
2732
2670
  else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
2733
2671
  } catch (error) {
2734
2672
  const message = error instanceof Error ? error.message : String(error);
2735
- exitWithError$1(`Error adding addons: ${message}`);
2673
+ exitWithError(`Error adding addons: ${message}`);
2736
2674
  }
2737
2675
  }
2738
2676
 
@@ -2965,10 +2903,6 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
2965
2903
 
2966
2904
  //#endregion
2967
2905
  //#region src/helpers/project-generation/add-deployment.ts
2968
- function exitWithError(message) {
2969
- cancel(pc.red(message));
2970
- process.exit(1);
2971
- }
2972
2906
  async function addDeploymentToProject(input) {
2973
2907
  try {
2974
2908
  const projectDir = input.projectDir || process.cwd();
@@ -3768,10 +3702,7 @@ async function setupNeonPostgres(config) {
3768
3702
  }],
3769
3703
  initialValue: "neondb"
3770
3704
  });
3771
- if (isCancel(setupMethod)) {
3772
- cancel(pc.red("Operation cancelled"));
3773
- process.exit(0);
3774
- }
3705
+ if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
3775
3706
  if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager);
3776
3707
  else {
3777
3708
  const suggestedProjectName = path.basename(projectDir);
@@ -3785,10 +3716,7 @@ async function setupNeonPostgres(config) {
3785
3716
  options: NEON_REGIONS,
3786
3717
  initialValue: NEON_REGIONS[0].value
3787
3718
  });
3788
- if (isCancel(projectName) || isCancel(regionId)) {
3789
- cancel(pc.red("Operation cancelled"));
3790
- process.exit(0);
3791
- }
3719
+ if (isCancel(projectName) || isCancel(regionId)) return exitCancelled("Operation cancelled");
3792
3720
  const neonConfig = await createNeonProject(projectName, regionId, packageManager);
3793
3721
  if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
3794
3722
  const finalSpinner = spinner();
@@ -3823,10 +3751,7 @@ async function setupWithCreateDb(serverDir, packageManager, orm) {
3823
3751
  if (orm === "drizzle" && !value.includes("?sslmode=require")) return "Please append ?sslmode=require to your database URL when using Drizzle";
3824
3752
  }
3825
3753
  });
3826
- if (isCancel(databaseUrl)) {
3827
- cancel("Database setup cancelled");
3828
- return null;
3829
- }
3754
+ if (isCancel(databaseUrl)) return null;
3830
3755
  return { databaseUrl };
3831
3756
  } catch (error) {
3832
3757
  if (error instanceof Error) consola$1.error(error.message);
@@ -3852,10 +3777,7 @@ async function initPrismaDatabase(serverDir, packageManager) {
3852
3777
  if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://";
3853
3778
  }
3854
3779
  });
3855
- if (isCancel(databaseUrl)) {
3856
- cancel("Database setup cancelled");
3857
- return null;
3858
- }
3780
+ if (isCancel(databaseUrl)) return null;
3859
3781
  return { databaseUrl };
3860
3782
  } catch (error) {
3861
3783
  if (error instanceof Error) consola$1.error(error.message);
@@ -3946,10 +3868,7 @@ async function setupPrismaPostgres(config) {
3946
3868
  options: setupOptions,
3947
3869
  initialValue: "create-db"
3948
3870
  });
3949
- if (isCancel(setupMethod)) {
3950
- cancel(pc.red("Operation cancelled"));
3951
- process.exit(0);
3952
- }
3871
+ if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
3953
3872
  let prismaConfig = null;
3954
3873
  if (setupMethod === "create-db") prismaConfig = await setupWithCreateDb(serverDir, packageManager, orm);
3955
3874
  else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
@@ -4183,10 +4102,7 @@ async function selectTursoGroup() {
4183
4102
  message: "Select a Turso database group:",
4184
4103
  options: groupOptions
4185
4104
  });
4186
- if (isCancel(selectedGroup)) {
4187
- cancel(pc.red("Operation cancelled"));
4188
- process.exit(0);
4189
- }
4105
+ if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
4190
4106
  return selectedGroup;
4191
4107
  }
4192
4108
  async function createTursoDatabase(dbName, groupName) {
@@ -4259,10 +4175,7 @@ async function setupTurso(config) {
4259
4175
  message: "Would you like to install Turso CLI?",
4260
4176
  initialValue: true
4261
4177
  });
4262
- if (isCancel(shouldInstall)) {
4263
- cancel(pc.red("Operation cancelled"));
4264
- process.exit(0);
4265
- }
4178
+ if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
4266
4179
  if (!shouldInstall) {
4267
4180
  await writeEnvFile(projectDir);
4268
4181
  displayManualSetupInstructions();
@@ -4283,10 +4196,7 @@ async function setupTurso(config) {
4283
4196
  initialValue: suggestedName,
4284
4197
  placeholder: suggestedName
4285
4198
  });
4286
- if (isCancel(dbNameResponse)) {
4287
- cancel(pc.red("Operation cancelled"));
4288
- process.exit(0);
4289
- }
4199
+ if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
4290
4200
  dbName = dbNameResponse;
4291
4201
  try {
4292
4202
  const config$1 = await createTursoDatabase(dbName, selectedGroup);
@@ -5203,13 +5113,11 @@ async function createProject(options) {
5203
5113
  return projectDir;
5204
5114
  } catch (error) {
5205
5115
  if (error instanceof Error) {
5206
- cancel(pc.red(`Error during project creation: ${error.message}`));
5207
5116
  console.error(error.stack);
5208
- process.exit(1);
5117
+ exitWithError(`Error during project creation: ${error.message}`);
5209
5118
  } else {
5210
- cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
5211
5119
  console.error(error);
5212
- process.exit(1);
5120
+ exitWithError(`An unexpected error occurred: ${String(error)}`);
5213
5121
  }
5214
5122
  }
5215
5123
  }
@@ -5268,18 +5176,14 @@ async function createProjectHandler(input) {
5268
5176
  const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
5269
5177
  outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
5270
5178
  } catch (error) {
5271
- console.error(error);
5272
- process.exit(1);
5179
+ handleError(error, "Failed to create project");
5273
5180
  }
5274
5181
  }
5275
5182
  async function addAddonsHandler(input) {
5276
5183
  try {
5277
5184
  const projectDir = input.projectDir || process.cwd();
5278
5185
  const detectedConfig = await detectProjectConfig(projectDir);
5279
- if (!detectedConfig) {
5280
- cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
5281
- process.exit(1);
5282
- }
5186
+ if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T Stack project.");
5283
5187
  if (!input.addons || input.addons.length === 0) {
5284
5188
  const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
5285
5189
  if (addonsPrompt.length > 0) input.addons = addonsPrompt;
@@ -5319,8 +5223,7 @@ async function addAddonsHandler(input) {
5319
5223
  else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
5320
5224
  outro("Add command completed successfully!");
5321
5225
  } catch (error) {
5322
- console.error(error);
5323
- process.exit(1);
5226
+ handleError(error, "Failed to add addons or deployment");
5324
5227
  }
5325
5228
  }
5326
5229
 
@@ -5435,8 +5338,7 @@ const router = t.router({
5435
5338
  const sponsors = await fetchSponsors();
5436
5339
  displaySponsors(sponsors);
5437
5340
  } catch (error) {
5438
- consola$1.error(error);
5439
- process.exit(1);
5341
+ handleError(error, "Failed to display sponsors");
5440
5342
  }
5441
5343
  }),
5442
5344
  docs: t.procedure.meta({ description: "Open Better-T Stack documentation" }).mutation(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.29.4",
3
+ "version": "2.30.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
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,132 @@
1
+ # Better-T-Stack Project Rules
2
+
3
+ This is a {{projectName}} project created with Better-T-Stack CLI.
4
+
5
+ ## Project Structure
6
+
7
+ This is a monorepo with the following structure:
8
+
9
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
10
+ (includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
11
+ - **`apps/web/`** - Frontend application{{#if (includes frontend "tanstack-router")}} (React with TanStack Router){{else
12
+ if (includes frontend "react-router")}} (React with React Router){{else if (includes frontend "next")}} (Next.js){{else
13
+ if (includes frontend "nuxt")}} (Nuxt.js){{else if (includes frontend "svelte")}} (SvelteKit){{else if (includes
14
+ frontend "solid")}} (SolidStart){{/if}}
15
+ {{/if}}
16
+
17
+ {{#if (ne backend "convex")}}
18
+ {{#if (ne backend "none")}}
19
+ - **`apps/server/`** - Backend server{{#if (eq backend "hono")}} (Hono){{else if (eq backend "express")}}
20
+ (Express){{else if (eq backend "fastify")}} (Fastify){{else if (eq backend "elysia")}} (Elysia){{else if (eq backend
21
+ "next")}} (Next.js API){{/if}}
22
+ {{/if}}
23
+ {{else}}
24
+ - **`packages/backend/`** - Convex backend functions
25
+ {{/if}}
26
+
27
+ {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
28
+ - **`apps/native/`** - React Native mobile app{{#if (includes frontend "native-nativewind")}} (with NativeWind){{else if
29
+ (includes frontend "native-unistyles")}} (with Unistyles){{/if}}
30
+ {{/if}}
31
+
32
+ ## Available Scripts
33
+
34
+ - `{{packageManager}} run dev` - Start all apps in development mode
35
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
36
+ (includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
37
+ - `{{packageManager}} run dev:web` - Start only the web app
38
+ {{/if}}
39
+ {{#if (ne backend "none")}}
40
+ {{#if (ne backend "convex")}}
41
+ - `{{packageManager}} run dev:server` - Start only the server
42
+ {{/if}}
43
+ {{/if}}
44
+ {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
45
+ - `{{packageManager}} run dev:native` - Start only the native app
46
+ {{/if}}
47
+
48
+ {{#if (and (ne database "none") (ne orm "none") (ne backend "convex"))}}
49
+ ## Database Commands
50
+
51
+ All database operations should be run from the server workspace:
52
+
53
+ - `{{packageManager}} run db:push` - Push schema changes to database
54
+ - `{{packageManager}} run db:studio` - Open database studio
55
+ - `{{packageManager}} run db:generate` - Generate {{#if (eq orm "drizzle")}}Drizzle{{else if (eq orm
56
+ "prisma")}}Prisma{{else}}{{orm}}{{/if}} files
57
+ - `{{packageManager}} run db:migrate` - Run database migrations
58
+
59
+ {{#if (eq orm "drizzle")}}
60
+ Database schema files are located in `apps/server/src/db/schema/`
61
+ {{else if (eq orm "prisma")}}
62
+ Database schema is located in `apps/server/prisma/schema.prisma`
63
+ {{else if (eq orm "mongoose")}}
64
+ Database models are located in `apps/server/src/db/models/`
65
+ {{/if}}
66
+ {{/if}}
67
+
68
+ {{#if (ne api "none")}}
69
+ ## API Structure
70
+
71
+ {{#if (eq api "trpc")}}
72
+ - tRPC routers are in `apps/server/src/routers/`
73
+ - Client-side tRPC utils are in `apps/web/src/utils/trpc.ts`
74
+ {{else if (eq api "orpc")}}
75
+ - oRPC endpoints are in `apps/server/src/api/`
76
+ - Client-side API utils are in `apps/web/src/utils/api.ts`
77
+ {{/if}}
78
+ {{/if}}
79
+
80
+ {{#if auth}}
81
+ ## Authentication
82
+
83
+ Authentication is enabled in this project:
84
+ {{#if (ne backend "convex")}}
85
+ - Server auth logic is in `apps/server/src/lib/auth.ts`
86
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
87
+ (includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
88
+ - Web app auth client is in `apps/web/src/lib/auth-client.ts`
89
+ {{/if}}
90
+ {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
91
+ - Native app auth client is in `apps/native/src/lib/auth-client.ts`
92
+ {{/if}}
93
+ {{else}}
94
+ {{/if}}
95
+ {{/if}}
96
+
97
+ ## Adding More Features
98
+
99
+ You can add additional addons or deployment options to your project using:
100
+
101
+ ```bash
102
+ {{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}} create-better-t-stack
103
+ add
104
+ ```
105
+
106
+ Available addons you can add:
107
+ - **Documentation**: Starlight, Fumadocs
108
+ - **Linting**: Biome, Oxlint, Ultracite
109
+ - **Other**: vibe-rules, Turborepo, PWA, Tauri, Husky
110
+
111
+ You can also add web deployment configurations like Cloudflare Workers support.
112
+
113
+ ## Project Configuration
114
+
115
+ This project includes a `bts.jsonc` configuration file that stores your Better-T-Stack settings:
116
+
117
+ - Contains your selected stack configuration (database, ORM, backend, frontend, etc.)
118
+ - Used by the CLI to understand your project structure
119
+ - Safe to delete if not needed
120
+ - Updated automatically when using the `add` command
121
+
122
+ ## Key Points
123
+
124
+ - This is a {{#if (includes addons "turborepo")}}Turborepo {{/if}}monorepo using {{packageManager}} workspaces
125
+ - Each app has its own `package.json` and dependencies
126
+ - Run commands from the root to execute across all workspaces
127
+ - Run workspace-specific commands with `{{packageManager}} run command-name`
128
+ {{#if (includes addons "turborepo")}}
129
+ - Turborepo handles build caching and parallel execution
130
+ {{/if}}
131
+ - Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
132
+ create-better-t-stack add` to add more features later