create-better-t-stack 2.29.3 → 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,19 +136,11 @@ const ADDON_COMPATIBILITY = {
136
136
  turborepo: [],
137
137
  starlight: [],
138
138
  ultracite: [],
139
+ "vibe-rules": [],
139
140
  oxlint: [],
140
141
  fumadocs: [],
141
142
  none: []
142
143
  };
143
- const WEB_FRAMEWORKS = [
144
- "tanstack-router",
145
- "react-router",
146
- "tanstack-start",
147
- "next",
148
- "nuxt",
149
- "svelte",
150
- "solid"
151
- ];
152
144
 
153
145
  //#endregion
154
146
  //#region src/types.ts
@@ -179,7 +171,7 @@ const RuntimeSchema = z.enum([
179
171
  "node",
180
172
  "workers",
181
173
  "none"
182
- ]).describe("Runtime environment (workers only available with hono backend and drizzle orm)");
174
+ ]).describe("Runtime environment");
183
175
  const FrontendSchema = z.enum([
184
176
  "tanstack-router",
185
177
  "react-router",
@@ -198,6 +190,7 @@ const AddonsSchema = z.enum([
198
190
  "starlight",
199
191
  "biome",
200
192
  "husky",
193
+ "vibe-rules",
201
194
  "turborepo",
202
195
  "fumadocs",
203
196
  "ultracite",
@@ -267,6 +260,22 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
267
260
  });
268
261
  }
269
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
+
270
279
  //#endregion
271
280
  //#region src/prompts/addons.ts
272
281
  function getAddonDisplay(addon) {
@@ -297,6 +306,10 @@ function getAddonDisplay(addon) {
297
306
  label = "Ultracite";
298
307
  hint = "Zero-config Biome preset with AI integration";
299
308
  break;
309
+ case "vibe-rules":
310
+ label = "vibe-rules";
311
+ hint = "Install and apply BTS rules to editors";
312
+ break;
300
313
  case "husky":
301
314
  label = "Husky";
302
315
  hint = "Modern native Git hooks made easy";
@@ -326,6 +339,7 @@ const ADDON_GROUPS = {
326
339
  "ultracite"
327
340
  ],
328
341
  Other: [
342
+ "vibe-rules",
329
343
  "turborepo",
330
344
  "pwa",
331
345
  "tauri",
@@ -365,10 +379,7 @@ async function getAddonsChoice(addons, frontends) {
365
379
  required: false,
366
380
  selectableGroups: false
367
381
  });
368
- if (isCancel(response)) {
369
- cancel(pc.red("Operation cancelled"));
370
- process.exit(0);
371
- }
382
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
372
383
  return response;
373
384
  }
374
385
  async function getAddonsToAdd(frontend, existingAddons = []) {
@@ -400,56 +411,154 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
400
411
  required: false,
401
412
  selectableGroups: false
402
413
  });
403
- if (isCancel(response)) {
404
- cancel(pc.red("Operation cancelled"));
405
- process.exit(0);
406
- }
414
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
407
415
  return response;
408
416
  }
409
417
 
418
+ //#endregion
419
+ //#region src/utils/compatibility.ts
420
+ const WEB_FRAMEWORKS = [
421
+ "tanstack-router",
422
+ "react-router",
423
+ "tanstack-start",
424
+ "next",
425
+ "nuxt",
426
+ "svelte",
427
+ "solid"
428
+ ];
429
+
430
+ //#endregion
431
+ //#region src/utils/compatibility-rules.ts
432
+ function isWebFrontend(value) {
433
+ return WEB_FRAMEWORKS.includes(value);
434
+ }
435
+ function splitFrontends(values = []) {
436
+ const web = values.filter((f) => isWebFrontend(f));
437
+ const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
438
+ return {
439
+ web,
440
+ native
441
+ };
442
+ }
443
+ function ensureSingleWebAndNative(frontends) {
444
+ const { web, native } = splitFrontends(frontends);
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");
447
+ }
448
+ function validateWorkersCompatibility(providedFlags, options, config) {
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.");
457
+ }
458
+ function coerceBackendPresets(config) {
459
+ if (config.backend === "convex") {
460
+ config.auth = false;
461
+ config.database = "none";
462
+ config.orm = "none";
463
+ config.api = "none";
464
+ config.runtime = "none";
465
+ config.dbSetup = "none";
466
+ config.examples = ["todo"];
467
+ }
468
+ if (config.backend === "none") {
469
+ config.auth = false;
470
+ config.database = "none";
471
+ config.orm = "none";
472
+ config.api = "none";
473
+ config.runtime = "none";
474
+ config.dbSetup = "none";
475
+ config.examples = [];
476
+ }
477
+ }
478
+ function incompatibleFlagsForBackend(backend, providedFlags, options) {
479
+ const list = [];
480
+ if (backend === "convex") {
481
+ if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
482
+ if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
483
+ if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
484
+ if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
485
+ if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
486
+ if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
487
+ }
488
+ if (backend === "none") {
489
+ if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
490
+ if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
491
+ if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
492
+ if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
493
+ if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
494
+ if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
495
+ if (providedFlags.has("examples") && options.examples) {
496
+ const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
497
+ if (hasNonNoneExamples) list.push("--examples");
498
+ }
499
+ }
500
+ return list;
501
+ }
502
+ function validateApiFrontendCompatibility(api, frontends = []) {
503
+ const includesNuxt = frontends.includes("nuxt");
504
+ const includesSvelte = frontends.includes("svelte");
505
+ const includesSolid = frontends.includes("solid");
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.`);
507
+ }
508
+ function isFrontendAllowedWithBackend(frontend, backend) {
509
+ if (backend === "convex" && frontend === "solid") return false;
510
+ return true;
511
+ }
512
+ function allowedApisForFrontends(frontends = []) {
513
+ const includesNuxt = frontends.includes("nuxt");
514
+ const includesSvelte = frontends.includes("svelte");
515
+ const includesSolid = frontends.includes("solid");
516
+ const base = [
517
+ "trpc",
518
+ "orpc",
519
+ "none"
520
+ ];
521
+ if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
522
+ return base;
523
+ }
524
+ function isExampleTodoAllowed(backend, database) {
525
+ return !(backend !== "convex" && backend !== "none" && database === "none");
526
+ }
527
+ function isExampleAIAllowed(backend, frontends = []) {
528
+ const includesSolid = frontends.includes("solid");
529
+ if (backend === "elysia") return false;
530
+ if (includesSolid) return false;
531
+ return true;
532
+ }
533
+ function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
534
+ if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
535
+ }
536
+
410
537
  //#endregion
411
538
  //#region src/prompts/api.ts
412
539
  async function getApiChoice(Api, frontend, backend) {
413
540
  if (backend === "convex" || backend === "none") return "none";
414
- if (Api) return Api;
415
- const includesNuxt = frontend?.includes("nuxt");
416
- const includesSvelte = frontend?.includes("svelte");
417
- const includesSolid = frontend?.includes("solid");
418
- let apiOptions = [
419
- {
420
- value: "trpc",
421
- label: "tRPC",
422
- hint: "End-to-end typesafe APIs made easy"
423
- },
424
- {
425
- value: "orpc",
426
- label: "oRPC",
427
- hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
428
- },
429
- {
430
- value: "none",
431
- label: "None",
432
- hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
433
- }
434
- ];
435
- if (includesNuxt || includesSvelte || includesSolid) apiOptions = [{
541
+ const allowed = allowedApisForFrontends(frontend ?? []);
542
+ if (Api) return allowed.includes(Api) ? Api : allowed[0];
543
+ const apiOptions = allowed.map((a) => a === "trpc" ? {
544
+ value: "trpc",
545
+ label: "tRPC",
546
+ hint: "End-to-end typesafe APIs made easy"
547
+ } : a === "orpc" ? {
436
548
  value: "orpc",
437
549
  label: "oRPC",
438
- hint: `End-to-end type-safe APIs (Recommended for ${includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"} frontend)`
439
- }, {
550
+ hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
551
+ } : {
440
552
  value: "none",
441
553
  label: "None",
442
- hint: "No API layer"
443
- }];
554
+ hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
555
+ });
444
556
  const apiType = await select({
445
557
  message: "Select API type",
446
558
  options: apiOptions,
447
559
  initialValue: apiOptions[0].value
448
560
  });
449
- if (isCancel(apiType)) {
450
- cancel(pc.red("Operation cancelled"));
451
- process.exit(0);
452
- }
561
+ if (isCancel(apiType)) return exitCancelled("Operation cancelled");
453
562
  return apiType;
454
563
  }
455
564
 
@@ -463,10 +572,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
463
572
  message: "Add authentication with Better-Auth?",
464
573
  initialValue: DEFAULT_CONFIG.auth
465
574
  });
466
- if (isCancel(response)) {
467
- cancel(pc.red("Operation cancelled"));
468
- process.exit(0);
469
- }
575
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
470
576
  return response;
471
577
  }
472
578
 
@@ -517,10 +623,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
517
623
  options: backendOptions,
518
624
  initialValue: DEFAULT_CONFIG.backend
519
625
  });
520
- if (isCancel(response)) {
521
- cancel(pc.red("Operation cancelled"));
522
- process.exit(0);
523
- }
626
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
524
627
  return response;
525
628
  }
526
629
 
@@ -561,10 +664,7 @@ async function getDatabaseChoice(database, backend, runtime) {
561
664
  options: databaseOptions,
562
665
  initialValue: DEFAULT_CONFIG.database
563
666
  });
564
- if (isCancel(response)) {
565
- cancel(pc.red("Operation cancelled"));
566
- process.exit(0);
567
- }
667
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
568
668
  return response;
569
669
  }
570
670
 
@@ -652,10 +752,7 @@ async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
652
752
  options,
653
753
  initialValue: "none"
654
754
  });
655
- if (isCancel(response)) {
656
- cancel(pc.red("Operation cancelled"));
657
- process.exit(0);
658
- }
755
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
659
756
  return response;
660
757
  }
661
758
 
@@ -670,26 +767,25 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
670
767
  const noFrontendSelected = !frontends || frontends.length === 0;
671
768
  if (noFrontendSelected) return [];
672
769
  let response = [];
673
- const options = [{
770
+ const options = [];
771
+ if (isExampleTodoAllowed(backend, database)) options.push({
674
772
  value: "todo",
675
773
  label: "Todo App",
676
774
  hint: "A simple CRUD example app"
677
- }];
678
- if (backend !== "elysia" && !frontends?.includes("solid")) options.push({
775
+ });
776
+ if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
679
777
  value: "ai",
680
778
  label: "AI Chat",
681
779
  hint: "A simple AI chat interface using AI SDK"
682
780
  });
781
+ if (options.length === 0) return [];
683
782
  response = await multiselect({
684
783
  message: "Include examples",
685
784
  options,
686
785
  required: false,
687
- initialValues: DEFAULT_CONFIG.examples
786
+ initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
688
787
  });
689
- if (isCancel(response)) {
690
- cancel(pc.red("Operation cancelled"));
691
- process.exit(0);
692
- }
788
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
693
789
  return response;
694
790
  }
695
791
 
@@ -711,10 +807,7 @@ async function getFrontendChoice(frontendOptions, backend) {
711
807
  required: false,
712
808
  initialValues: ["web"]
713
809
  });
714
- if (isCancel(frontendTypes)) {
715
- cancel(pc.red("Operation cancelled"));
716
- process.exit(0);
717
- }
810
+ if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
718
811
  const result = [];
719
812
  if (frontendTypes.includes("web")) {
720
813
  const allWebOptions = [
@@ -750,23 +843,17 @@ async function getFrontendChoice(frontendOptions, backend) {
750
843
  },
751
844
  {
752
845
  value: "tanstack-start",
753
- label: "TanStack Start (vite)",
846
+ label: "TanStack Start",
754
847
  hint: "SSR, Server Functions, API Routes and more with TanStack Router"
755
848
  }
756
849
  ];
757
- const webOptions = allWebOptions.filter((option) => {
758
- if (backend === "convex") return option.value !== "solid";
759
- return true;
760
- });
850
+ const webOptions = allWebOptions.filter((option) => isFrontendAllowedWithBackend(option.value, backend));
761
851
  const webFramework = await select({
762
852
  message: "Choose web",
763
853
  options: webOptions,
764
854
  initialValue: DEFAULT_CONFIG.frontend[0]
765
855
  });
766
- if (isCancel(webFramework)) {
767
- cancel(pc.red("Operation cancelled"));
768
- process.exit(0);
769
- }
856
+ if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
770
857
  result.push(webFramework);
771
858
  }
772
859
  if (frontendTypes.includes("native")) {
@@ -783,10 +870,7 @@ async function getFrontendChoice(frontendOptions, backend) {
783
870
  }],
784
871
  initialValue: "native-nativewind"
785
872
  });
786
- if (isCancel(nativeFramework)) {
787
- cancel(pc.red("Operation cancelled"));
788
- process.exit(0);
789
- }
873
+ if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
790
874
  result.push(nativeFramework);
791
875
  }
792
876
  return result;
@@ -800,10 +884,7 @@ async function getGitChoice(git) {
800
884
  message: "Initialize git repository?",
801
885
  initialValue: DEFAULT_CONFIG.git
802
886
  });
803
- if (isCancel(response)) {
804
- cancel(pc.red("Operation cancelled"));
805
- process.exit(0);
806
- }
887
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
807
888
  return response;
808
889
  }
809
890
 
@@ -815,10 +896,7 @@ async function getinstallChoice(install) {
815
896
  message: "Install dependencies?",
816
897
  initialValue: DEFAULT_CONFIG.install
817
898
  });
818
- if (isCancel(response)) {
819
- cancel(pc.red("Operation cancelled"));
820
- process.exit(0);
821
- }
899
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
822
900
  return response;
823
901
  }
824
902
 
@@ -852,10 +930,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
852
930
  options,
853
931
  initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
854
932
  });
855
- if (isCancel(response)) {
856
- cancel(pc.red("Operation cancelled"));
857
- process.exit(0);
858
- }
933
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
859
934
  return response;
860
935
  }
861
936
 
@@ -885,10 +960,7 @@ async function getPackageManagerChoice(packageManager) {
885
960
  ],
886
961
  initialValue: detectedPackageManager
887
962
  });
888
- if (isCancel(response)) {
889
- cancel(pc.red("Operation cancelled"));
890
- process.exit(0);
891
- }
963
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
892
964
  return response;
893
965
  }
894
966
 
@@ -917,10 +989,7 @@ async function getRuntimeChoice(runtime, backend) {
917
989
  options: runtimeOptions,
918
990
  initialValue: DEFAULT_CONFIG.runtime
919
991
  });
920
- if (isCancel(response)) {
921
- cancel(pc.red("Operation cancelled"));
922
- process.exit(0);
923
- }
992
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
924
993
  return response;
925
994
  }
926
995
 
@@ -956,10 +1025,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
956
1025
  options,
957
1026
  initialValue: DEFAULT_CONFIG.webDeploy
958
1027
  });
959
- if (isCancel(response)) {
960
- cancel(pc.red("Operation cancelled"));
961
- process.exit(0);
962
- }
1028
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
963
1029
  return response;
964
1030
  }
965
1031
  async function getDeploymentToAdd(frontend, existingDeployment) {
@@ -985,10 +1051,7 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
985
1051
  options,
986
1052
  initialValue: DEFAULT_CONFIG.webDeploy
987
1053
  });
988
- if (isCancel(response)) {
989
- cancel(pc.red("Operation cancelled"));
990
- process.exit(0);
991
- }
1054
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
992
1055
  return response;
993
1056
  }
994
1057
 
@@ -1010,10 +1073,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1010
1073
  git: () => getGitChoice(flags.git),
1011
1074
  packageManager: () => getPackageManagerChoice(flags.packageManager),
1012
1075
  install: () => getinstallChoice(flags.install)
1013
- }, { onCancel: () => {
1014
- cancel(pc.red("Operation cancelled"));
1015
- process.exit(0);
1016
- } });
1076
+ }, { onCancel: () => exitCancelled("Operation cancelled") });
1017
1077
  if (result.backend === "convex") {
1018
1078
  result.runtime = "none";
1019
1079
  result.database = "none";
@@ -1057,6 +1117,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1057
1117
 
1058
1118
  //#endregion
1059
1119
  //#region src/prompts/project-name.ts
1120
+ function isPathWithinCwd(targetPath) {
1121
+ const resolved = path.resolve(targetPath);
1122
+ const rel = path.relative(process.cwd(), resolved);
1123
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
1124
+ }
1060
1125
  function validateDirectoryName(name) {
1061
1126
  if (name === ".") return void 0;
1062
1127
  const result = ProjectNameSchema.safeParse(name);
@@ -1068,7 +1133,11 @@ async function getProjectName(initialName) {
1068
1133
  if (initialName === ".") return initialName;
1069
1134
  const finalDirName = path.basename(initialName);
1070
1135
  const validationError = validateDirectoryName(finalDirName);
1071
- if (!validationError) return initialName;
1136
+ if (!validationError) {
1137
+ const projectDir = path.resolve(process.cwd(), initialName);
1138
+ if (isPathWithinCwd(projectDir)) return initialName;
1139
+ consola.error(pc.red("Project path must be within current directory"));
1140
+ }
1072
1141
  }
1073
1142
  let isValid = false;
1074
1143
  let projectPath = "";
@@ -1091,15 +1160,12 @@ async function getProjectName(initialName) {
1091
1160
  if (validationError) return validationError;
1092
1161
  if (nameToUse !== ".") {
1093
1162
  const projectDir = path.resolve(process.cwd(), nameToUse);
1094
- if (!projectDir.startsWith(process.cwd())) return "Project path must be within current directory";
1163
+ if (!isPathWithinCwd(projectDir)) return "Project path must be within current directory";
1095
1164
  }
1096
1165
  return void 0;
1097
1166
  }
1098
1167
  });
1099
- if (isCancel(response)) {
1100
- cancel(pc.red("Operation cancelled."));
1101
- process.exit(0);
1102
- }
1168
+ if (isCancel(response)) return exitCancelled("Operation cancelled.");
1103
1169
  projectPath = response || defaultName;
1104
1170
  isValid = true;
1105
1171
  }
@@ -1224,11 +1290,11 @@ function generateReproducibleCommand(config) {
1224
1290
  flags.push(config.git ? "--git" : "--no-git");
1225
1291
  flags.push(`--package-manager ${config.packageManager}`);
1226
1292
  flags.push(config.install ? "--install" : "--no-install");
1227
- let baseCommand = "";
1293
+ let baseCommand = "npx create-better-t-stack@latest";
1228
1294
  const pkgManager = config.packageManager;
1229
- if (pkgManager === "npm") baseCommand = "npx create-better-t-stack@latest";
1295
+ if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
1230
1296
  else if (pkgManager === "pnpm") baseCommand = "pnpm create better-t-stack@latest";
1231
- else if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
1297
+ else if (pkgManager === "npm") baseCommand = "npx create-better-t-stack@latest";
1232
1298
  const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
1233
1299
  return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
1234
1300
  }
@@ -1271,10 +1337,7 @@ async function handleDirectoryConflict(currentPathInput) {
1271
1337
  ],
1272
1338
  initialValue: "rename"
1273
1339
  });
1274
- if (isCancel(action)) {
1275
- cancel(pc.red("Operation cancelled."));
1276
- process.exit(0);
1277
- }
1340
+ if (isCancel(action)) return exitCancelled("Operation cancelled.");
1278
1341
  switch (action) {
1279
1342
  case "overwrite": return {
1280
1343
  finalPathInput: currentPathInput,
@@ -1291,9 +1354,7 @@ async function handleDirectoryConflict(currentPathInput) {
1291
1354
  const newPathInput = await getProjectName(void 0);
1292
1355
  return await handleDirectoryConflict(newPathInput);
1293
1356
  }
1294
- case "cancel":
1295
- cancel(pc.red("Operation cancelled."));
1296
- process.exit(0);
1357
+ case "cancel": return exitCancelled("Operation cancelled.");
1297
1358
  }
1298
1359
  }
1299
1360
  }
@@ -1315,8 +1376,7 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
1315
1376
  s.stop(`Directory "${finalResolvedPath}" cleared.`);
1316
1377
  } catch (error) {
1317
1378
  s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
1318
- consola$1.error(error);
1319
- process.exit(1);
1379
+ handleError(error);
1320
1380
  }
1321
1381
  } else await fs.ensureDir(finalResolvedPath);
1322
1382
  return {
@@ -1376,18 +1436,12 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1376
1436
  if (options.api) {
1377
1437
  config.api = options.api;
1378
1438
  if (options.api === "none") {
1379
- if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") {
1380
- consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1381
- process.exit(1);
1382
- }
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.");
1383
1440
  }
1384
1441
  }
1385
1442
  if (options.backend) config.backend = options.backend;
1386
1443
  if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
1387
- if (providedFlags.has("runtime") && options.runtime === "none") {
1388
- consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1389
- process.exit(1);
1390
- }
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.`);
1391
1445
  }
1392
1446
  if (options.database) config.database = options.database;
1393
1447
  if (options.orm) config.orm = options.orm;
@@ -1400,210 +1454,68 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1400
1454
  if (options.webDeploy) config.webDeploy = options.webDeploy;
1401
1455
  if (projectName) {
1402
1456
  const result = ProjectNameSchema.safeParse(path.basename(projectName));
1403
- if (!result.success) {
1404
- consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1405
- process.exit(1);
1406
- }
1457
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1407
1458
  config.projectName = projectName;
1408
1459
  } else if (options.projectDirectory) {
1409
1460
  const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
1410
1461
  const result = ProjectNameSchema.safeParse(baseName);
1411
- if (!result.success) {
1412
- consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1413
- process.exit(1);
1414
- }
1462
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1415
1463
  config.projectName = baseName;
1416
1464
  }
1417
1465
  if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
1418
- if (options.frontend.length > 1) {
1419
- consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
1420
- process.exit(1);
1421
- }
1466
+ if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
1422
1467
  config.frontend = [];
1423
1468
  } else {
1424
1469
  const validOptions = options.frontend.filter((f) => f !== "none");
1425
- const webFrontends = validOptions.filter((f) => WEB_FRAMEWORKS.includes(f));
1426
- const nativeFrontends = validOptions.filter((f) => f === "native-nativewind" || f === "native-unistyles");
1427
- if (webFrontends.length > 1) {
1428
- consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
1429
- process.exit(1);
1430
- }
1431
- if (nativeFrontends.length > 1) {
1432
- consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
1433
- process.exit(1);
1434
- }
1470
+ ensureSingleWebAndNative(validOptions);
1435
1471
  config.frontend = validOptions;
1436
1472
  }
1473
+ if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
1437
1474
  if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
1438
- if (options.addons.length > 1) {
1439
- consola$1.fatal(`Cannot combine 'none' with other addons.`);
1440
- process.exit(1);
1441
- }
1475
+ if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
1442
1476
  config.addons = [];
1443
1477
  } else config.addons = options.addons.filter((addon) => addon !== "none");
1444
1478
  if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
1445
- if (options.examples.length > 1) {
1446
- consola$1.fatal("Cannot combine 'none' with other examples.");
1447
- process.exit(1);
1448
- }
1479
+ if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
1449
1480
  config.examples = [];
1450
1481
  } else {
1451
1482
  config.examples = options.examples.filter((ex) => ex !== "none");
1452
1483
  if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
1453
1484
  }
1454
- if (config.backend === "convex") {
1455
- const incompatibleFlags = [];
1456
- if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
1457
- if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
1458
- if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
1459
- if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
1460
- if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
1461
- if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
1462
- if (incompatibleFlags.length > 0) {
1463
- consola$1.fatal(`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(", ")}. Please remove them.`);
1464
- process.exit(1);
1465
- }
1466
- if (providedFlags.has("frontend") && options.frontend) {
1485
+ if (config.backend === "convex" || config.backend === "none") {
1486
+ const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
1487
+ if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1488
+ if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1467
1489
  const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
1468
- if (incompatibleFrontends.length > 0) {
1469
- consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1470
- process.exit(1);
1471
- }
1490
+ if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1472
1491
  }
1473
- config.auth = false;
1474
- config.database = "none";
1475
- config.orm = "none";
1476
- config.api = "none";
1477
- config.runtime = "none";
1478
- config.dbSetup = "none";
1479
- config.examples = ["todo"];
1480
- } else if (config.backend === "none") {
1481
- const incompatibleFlags = [];
1482
- if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
1483
- if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
1484
- if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
1485
- if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
1486
- if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
1487
- if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
1488
- if (providedFlags.has("examples") && options.examples) {
1489
- const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
1490
- if (hasNonNoneExamples) incompatibleFlags.push("--examples");
1491
- }
1492
- if (incompatibleFlags.length > 0) {
1493
- consola$1.fatal(`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(", ")}. Please remove them.`);
1494
- process.exit(1);
1495
- }
1496
- config.auth = false;
1497
- config.database = "none";
1498
- config.orm = "none";
1499
- config.api = "none";
1500
- config.runtime = "none";
1501
- config.dbSetup = "none";
1502
- config.examples = [];
1503
- }
1504
- if (config.orm === "mongoose" && config.database !== "mongodb") {
1505
- consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1506
- process.exit(1);
1507
- }
1508
- if (config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
1509
- consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1510
- process.exit(1);
1511
- }
1512
- if (config.orm === "drizzle" && config.database === "mongodb") {
1513
- consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1514
- process.exit(1);
1515
- }
1516
- if (config.database && config.database !== "none" && config.orm === "none") {
1517
- consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1518
- process.exit(1);
1519
- }
1520
- if (config.orm && config.orm !== "none" && config.database === "none") {
1521
- consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
1522
- process.exit(1);
1523
- }
1524
- if (config.auth && config.database === "none") {
1525
- consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
1526
- process.exit(1);
1527
- }
1528
- if (config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
1529
- consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1530
- process.exit(1);
1531
- }
1532
- if (config.dbSetup === "turso" && config.database !== "sqlite") {
1533
- consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1534
- process.exit(1);
1535
- }
1536
- if (config.dbSetup === "neon" && config.database !== "postgres") {
1537
- consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1538
- process.exit(1);
1539
- }
1540
- if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
1541
- consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1542
- process.exit(1);
1543
- }
1544
- if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
1545
- consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1546
- process.exit(1);
1547
- }
1548
- if (config.dbSetup === "supabase" && config.database !== "postgres") {
1549
- consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1550
- process.exit(1);
1551
- }
1492
+ coerceBackendPresets(config);
1493
+ }
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.");
1552
1506
  if (config.dbSetup === "d1") {
1553
- if (config.database !== "sqlite") {
1554
- consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1555
- process.exit(1);
1507
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
1508
+ if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1556
1509
  }
1557
- if (config.runtime !== "workers") {
1558
- consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1559
- process.exit(1);
1510
+ if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
1511
+ if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1560
1512
  }
1561
1513
  }
1562
- if (config.dbSetup === "docker" && config.database === "sqlite") {
1563
- 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.");
1564
- process.exit(1);
1565
- }
1566
- if (config.dbSetup === "docker" && config.runtime === "workers") {
1567
- 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.");
1568
- process.exit(1);
1569
- }
1570
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
1571
- 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.`);
1572
- process.exit(1);
1573
- }
1574
- if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
1575
- 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.`);
1576
- process.exit(1);
1577
- }
1578
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
1579
- 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.`);
1580
- process.exit(1);
1581
- }
1582
- if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
1583
- 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.`);
1584
- process.exit(1);
1585
- }
1586
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
1587
- 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.");
1588
- process.exit(1);
1589
- }
1590
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
1591
- 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.");
1592
- process.exit(1);
1593
- }
1594
- if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
1595
- 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.");
1596
- process.exit(1);
1597
- }
1598
- if (providedFlags.has("db-setup") && options.dbSetup === "docker" && config.runtime === "workers") {
1599
- 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.");
1600
- process.exit(1);
1601
- }
1602
- const hasWebFrontendFlag = (config.frontend ?? []).some((f) => WEB_FRAMEWORKS.includes(f));
1603
- if (config.webDeploy && config.webDeploy !== "none" && !hasWebFrontendFlag && providedFlags.has("frontend")) {
1604
- consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
1605
- process.exit(1);
1606
- }
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.");
1516
+ validateWorkersCompatibility(providedFlags, options, config);
1517
+ const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
1518
+ validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
1607
1519
  return config;
1608
1520
  }
1609
1521
  function getProvidedFlags(options) {
@@ -1774,12 +1686,9 @@ async function setupFumadocs(config) {
1774
1686
  })),
1775
1687
  initialValue: "next-mdx"
1776
1688
  });
1777
- if (isCancel(template)) {
1778
- cancel(pc.red("Operation cancelled"));
1779
- process.exit(0);
1780
- }
1689
+ if (isCancel(template)) return exitCancelled("Operation cancelled");
1781
1690
  const templateArg = TEMPLATES[template].value;
1782
- const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint`;
1691
+ const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint --no-git`;
1783
1692
  const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
1784
1693
  await execa(fumadocsInitCommand, {
1785
1694
  cwd: path.join(projectDir, "apps"),
@@ -1941,10 +1850,7 @@ async function setupUltracite(config, hasHusky) {
1941
1850
  })),
1942
1851
  required: false
1943
1852
  });
1944
- if (isCancel(editors)) {
1945
- cancel(pc.red("Operation cancelled"));
1946
- process.exit(0);
1947
- }
1853
+ if (isCancel(editors)) return exitCancelled("Operation cancelled");
1948
1854
  const rules = await multiselect({
1949
1855
  message: "Choose rules",
1950
1856
  options: Object.entries(RULES).map(([key, rule]) => ({
@@ -1954,10 +1860,7 @@ async function setupUltracite(config, hasHusky) {
1954
1860
  })),
1955
1861
  required: false
1956
1862
  });
1957
- if (isCancel(rules)) {
1958
- cancel(pc.red("Operation cancelled"));
1959
- process.exit(0);
1960
- }
1863
+ if (isCancel(rules)) return exitCancelled("Operation cancelled");
1961
1864
  const ultraciteArgs = [
1962
1865
  "init",
1963
1866
  "--pm",
@@ -1989,6 +1892,135 @@ async function setupUltracite(config, hasHusky) {
1989
1892
  }
1990
1893
  }
1991
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
+
1992
2024
  //#endregion
1993
2025
  //#region src/utils/ts-morph.ts
1994
2026
  const tsProject = new Project({
@@ -2080,6 +2112,7 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2080
2112
  }
2081
2113
  if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2082
2114
  if (addons.includes("starlight")) await setupStarlight(config);
2115
+ if (addons.includes("vibe-rules")) await setupVibeRules(config);
2083
2116
  if (addons.includes("fumadocs")) await setupFumadocs(config);
2084
2117
  }
2085
2118
  function getWebAppDir(projectDir, frontends) {
@@ -2224,31 +2257,6 @@ async function installDependencies({ projectDir, packageManager }) {
2224
2257
  }
2225
2258
  }
2226
2259
 
2227
- //#endregion
2228
- //#region src/utils/template-processor.ts
2229
- /**
2230
- * Processes a Handlebars template file and writes the output to the destination.
2231
- * @param srcPath Path to the source .hbs template file.
2232
- * @param destPath Path to write the processed file.
2233
- * @param context Data to be passed to the Handlebars template.
2234
- */
2235
- async function processTemplate(srcPath, destPath, context) {
2236
- try {
2237
- const templateContent = await fs.readFile(srcPath, "utf-8");
2238
- const template = handlebars.compile(templateContent);
2239
- const processedContent = template(context);
2240
- await fs.ensureDir(path.dirname(destPath));
2241
- await fs.writeFile(destPath, processedContent);
2242
- } catch (error) {
2243
- consola.error(`Error processing template ${srcPath}:`, error);
2244
- throw new Error(`Failed to process template ${srcPath}`);
2245
- }
2246
- }
2247
- handlebars.registerHelper("eq", (a, b) => a === b);
2248
- handlebars.registerHelper("and", (a, b) => a && b);
2249
- handlebars.registerHelper("or", (a, b) => a || b);
2250
- handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
2251
-
2252
2260
  //#endregion
2253
2261
  //#region src/helpers/project-generation/template-manager.ts
2254
2262
  async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
@@ -2459,6 +2467,7 @@ async function setupAddonsTemplate(projectDir, context) {
2459
2467
  if (!context.addons || context.addons.length === 0) return;
2460
2468
  for (const addon of context.addons) {
2461
2469
  if (addon === "none") continue;
2470
+ if (addon === "vibe-rules") continue;
2462
2471
  let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
2463
2472
  let addonDestDir = projectDir;
2464
2473
  if (addon === "pwa") {
@@ -2619,17 +2628,13 @@ async function setupDeploymentTemplates(projectDir, context) {
2619
2628
 
2620
2629
  //#endregion
2621
2630
  //#region src/helpers/project-generation/add-addons.ts
2622
- function exitWithError$1(message) {
2623
- cancel(pc.red(message));
2624
- process.exit(1);
2625
- }
2626
2631
  async function addAddonsToProject(input) {
2627
2632
  try {
2628
2633
  const projectDir = input.projectDir || process.cwd();
2629
2634
  const isBetterTStack = await isBetterTStackProject(projectDir);
2630
- 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.");
2631
2636
  const detectedConfig = await detectProjectConfig(projectDir);
2632
- 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.");
2633
2638
  const config = {
2634
2639
  projectName: detectedConfig.projectName || path.basename(projectDir),
2635
2640
  projectDir,
@@ -2651,9 +2656,8 @@ async function addAddonsToProject(input) {
2651
2656
  };
2652
2657
  for (const addon of input.addons) {
2653
2658
  const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
2654
- 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`);
2655
2660
  }
2656
- log.info(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`);
2657
2661
  await setupAddonsTemplate(projectDir, config);
2658
2662
  await setupAddons(config, true);
2659
2663
  const currentAddons = detectedConfig.addons || [];
@@ -2666,7 +2670,7 @@ async function addAddonsToProject(input) {
2666
2670
  else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
2667
2671
  } catch (error) {
2668
2672
  const message = error instanceof Error ? error.message : String(error);
2669
- exitWithError$1(`Error adding addons: ${message}`);
2673
+ exitWithError(`Error adding addons: ${message}`);
2670
2674
  }
2671
2675
  }
2672
2676
 
@@ -2899,10 +2903,6 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
2899
2903
 
2900
2904
  //#endregion
2901
2905
  //#region src/helpers/project-generation/add-deployment.ts
2902
- function exitWithError(message) {
2903
- cancel(pc.red(message));
2904
- process.exit(1);
2905
- }
2906
2906
  async function addDeploymentToProject(input) {
2907
2907
  try {
2908
2908
  const projectDir = input.projectDir || process.cwd();
@@ -3702,10 +3702,7 @@ async function setupNeonPostgres(config) {
3702
3702
  }],
3703
3703
  initialValue: "neondb"
3704
3704
  });
3705
- if (isCancel(setupMethod)) {
3706
- cancel(pc.red("Operation cancelled"));
3707
- process.exit(0);
3708
- }
3705
+ if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
3709
3706
  if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager);
3710
3707
  else {
3711
3708
  const suggestedProjectName = path.basename(projectDir);
@@ -3719,10 +3716,7 @@ async function setupNeonPostgres(config) {
3719
3716
  options: NEON_REGIONS,
3720
3717
  initialValue: NEON_REGIONS[0].value
3721
3718
  });
3722
- if (isCancel(projectName) || isCancel(regionId)) {
3723
- cancel(pc.red("Operation cancelled"));
3724
- process.exit(0);
3725
- }
3719
+ if (isCancel(projectName) || isCancel(regionId)) return exitCancelled("Operation cancelled");
3726
3720
  const neonConfig = await createNeonProject(projectName, regionId, packageManager);
3727
3721
  if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
3728
3722
  const finalSpinner = spinner();
@@ -3757,10 +3751,7 @@ async function setupWithCreateDb(serverDir, packageManager, orm) {
3757
3751
  if (orm === "drizzle" && !value.includes("?sslmode=require")) return "Please append ?sslmode=require to your database URL when using Drizzle";
3758
3752
  }
3759
3753
  });
3760
- if (isCancel(databaseUrl)) {
3761
- cancel("Database setup cancelled");
3762
- return null;
3763
- }
3754
+ if (isCancel(databaseUrl)) return null;
3764
3755
  return { databaseUrl };
3765
3756
  } catch (error) {
3766
3757
  if (error instanceof Error) consola$1.error(error.message);
@@ -3786,10 +3777,7 @@ async function initPrismaDatabase(serverDir, packageManager) {
3786
3777
  if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://";
3787
3778
  }
3788
3779
  });
3789
- if (isCancel(databaseUrl)) {
3790
- cancel("Database setup cancelled");
3791
- return null;
3792
- }
3780
+ if (isCancel(databaseUrl)) return null;
3793
3781
  return { databaseUrl };
3794
3782
  } catch (error) {
3795
3783
  if (error instanceof Error) consola$1.error(error.message);
@@ -3880,10 +3868,7 @@ async function setupPrismaPostgres(config) {
3880
3868
  options: setupOptions,
3881
3869
  initialValue: "create-db"
3882
3870
  });
3883
- if (isCancel(setupMethod)) {
3884
- cancel(pc.red("Operation cancelled"));
3885
- process.exit(0);
3886
- }
3871
+ if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
3887
3872
  let prismaConfig = null;
3888
3873
  if (setupMethod === "create-db") prismaConfig = await setupWithCreateDb(serverDir, packageManager, orm);
3889
3874
  else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
@@ -4117,10 +4102,7 @@ async function selectTursoGroup() {
4117
4102
  message: "Select a Turso database group:",
4118
4103
  options: groupOptions
4119
4104
  });
4120
- if (isCancel(selectedGroup)) {
4121
- cancel(pc.red("Operation cancelled"));
4122
- process.exit(0);
4123
- }
4105
+ if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
4124
4106
  return selectedGroup;
4125
4107
  }
4126
4108
  async function createTursoDatabase(dbName, groupName) {
@@ -4193,10 +4175,7 @@ async function setupTurso(config) {
4193
4175
  message: "Would you like to install Turso CLI?",
4194
4176
  initialValue: true
4195
4177
  });
4196
- if (isCancel(shouldInstall)) {
4197
- cancel(pc.red("Operation cancelled"));
4198
- process.exit(0);
4199
- }
4178
+ if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
4200
4179
  if (!shouldInstall) {
4201
4180
  await writeEnvFile(projectDir);
4202
4181
  displayManualSetupInstructions();
@@ -4217,10 +4196,7 @@ async function setupTurso(config) {
4217
4196
  initialValue: suggestedName,
4218
4197
  placeholder: suggestedName
4219
4198
  });
4220
- if (isCancel(dbNameResponse)) {
4221
- cancel(pc.red("Operation cancelled"));
4222
- process.exit(0);
4223
- }
4199
+ if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
4224
4200
  dbName = dbNameResponse;
4225
4201
  try {
4226
4202
  const config$1 = await createTursoDatabase(dbName, selectedGroup);
@@ -5137,13 +5113,11 @@ async function createProject(options) {
5137
5113
  return projectDir;
5138
5114
  } catch (error) {
5139
5115
  if (error instanceof Error) {
5140
- cancel(pc.red(`Error during project creation: ${error.message}`));
5141
5116
  console.error(error.stack);
5142
- process.exit(1);
5117
+ exitWithError(`Error during project creation: ${error.message}`);
5143
5118
  } else {
5144
- cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
5145
5119
  console.error(error);
5146
- process.exit(1);
5120
+ exitWithError(`An unexpected error occurred: ${String(error)}`);
5147
5121
  }
5148
5122
  }
5149
5123
  }
@@ -5202,18 +5176,14 @@ async function createProjectHandler(input) {
5202
5176
  const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
5203
5177
  outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
5204
5178
  } catch (error) {
5205
- console.error(error);
5206
- process.exit(1);
5179
+ handleError(error, "Failed to create project");
5207
5180
  }
5208
5181
  }
5209
5182
  async function addAddonsHandler(input) {
5210
5183
  try {
5211
5184
  const projectDir = input.projectDir || process.cwd();
5212
5185
  const detectedConfig = await detectProjectConfig(projectDir);
5213
- if (!detectedConfig) {
5214
- cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
5215
- process.exit(1);
5216
- }
5186
+ if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T Stack project.");
5217
5187
  if (!input.addons || input.addons.length === 0) {
5218
5188
  const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
5219
5189
  if (addonsPrompt.length > 0) input.addons = addonsPrompt;
@@ -5253,8 +5223,7 @@ async function addAddonsHandler(input) {
5253
5223
  else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
5254
5224
  outro("Add command completed successfully!");
5255
5225
  } catch (error) {
5256
- console.error(error);
5257
- process.exit(1);
5226
+ handleError(error, "Failed to add addons or deployment");
5258
5227
  }
5259
5228
  }
5260
5229
 
@@ -5369,8 +5338,7 @@ const router = t.router({
5369
5338
  const sponsors = await fetchSponsors();
5370
5339
  displaySponsors(sponsors);
5371
5340
  } catch (error) {
5372
- consola$1.error(error);
5373
- process.exit(1);
5341
+ handleError(error, "Failed to display sponsors");
5374
5342
  }
5375
5343
  }),
5376
5344
  docs: t.procedure.meta({ description: "Open Better-T Stack documentation" }).mutation(async () => {