create-better-t-stack 2.29.4 → 2.31.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
@@ -88,11 +88,11 @@ const dependencyVersionMap = {
88
88
  fastify: "^5.3.3",
89
89
  "@fastify/cors": "^11.0.1",
90
90
  turbo: "^2.5.4",
91
- ai: "^4.3.16",
92
- "@ai-sdk/google": "^1.2.3",
93
- "@ai-sdk/vue": "^1.2.8",
94
- "@ai-sdk/svelte": "^2.1.9",
95
- "@ai-sdk/react": "^1.2.12",
91
+ ai: "^5.0.9",
92
+ "@ai-sdk/google": "^2.0.3",
93
+ "@ai-sdk/vue": "^2.0.9",
94
+ "@ai-sdk/svelte": "^3.0.9",
95
+ "@ai-sdk/react": "^2.0.9",
96
96
  "@orpc/server": "^1.5.0",
97
97
  "@orpc/client": "^1.5.0",
98
98
  "@orpc/tanstack-query": "^1.5.0",
@@ -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);
@@ -4380,23 +4290,30 @@ async function setupExamples(config) {
4380
4290
  const { examples, frontend, backend, projectDir } = config;
4381
4291
  if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
4382
4292
  if (examples.includes("ai")) {
4383
- const clientDir = path.join(projectDir, "apps/web");
4293
+ const webClientDir = path.join(projectDir, "apps/web");
4294
+ const nativeClientDir = path.join(projectDir, "apps/native");
4384
4295
  const serverDir = path.join(projectDir, "apps/server");
4385
- const clientDirExists = await fs.pathExists(clientDir);
4296
+ const webClientDirExists = await fs.pathExists(webClientDir);
4297
+ const nativeClientDirExists = await fs.pathExists(nativeClientDir);
4386
4298
  const serverDirExists = await fs.pathExists(serverDir);
4387
4299
  const hasNuxt = frontend.includes("nuxt");
4388
4300
  const hasSvelte = frontend.includes("svelte");
4389
- const hasReact = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start") || frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
4390
- if (clientDirExists) {
4301
+ const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
4302
+ const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
4303
+ if (webClientDirExists) {
4391
4304
  const dependencies = ["ai"];
4392
4305
  if (hasNuxt) dependencies.push("@ai-sdk/vue");
4393
4306
  else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
4394
- else if (hasReact) dependencies.push("@ai-sdk/react");
4307
+ else if (hasReactWeb) dependencies.push("@ai-sdk/react");
4395
4308
  await addPackageDependency({
4396
4309
  dependencies,
4397
- projectDir: clientDir
4310
+ projectDir: webClientDir
4398
4311
  });
4399
4312
  }
4313
+ if (nativeClientDirExists && hasReactNative) await addPackageDependency({
4314
+ dependencies: ["ai", "@ai-sdk/react"],
4315
+ projectDir: nativeClientDir
4316
+ });
4400
4317
  if (serverDirExists && backend !== "none") await addPackageDependency({
4401
4318
  dependencies: ["ai", "@ai-sdk/google"],
4402
4319
  projectDir: serverDir
@@ -5203,13 +5120,11 @@ async function createProject(options) {
5203
5120
  return projectDir;
5204
5121
  } catch (error) {
5205
5122
  if (error instanceof Error) {
5206
- cancel(pc.red(`Error during project creation: ${error.message}`));
5207
5123
  console.error(error.stack);
5208
- process.exit(1);
5124
+ exitWithError(`Error during project creation: ${error.message}`);
5209
5125
  } else {
5210
- cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
5211
5126
  console.error(error);
5212
- process.exit(1);
5127
+ exitWithError(`An unexpected error occurred: ${String(error)}`);
5213
5128
  }
5214
5129
  }
5215
5130
  }
@@ -5268,18 +5183,14 @@ async function createProjectHandler(input) {
5268
5183
  const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
5269
5184
  outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
5270
5185
  } catch (error) {
5271
- console.error(error);
5272
- process.exit(1);
5186
+ handleError(error, "Failed to create project");
5273
5187
  }
5274
5188
  }
5275
5189
  async function addAddonsHandler(input) {
5276
5190
  try {
5277
5191
  const projectDir = input.projectDir || process.cwd();
5278
5192
  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
- }
5193
+ if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T Stack project.");
5283
5194
  if (!input.addons || input.addons.length === 0) {
5284
5195
  const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
5285
5196
  if (addonsPrompt.length > 0) input.addons = addonsPrompt;
@@ -5319,8 +5230,7 @@ async function addAddonsHandler(input) {
5319
5230
  else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
5320
5231
  outro("Add command completed successfully!");
5321
5232
  } catch (error) {
5322
- console.error(error);
5323
- process.exit(1);
5233
+ handleError(error, "Failed to add addons or deployment");
5324
5234
  }
5325
5235
  }
5326
5236
 
@@ -5435,8 +5345,7 @@ const router = t.router({
5435
5345
  const sponsors = await fetchSponsors();
5436
5346
  displaySponsors(sponsors);
5437
5347
  } catch (error) {
5438
- consola$1.error(error);
5439
- process.exit(1);
5348
+ handleError(error, "Failed to display sponsors");
5440
5349
  }
5441
5350
  }),
5442
5351
  docs: t.procedure.meta({ description: "Open Better-T Stack documentation" }).mutation(async () => {