create-better-t-stack 3.12.7 → 3.13.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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __reExport } from "./chunk-Dt3mZKp0.mjs";
3
- import { autocompleteMultiselect, cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
3
+ import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
4
4
  import { createRouterClient, os } from "@orpc/server";
5
5
  import pc from "picocolors";
6
6
  import { createCli } from "trpc-cli";
@@ -9,6 +9,8 @@ import consola, { consola as consola$1 } from "consola";
9
9
  import fs from "fs-extra";
10
10
  import path from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
13
+ import { AsyncLocalStorage } from "node:async_hooks";
12
14
  import gradient from "gradient-string";
13
15
  import * as JSONC from "jsonc-parser";
14
16
  import { $, execa } from "execa";
@@ -215,18 +217,30 @@ const WEB_FRAMEWORKS = [
215
217
 
216
218
  //#endregion
217
219
  //#region src/utils/errors.ts
220
+ var UserCancelledError = class extends Error {
221
+ constructor(message = "Operation cancelled") {
222
+ super(message);
223
+ this.name = "UserCancelledError";
224
+ }
225
+ };
226
+ var CLIError = class extends Error {
227
+ constructor(message) {
228
+ super(message);
229
+ this.name = "CLIError";
230
+ }
231
+ };
218
232
  function exitWithError(message) {
219
233
  consola.error(pc.red(message));
220
- throw new Error(message);
234
+ throw new CLIError(message);
221
235
  }
222
236
  function exitCancelled(message = "Operation cancelled") {
223
237
  cancel(pc.red(message));
224
- throw new Error(message);
238
+ throw new UserCancelledError(message);
225
239
  }
226
240
  function handleError(error, fallbackMessage) {
227
241
  const message = error instanceof Error ? error.message : fallbackMessage || String(error);
228
242
  consola.error(pc.red(message));
229
- throw new Error(message);
243
+ throw error instanceof Error ? error : new Error(message);
230
244
  }
231
245
 
232
246
  //#endregion
@@ -362,6 +376,295 @@ function validateExamplesCompatibility(examples, backend, database, frontend, ap
362
376
  }
363
377
  }
364
378
 
379
+ //#endregion
380
+ //#region src/utils/context.ts
381
+ const cliStorage = new AsyncLocalStorage();
382
+ function defaultContext() {
383
+ return {
384
+ navigation: {
385
+ isFirstPrompt: false,
386
+ lastPromptShownUI: false
387
+ },
388
+ silent: false,
389
+ verbose: false
390
+ };
391
+ }
392
+ function getContext() {
393
+ const ctx = cliStorage.getStore();
394
+ if (!ctx) return defaultContext();
395
+ return ctx;
396
+ }
397
+ function tryGetContext() {
398
+ return cliStorage.getStore();
399
+ }
400
+ function isSilent() {
401
+ return getContext().silent;
402
+ }
403
+ function isFirstPrompt() {
404
+ return getContext().navigation.isFirstPrompt;
405
+ }
406
+ function didLastPromptShowUI() {
407
+ return getContext().navigation.lastPromptShownUI;
408
+ }
409
+ function setIsFirstPrompt$1(value) {
410
+ const ctx = tryGetContext();
411
+ if (ctx) ctx.navigation.isFirstPrompt = value;
412
+ }
413
+ function setLastPromptShownUI(value) {
414
+ const ctx = tryGetContext();
415
+ if (ctx) ctx.navigation.lastPromptShownUI = value;
416
+ }
417
+ async function runWithContextAsync(options, fn) {
418
+ const ctx = {
419
+ navigation: {
420
+ isFirstPrompt: false,
421
+ lastPromptShownUI: false
422
+ },
423
+ silent: options.silent ?? false,
424
+ verbose: options.verbose ?? false,
425
+ projectDir: options.projectDir,
426
+ projectName: options.projectName,
427
+ packageManager: options.packageManager
428
+ };
429
+ return cliStorage.run(ctx, fn);
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/utils/navigation.ts
434
+ /**
435
+ * Navigation symbols and utilities for prompt navigation
436
+ */
437
+ const GO_BACK_SYMBOL = Symbol("clack:goBack");
438
+ function isGoBack(value) {
439
+ return value === GO_BACK_SYMBOL;
440
+ }
441
+
442
+ //#endregion
443
+ //#region src/prompts/navigable.ts
444
+ /**
445
+ * Navigable prompt wrappers using @clack/core
446
+ * These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
447
+ */
448
+ const unicode = process.platform !== "win32";
449
+ const S_STEP_ACTIVE = unicode ? "◆" : "*";
450
+ const S_STEP_CANCEL = unicode ? "■" : "x";
451
+ const S_STEP_ERROR = unicode ? "▲" : "x";
452
+ const S_STEP_SUBMIT = unicode ? "◇" : "o";
453
+ const S_BAR = unicode ? "│" : "|";
454
+ const S_BAR_END = unicode ? "└" : "—";
455
+ const S_RADIO_ACTIVE = unicode ? "●" : ">";
456
+ const S_RADIO_INACTIVE = unicode ? "○" : " ";
457
+ const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
458
+ const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
459
+ const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
460
+ function symbol(state) {
461
+ switch (state) {
462
+ case "initial":
463
+ case "active": return pc.cyan(S_STEP_ACTIVE);
464
+ case "cancel": return pc.red(S_STEP_CANCEL);
465
+ case "error": return pc.yellow(S_STEP_ERROR);
466
+ case "submit": return pc.green(S_STEP_SUBMIT);
467
+ }
468
+ }
469
+ const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
470
+ const KEYBOARD_HINT_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
471
+ const KEYBOARD_HINT_MULTI = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
472
+ const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
473
+ const setIsFirstPrompt = setIsFirstPrompt$1;
474
+ function getHint() {
475
+ return isFirstPrompt() ? KEYBOARD_HINT_FIRST : KEYBOARD_HINT;
476
+ }
477
+ function getMultiHint() {
478
+ return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
479
+ }
480
+ async function runWithNavigation(prompt) {
481
+ let goBack = false;
482
+ prompt.on("key", (char) => {
483
+ if (char === "b" && !isFirstPrompt()) {
484
+ goBack = true;
485
+ prompt.state = "cancel";
486
+ }
487
+ });
488
+ setLastPromptShownUI(true);
489
+ const result = await prompt.prompt();
490
+ return goBack ? GO_BACK_SYMBOL : result;
491
+ }
492
+ async function navigableSelect(opts) {
493
+ const opt = (option, state) => {
494
+ const label = option.label ?? String(option.value);
495
+ switch (state) {
496
+ case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
497
+ case "selected": return `${pc.dim(label)}`;
498
+ case "active": return `${pc.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
499
+ case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
500
+ default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
501
+ }
502
+ };
503
+ return runWithNavigation(new SelectPrompt({
504
+ options: opts.options,
505
+ initialValue: opts.initialValue,
506
+ render() {
507
+ const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
508
+ switch (this.state) {
509
+ case "submit": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
510
+ case "cancel": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR)}`;
511
+ default: {
512
+ const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
513
+ const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
514
+ return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
515
+ }
516
+ }
517
+ }
518
+ }));
519
+ }
520
+ async function navigableMultiselect(opts) {
521
+ const required = opts.required ?? true;
522
+ const opt = (option, state) => {
523
+ const label = option.label ?? String(option.value);
524
+ if (state === "disabled") return `${pc.gray(S_CHECKBOX_INACTIVE)} ${pc.strikethrough(pc.gray(label))}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
525
+ if (state === "active") return `${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
526
+ if (state === "selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
527
+ if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
528
+ if (state === "active-selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
529
+ if (state === "submitted") return `${pc.dim(label)}`;
530
+ return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
531
+ };
532
+ return runWithNavigation(new MultiSelectPrompt({
533
+ options: opts.options,
534
+ initialValues: opts.initialValues,
535
+ required,
536
+ validate(selected) {
537
+ if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
538
+ },
539
+ render() {
540
+ const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
541
+ const value = this.value ?? [];
542
+ const styleOption = (option, active) => {
543
+ if (option.disabled) return opt(option, "disabled");
544
+ const selected = value.includes(option.value);
545
+ if (active && selected) return opt(option, "active-selected");
546
+ if (selected) return opt(option, "selected");
547
+ return opt(option, active ? "active" : "inactive");
548
+ };
549
+ switch (this.state) {
550
+ case "submit": {
551
+ const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
552
+ return `${title}${pc.gray(S_BAR)} ${submitText}`;
553
+ }
554
+ case "cancel": {
555
+ const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
556
+ return `${title}${pc.gray(S_BAR)} ${label}\n${pc.gray(S_BAR)}`;
557
+ }
558
+ case "error": {
559
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
560
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
561
+ return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
562
+ }
563
+ default: {
564
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
565
+ const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
566
+ return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
567
+ }
568
+ }
569
+ }
570
+ }));
571
+ }
572
+ async function navigableConfirm(opts) {
573
+ const active = opts.active ?? "Yes";
574
+ const inactive = opts.inactive ?? "No";
575
+ return runWithNavigation(new ConfirmPrompt({
576
+ active,
577
+ inactive,
578
+ initialValue: opts.initialValue ?? true,
579
+ render() {
580
+ const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
581
+ const value = this.value ? active : inactive;
582
+ switch (this.state) {
583
+ case "submit": return `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
584
+ case "cancel": return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR)}`;
585
+ default: {
586
+ const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
587
+ return `${title}${pc.cyan(S_BAR)} ${this.value ? `${pc.green(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.green(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${pc.cyan(S_BAR_END)}${hint}\n`;
588
+ }
589
+ }
590
+ }
591
+ }));
592
+ }
593
+ async function navigableGroupMultiselect(opts) {
594
+ const required = opts.required ?? true;
595
+ const opt = (option, state, options = []) => {
596
+ const label = option.label ?? String(option.value);
597
+ const isItem = typeof option.group === "string";
598
+ const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
599
+ const isLast = isItem && next && next.group === true;
600
+ const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
601
+ if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
602
+ if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
603
+ if (state === "group-active-selected") return `${prefix}${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
604
+ if (state === "selected") {
605
+ const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
606
+ return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
607
+ }
608
+ if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
609
+ if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
610
+ if (state === "submitted") return `${pc.dim(label)}`;
611
+ const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
612
+ return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
613
+ };
614
+ return runWithNavigation(new GroupMultiSelectPrompt({
615
+ options: opts.options,
616
+ initialValues: opts.initialValues,
617
+ required,
618
+ selectableGroups: true,
619
+ validate(selected) {
620
+ if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
621
+ },
622
+ render() {
623
+ const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
624
+ const value = this.value ?? [];
625
+ switch (this.state) {
626
+ case "submit": {
627
+ const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
628
+ const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
629
+ return `${title}${pc.gray(S_BAR)}${optionsText}`;
630
+ }
631
+ case "cancel": {
632
+ const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
633
+ return `${title}${pc.gray(S_BAR)} ${label.trim() ? `${label}\n${pc.gray(S_BAR)}` : ""}`;
634
+ }
635
+ case "error": {
636
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
637
+ const optionsText = this.options.map((option, i, options) => {
638
+ const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
639
+ const active = i === this.cursor;
640
+ if (!active && typeof option.group === "string" && this.options[this.cursor].value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", options);
641
+ if (active && selected) return opt(option, "active-selected", options);
642
+ if (selected) return opt(option, "selected", options);
643
+ return opt(option, active ? "active" : "inactive", options);
644
+ }).join(`\n${pc.yellow(S_BAR)} `);
645
+ return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
646
+ }
647
+ default: {
648
+ const optionsText = this.options.map((option, i, options) => {
649
+ const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
650
+ const active = i === this.cursor;
651
+ const groupActive = !active && typeof option.group === "string" && this.options[this.cursor].value === option.group;
652
+ let optionText = "";
653
+ if (groupActive) optionText = opt(option, selected ? "group-active-selected" : "group-active", options);
654
+ else if (active && selected) optionText = opt(option, "active-selected", options);
655
+ else if (selected) optionText = opt(option, "selected", options);
656
+ else optionText = opt(option, active ? "active" : "inactive", options);
657
+ return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
658
+ }).join(`\n${pc.cyan(S_BAR)}`);
659
+ const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
660
+ const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
661
+ return `${title}${pc.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
662
+ }
663
+ }
664
+ }
665
+ }));
666
+ }
667
+
365
668
  //#endregion
366
669
  //#region src/prompts/addons.ts
367
670
  function getAddonDisplay(addon) {
@@ -473,14 +776,13 @@ async function getAddonsChoice(addons, frontends, auth) {
473
776
  });
474
777
  }
475
778
  });
476
- const response = await groupMultiselect({
779
+ const response = await navigableGroupMultiselect({
477
780
  message: "Select addons",
478
781
  options: groupedOptions,
479
782
  initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
480
- required: false,
481
- selectableGroups: false
783
+ required: false
482
784
  });
483
- if (isCancel(response)) return exitCancelled("Operation cancelled");
785
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
484
786
  return response;
485
787
  }
486
788
  async function getAddonsToAdd(frontend, existingAddons = [], auth) {
@@ -512,13 +814,12 @@ async function getAddonsToAdd(frontend, existingAddons = [], auth) {
512
814
  }
513
815
  });
514
816
  if (Object.keys(groupedOptions).length === 0) return [];
515
- const response = await groupMultiselect({
817
+ const response = await navigableGroupMultiselect({
516
818
  message: "Select addons to add",
517
819
  options: groupedOptions,
518
- required: false,
519
- selectableGroups: false
820
+ required: false
520
821
  });
521
- if (isCancel(response)) return exitCancelled("Operation cancelled");
822
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
522
823
  return response;
523
824
  }
524
825
 
@@ -541,12 +842,12 @@ async function getApiChoice(Api, frontend, backend) {
541
842
  label: "None",
542
843
  hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
543
844
  });
544
- const apiType = await select({
845
+ const apiType = await navigableSelect({
545
846
  message: "Select API type",
546
847
  options: apiOptions,
547
848
  initialValue: apiOptions[0].value
548
849
  });
549
- if (isCancel(apiType)) return exitCancelled("Operation cancelled");
850
+ if (isCancel$1(apiType)) return exitCancelled("Operation cancelled");
550
851
  return apiType;
551
852
  }
552
853
 
@@ -590,15 +891,15 @@ async function getAuthChoice(auth, backend, frontend) {
590
891
  label: "None",
591
892
  hint: "No auth"
592
893
  });
593
- const response$1 = await select({
894
+ const response$1 = await navigableSelect({
594
895
  message: "Select authentication provider",
595
896
  options,
596
897
  initialValue: "none"
597
898
  });
598
- if (isCancel(response$1)) return exitCancelled("Operation cancelled");
899
+ if (isCancel$1(response$1)) return exitCancelled("Operation cancelled");
599
900
  return response$1;
600
901
  }
601
- const response = await select({
902
+ const response = await navigableSelect({
602
903
  message: "Select authentication provider",
603
904
  options: [{
604
905
  value: "better-auth",
@@ -610,7 +911,7 @@ async function getAuthChoice(auth, backend, frontend) {
610
911
  }],
611
912
  initialValue: DEFAULT_CONFIG.auth
612
913
  });
613
- if (isCancel(response)) return exitCancelled("Operation cancelled");
914
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
614
915
  return response;
615
916
  }
616
917
 
@@ -654,12 +955,12 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
654
955
  label: "None",
655
956
  hint: "No backend server"
656
957
  });
657
- const response = await select({
958
+ const response = await navigableSelect({
658
959
  message: "Select backend",
659
960
  options: backendOptions,
660
961
  initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
661
962
  });
662
- if (isCancel(response)) return exitCancelled("Operation cancelled");
963
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
663
964
  return response;
664
965
  }
665
966
 
@@ -695,12 +996,12 @@ async function getDatabaseChoice(database, backend, runtime) {
695
996
  label: "MongoDB",
696
997
  hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
697
998
  });
698
- const response = await select({
999
+ const response = await navigableSelect({
699
1000
  message: "Select database",
700
1001
  options: databaseOptions,
701
1002
  initialValue: DEFAULT_CONFIG.database
702
1003
  });
703
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1004
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
704
1005
  return response;
705
1006
  }
706
1007
 
@@ -795,12 +1096,12 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
795
1096
  }
796
1097
  ];
797
1098
  else return "none";
798
- const response = await select({
1099
+ const response = await navigableSelect({
799
1100
  message: `Select ${databaseType} setup option`,
800
1101
  options,
801
1102
  initialValue: "none"
802
1103
  });
803
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1104
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
804
1105
  return response;
805
1106
  }
806
1107
 
@@ -822,13 +1123,13 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
822
1123
  hint: "A simple AI chat interface using AI SDK"
823
1124
  });
824
1125
  if (options.length === 0) return [];
825
- response = await multiselect({
1126
+ response = await navigableMultiselect({
826
1127
  message: "Include examples",
827
1128
  options,
828
1129
  required: false,
829
1130
  initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
830
1131
  });
831
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1132
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
832
1133
  return response;
833
1134
  }
834
1135
 
@@ -836,104 +1137,124 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
836
1137
  //#region src/prompts/frontend.ts
837
1138
  async function getFrontendChoice(frontendOptions, backend, auth) {
838
1139
  if (frontendOptions !== void 0) return frontendOptions;
839
- const frontendTypes = await multiselect({
840
- message: "Select project type",
841
- options: [{
842
- value: "web",
843
- label: "Web",
844
- hint: "React, Vue or Svelte Web Application"
845
- }, {
846
- value: "native",
847
- label: "Native",
848
- hint: "Create a React Native/Expo app"
849
- }],
850
- required: false,
851
- initialValues: ["web"]
852
- });
853
- if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
854
- const result = [];
855
- if (frontendTypes.includes("web")) {
856
- const webFramework = await select({
857
- message: "Choose web",
858
- options: [
859
- {
860
- value: "tanstack-router",
861
- label: "TanStack Router",
862
- hint: "Modern and scalable routing for React Applications"
863
- },
864
- {
865
- value: "react-router",
866
- label: "React Router",
867
- hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
868
- },
869
- {
870
- value: "next",
871
- label: "Next.js",
872
- hint: "The React Framework for the Web"
873
- },
874
- {
875
- value: "nuxt",
876
- label: "Nuxt",
877
- hint: "The Progressive Web Framework for Vue.js"
878
- },
879
- {
880
- value: "svelte",
881
- label: "Svelte",
882
- hint: "web development for the rest of us"
883
- },
884
- {
885
- value: "solid",
886
- label: "Solid",
887
- hint: "Simple and performant reactivity for building user interfaces"
888
- },
889
- {
890
- value: "tanstack-start",
891
- label: "TanStack Start",
892
- hint: "SSR, Server Functions, API Routes and more with TanStack Router"
893
- }
894
- ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
895
- initialValue: DEFAULT_CONFIG.frontend[0]
896
- });
897
- if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
898
- result.push(webFramework);
899
- }
900
- if (frontendTypes.includes("native")) {
901
- const nativeFramework = await select({
902
- message: "Choose native",
903
- options: [
904
- {
905
- value: "native-bare",
906
- label: "Bare",
907
- hint: "Bare Expo without styling library"
908
- },
909
- {
910
- value: "native-uniwind",
911
- label: "Uniwind",
912
- hint: "Fastest Tailwind bindings for React Native with HeroUI Native"
913
- },
914
- {
915
- value: "native-unistyles",
916
- label: "Unistyles",
917
- hint: "Consistent styling for React Native"
918
- }
919
- ],
920
- initialValue: "native-bare"
1140
+ while (true) {
1141
+ const wasFirstPrompt = isFirstPrompt();
1142
+ const frontendTypes = await navigableMultiselect({
1143
+ message: "Select project type",
1144
+ options: [{
1145
+ value: "web",
1146
+ label: "Web",
1147
+ hint: "React, Vue or Svelte Web Application"
1148
+ }, {
1149
+ value: "native",
1150
+ label: "Native",
1151
+ hint: "Create a React Native/Expo app"
1152
+ }],
1153
+ required: false,
1154
+ initialValues: ["web"]
921
1155
  });
922
- if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
923
- result.push(nativeFramework);
1156
+ if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
1157
+ if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
1158
+ setIsFirstPrompt(false);
1159
+ const result = [];
1160
+ let shouldRestart = false;
1161
+ if (frontendTypes.includes("web")) {
1162
+ const webFramework = await navigableSelect({
1163
+ message: "Choose web",
1164
+ options: [
1165
+ {
1166
+ value: "tanstack-router",
1167
+ label: "TanStack Router",
1168
+ hint: "Modern and scalable routing for React Applications"
1169
+ },
1170
+ {
1171
+ value: "react-router",
1172
+ label: "React Router",
1173
+ hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
1174
+ },
1175
+ {
1176
+ value: "next",
1177
+ label: "Next.js",
1178
+ hint: "The React Framework for the Web"
1179
+ },
1180
+ {
1181
+ value: "nuxt",
1182
+ label: "Nuxt",
1183
+ hint: "The Progressive Web Framework for Vue.js"
1184
+ },
1185
+ {
1186
+ value: "svelte",
1187
+ label: "Svelte",
1188
+ hint: "web development for the rest of us"
1189
+ },
1190
+ {
1191
+ value: "solid",
1192
+ label: "Solid",
1193
+ hint: "Simple and performant reactivity for building user interfaces"
1194
+ },
1195
+ {
1196
+ value: "tanstack-start",
1197
+ label: "TanStack Start",
1198
+ hint: "SSR, Server Functions, API Routes and more with TanStack Router"
1199
+ }
1200
+ ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
1201
+ initialValue: DEFAULT_CONFIG.frontend[0]
1202
+ });
1203
+ if (isGoBack(webFramework)) shouldRestart = true;
1204
+ else if (isCancel$1(webFramework)) return exitCancelled("Operation cancelled");
1205
+ else result.push(webFramework);
1206
+ }
1207
+ if (shouldRestart) {
1208
+ setIsFirstPrompt(wasFirstPrompt);
1209
+ continue;
1210
+ }
1211
+ if (frontendTypes.includes("native")) {
1212
+ const nativeFramework = await navigableSelect({
1213
+ message: "Choose native",
1214
+ options: [
1215
+ {
1216
+ value: "native-bare",
1217
+ label: "Bare",
1218
+ hint: "Bare Expo without styling library"
1219
+ },
1220
+ {
1221
+ value: "native-uniwind",
1222
+ label: "Uniwind",
1223
+ hint: "Fastest Tailwind bindings for React Native with HeroUI Native"
1224
+ },
1225
+ {
1226
+ value: "native-unistyles",
1227
+ label: "Unistyles",
1228
+ hint: "Consistent styling for React Native"
1229
+ }
1230
+ ],
1231
+ initialValue: "native-bare"
1232
+ });
1233
+ if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
1234
+ else {
1235
+ setIsFirstPrompt(wasFirstPrompt);
1236
+ continue;
1237
+ }
1238
+ else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
1239
+ else result.push(nativeFramework);
1240
+ }
1241
+ if (shouldRestart) {
1242
+ setIsFirstPrompt(wasFirstPrompt);
1243
+ continue;
1244
+ }
1245
+ return result;
924
1246
  }
925
- return result;
926
1247
  }
927
1248
 
928
1249
  //#endregion
929
1250
  //#region src/prompts/git.ts
930
1251
  async function getGitChoice(git) {
931
1252
  if (git !== void 0) return git;
932
- const response = await confirm({
1253
+ const response = await navigableConfirm({
933
1254
  message: "Initialize git repository?",
934
1255
  initialValue: DEFAULT_CONFIG.git
935
1256
  });
936
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1257
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
937
1258
  return response;
938
1259
  }
939
1260
 
@@ -941,14 +1262,71 @@ async function getGitChoice(git) {
941
1262
  //#region src/prompts/install.ts
942
1263
  async function getinstallChoice(install) {
943
1264
  if (install !== void 0) return install;
944
- const response = await confirm({
1265
+ const response = await navigableConfirm({
945
1266
  message: "Install dependencies?",
946
1267
  initialValue: DEFAULT_CONFIG.install
947
1268
  });
948
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1269
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
949
1270
  return response;
950
1271
  }
951
1272
 
1273
+ //#endregion
1274
+ //#region src/prompts/navigable-group.ts
1275
+ /**
1276
+ * Navigable group - a group of prompts that allows going back
1277
+ */
1278
+ /**
1279
+ * Define a group of prompts that supports going back to previous prompts.
1280
+ * Returns a result object with all the values, or handles cancel/go-back navigation.
1281
+ */
1282
+ async function navigableGroup(prompts, opts) {
1283
+ const results = {};
1284
+ const promptNames = Object.keys(prompts);
1285
+ let currentIndex = 0;
1286
+ let goingBack = false;
1287
+ while (currentIndex < promptNames.length) {
1288
+ const name = promptNames[currentIndex];
1289
+ const prompt = prompts[name];
1290
+ setIsFirstPrompt$1(currentIndex === 0);
1291
+ setLastPromptShownUI(false);
1292
+ const result = await prompt({ results })?.catch((e) => {
1293
+ throw e;
1294
+ });
1295
+ if (isGoBack(result)) {
1296
+ goingBack = true;
1297
+ if (currentIndex > 0) {
1298
+ const prevName = promptNames[currentIndex - 1];
1299
+ delete results[prevName];
1300
+ currentIndex--;
1301
+ continue;
1302
+ }
1303
+ goingBack = false;
1304
+ continue;
1305
+ }
1306
+ if (isCancel$1(result)) {
1307
+ if (typeof opts?.onCancel === "function") {
1308
+ results[name] = "canceled";
1309
+ opts.onCancel({ results });
1310
+ }
1311
+ setIsFirstPrompt$1(false);
1312
+ return results;
1313
+ }
1314
+ if (goingBack && !didLastPromptShowUI()) {
1315
+ if (currentIndex > 0) {
1316
+ const prevName = promptNames[currentIndex - 1];
1317
+ delete results[prevName];
1318
+ currentIndex--;
1319
+ continue;
1320
+ }
1321
+ }
1322
+ goingBack = false;
1323
+ results[name] = result;
1324
+ currentIndex++;
1325
+ }
1326
+ setIsFirstPrompt$1(false);
1327
+ return results;
1328
+ }
1329
+
952
1330
  //#endregion
953
1331
  //#region src/prompts/orm.ts
954
1332
  const ormOptions = {
@@ -972,12 +1350,12 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
972
1350
  if (backend === "convex") return "none";
973
1351
  if (!hasDatabase) return "none";
974
1352
  if (orm !== void 0) return orm;
975
- const response = await select({
1353
+ const response = await navigableSelect({
976
1354
  message: "Select ORM",
977
1355
  options: database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma],
978
1356
  initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
979
1357
  });
980
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1358
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
981
1359
  return response;
982
1360
  }
983
1361
 
@@ -985,13 +1363,13 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
985
1363
  //#region src/prompts/package-manager.ts
986
1364
  async function getPackageManagerChoice(packageManager) {
987
1365
  if (packageManager !== void 0) return packageManager;
988
- const response = await select({
1366
+ const response = await navigableSelect({
989
1367
  message: "Choose package manager",
990
1368
  options: [
991
1369
  {
992
1370
  value: "npm",
993
1371
  label: "npm",
994
- hint: "Node Package Manager"
1372
+ hint: "not recommended"
995
1373
  },
996
1374
  {
997
1375
  value: "pnpm",
@@ -1006,7 +1384,7 @@ async function getPackageManagerChoice(packageManager) {
1006
1384
  ],
1007
1385
  initialValue: getUserPkgManager()
1008
1386
  });
1009
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1387
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1010
1388
  return response;
1011
1389
  }
1012
1390
 
@@ -1016,7 +1394,7 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
1016
1394
  if (payments !== void 0) return payments;
1017
1395
  if (backend === "none") return "none";
1018
1396
  if (!(auth === "better-auth" && backend !== "convex" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0))) return "none";
1019
- const response = await select({
1397
+ const response = await navigableSelect({
1020
1398
  message: "Select payments provider",
1021
1399
  options: [{
1022
1400
  value: "polar",
@@ -1029,7 +1407,7 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
1029
1407
  }],
1030
1408
  initialValue: DEFAULT_CONFIG.payments
1031
1409
  });
1032
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1410
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1033
1411
  return response;
1034
1412
  }
1035
1413
 
@@ -1052,12 +1430,12 @@ async function getRuntimeChoice(runtime, backend) {
1052
1430
  label: "Cloudflare Workers",
1053
1431
  hint: "Edge runtime on Cloudflare's global network"
1054
1432
  });
1055
- const response = await select({
1433
+ const response = await navigableSelect({
1056
1434
  message: "Select runtime",
1057
1435
  options: runtimeOptions,
1058
1436
  initialValue: DEFAULT_CONFIG.runtime
1059
1437
  });
1060
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1438
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1061
1439
  return response;
1062
1440
  }
1063
1441
 
@@ -1095,12 +1473,12 @@ async function getServerDeploymentToAdd(runtime, existingDeployment, backend) {
1095
1473
  }
1096
1474
  if (existingDeployment && existingDeployment !== "none") return "none";
1097
1475
  if (options.length === 0) return "none";
1098
- const response = await select({
1476
+ const response = await navigableSelect({
1099
1477
  message: "Select server deployment",
1100
1478
  options,
1101
1479
  initialValue: DEFAULT_CONFIG.serverDeploy
1102
1480
  });
1103
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1481
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1104
1482
  return response;
1105
1483
  }
1106
1484
 
@@ -1122,7 +1500,7 @@ function getDeploymentDisplay(deployment) {
1122
1500
  async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
1123
1501
  if (deployment !== void 0) return deployment;
1124
1502
  if (!hasWebFrontend(frontend)) return "none";
1125
- const response = await select({
1503
+ const response = await navigableSelect({
1126
1504
  message: "Select web deployment",
1127
1505
  options: ["cloudflare", "none"].map((deploy) => {
1128
1506
  const { label, hint } = getDeploymentDisplay(deploy);
@@ -1134,7 +1512,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
1134
1512
  }),
1135
1513
  initialValue: DEFAULT_CONFIG.webDeploy
1136
1514
  });
1137
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1515
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1138
1516
  return response;
1139
1517
  }
1140
1518
  async function getDeploymentToAdd(frontend, existingDeployment) {
@@ -1155,19 +1533,19 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
1155
1533
  hint: "Skip deployment setup"
1156
1534
  });
1157
1535
  if (options.length === 0) return "none";
1158
- const response = await select({
1536
+ const response = await navigableSelect({
1159
1537
  message: "Select web deployment",
1160
1538
  options,
1161
1539
  initialValue: DEFAULT_CONFIG.webDeploy
1162
1540
  });
1163
- if (isCancel(response)) return exitCancelled("Operation cancelled");
1541
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1164
1542
  return response;
1165
1543
  }
1166
1544
 
1167
1545
  //#endregion
1168
1546
  //#region src/prompts/config-prompts.ts
1169
1547
  async function gatherConfig(flags, projectName, projectDir, relativePath) {
1170
- const result = await group({
1548
+ const result = await navigableGroup({
1171
1549
  frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
1172
1550
  backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
1173
1551
  runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
@@ -1383,14 +1761,14 @@ function generateReproducibleCommand(config) {
1383
1761
 
1384
1762
  //#endregion
1385
1763
  //#region src/utils/project-directory.ts
1386
- async function handleDirectoryConflict(currentPathInput, silent = false) {
1764
+ async function handleDirectoryConflict(currentPathInput) {
1387
1765
  while (true) {
1388
1766
  const resolvedPath = path.resolve(process.cwd(), currentPathInput);
1389
1767
  if (!(await fs.pathExists(resolvedPath) && (await fs.readdir(resolvedPath)).length > 0)) return {
1390
1768
  finalPathInput: currentPathInput,
1391
1769
  shouldClearDirectory: false
1392
1770
  };
1393
- if (silent) throw new Error(`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`);
1771
+ if (isSilent()) throw new Error(`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`);
1394
1772
  log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
1395
1773
  const action = await select({
1396
1774
  message: "What would you like to do?",
@@ -1497,11 +1875,7 @@ const catppuccinTheme = {
1497
1875
  const renderTitle = () => {
1498
1876
  const terminalWidth = process.stdout.columns || 80;
1499
1877
  const titleLines = TITLE_TEXT.split("\n");
1500
- if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`
1501
- ╔══════════════════╗
1502
- ║ Better T Stack ║
1503
- ╚══════════════════╝
1504
- `));
1878
+ if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
1505
1879
  else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
1506
1880
  };
1507
1881
 
@@ -6799,7 +7173,6 @@ async function updateConvexPackageJson(projectDir, options) {
6799
7173
  //#endregion
6800
7174
  //#region src/helpers/core/create-project.ts
6801
7175
  async function createProject(options, cliInput = {}) {
6802
- const { silent = false } = cliInput;
6803
7176
  const projectDir = options.projectDir;
6804
7177
  const isConvex = options.backend === "convex";
6805
7178
  const isSelfBackend = options.backend === "self";
@@ -6835,23 +7208,23 @@ async function createProject(options, cliInput = {}) {
6835
7208
  await setupCatalogs(projectDir, options);
6836
7209
  await createReadme(projectDir, options);
6837
7210
  await writeBtsConfig(options);
6838
- if (!silent) log.success("Project template successfully scaffolded!");
7211
+ if (!isSilent()) log.success("Project template successfully scaffolded!");
6839
7212
  if (options.install) await installDependencies({
6840
7213
  projectDir,
6841
7214
  packageManager: options.packageManager
6842
7215
  });
6843
7216
  await initializeGit(projectDir, options.git);
6844
- if (!silent) await displayPostInstallInstructions({
7217
+ if (!isSilent()) await displayPostInstallInstructions({
6845
7218
  ...options,
6846
7219
  depsInstalled: options.install
6847
7220
  });
6848
7221
  return projectDir;
6849
7222
  } catch (error) {
6850
7223
  if (error instanceof Error) {
6851
- if (!silent) console.error(error.stack);
7224
+ if (!isSilent()) console.error(error.stack);
6852
7225
  exitWithError(`Error during project creation: ${error.message}`);
6853
7226
  } else {
6854
- if (!silent) console.error(error);
7227
+ if (!isSilent()) console.error(error);
6855
7228
  exitWithError(`An unexpected error occurred: ${String(error)}`);
6856
7229
  }
6857
7230
  }
@@ -6861,139 +7234,169 @@ async function createProject(options, cliInput = {}) {
6861
7234
  //#region src/helpers/core/command-handlers.ts
6862
7235
  async function createProjectHandler(input, options = {}) {
6863
7236
  const { silent = false } = options;
6864
- const startTime = Date.now();
6865
- const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
6866
- if (!silent && input.renderTitle !== false) renderTitle();
6867
- if (!silent) intro(pc.magenta("Creating a new Better-T-Stack project"));
6868
- if (!silent && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
6869
- let currentPathInput;
6870
- if (input.yes && input.projectName) currentPathInput = input.projectName;
6871
- else if (input.yes) {
6872
- const defaultConfig = getDefaultConfig();
6873
- let defaultName = defaultConfig.relativePath;
6874
- let counter = 1;
6875
- while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
6876
- defaultName = `${defaultConfig.projectName}-${counter}`;
6877
- counter++;
6878
- }
6879
- currentPathInput = defaultName;
6880
- } else currentPathInput = await getProjectName(input.projectName);
6881
- let finalPathInput;
6882
- let shouldClearDirectory;
6883
- try {
6884
- if (input.directoryConflict) {
6885
- const result = await handleDirectoryConflictProgrammatically(currentPathInput, input.directoryConflict);
6886
- finalPathInput = result.finalPathInput;
6887
- shouldClearDirectory = result.shouldClearDirectory;
6888
- } else {
6889
- const result = await handleDirectoryConflict(currentPathInput);
6890
- finalPathInput = result.finalPathInput;
6891
- shouldClearDirectory = result.shouldClearDirectory;
6892
- }
6893
- } catch (error) {
6894
- return {
6895
- success: false,
6896
- projectConfig: {
6897
- projectName: "",
6898
- projectDir: "",
6899
- relativePath: "",
6900
- database: "none",
6901
- orm: "none",
6902
- backend: "none",
6903
- runtime: "none",
6904
- frontend: [],
6905
- addons: [],
6906
- examples: [],
6907
- auth: "none",
6908
- payments: "none",
6909
- git: false,
6910
- packageManager: "npm",
6911
- install: false,
6912
- dbSetup: "none",
6913
- api: "none",
6914
- webDeploy: "none",
6915
- serverDeploy: "none"
6916
- },
6917
- reproducibleCommand: "",
6918
- timeScaffolded,
6919
- elapsedTimeMs: Date.now() - startTime,
6920
- projectDirectory: "",
6921
- relativePath: "",
6922
- error: error instanceof Error ? error.message : String(error)
6923
- };
6924
- }
6925
- const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(finalPathInput, shouldClearDirectory);
6926
- const originalInput = {
6927
- ...input,
6928
- projectDirectory: input.projectName
6929
- };
6930
- const providedFlags = getProvidedFlags(originalInput);
6931
- let cliInput = originalInput;
6932
- if (input.template && input.template !== "none") {
6933
- const templateConfig = getTemplateConfig(input.template);
6934
- if (templateConfig) {
6935
- const templateName = input.template.toUpperCase();
6936
- const templateDescription = getTemplateDescription(input.template);
6937
- if (!silent) {
6938
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
6939
- log.message(pc.dim(` ${templateDescription}`));
7237
+ return runWithContextAsync({ silent }, async () => {
7238
+ const startTime = Date.now();
7239
+ const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
7240
+ try {
7241
+ if (!isSilent() && input.renderTitle !== false) renderTitle();
7242
+ if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
7243
+ if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
7244
+ let currentPathInput;
7245
+ if (input.yes && input.projectName) currentPathInput = input.projectName;
7246
+ else if (input.yes) {
7247
+ const defaultConfig = getDefaultConfig();
7248
+ let defaultName = defaultConfig.relativePath;
7249
+ let counter = 1;
7250
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
7251
+ defaultName = `${defaultConfig.projectName}-${counter}`;
7252
+ counter++;
7253
+ }
7254
+ currentPathInput = defaultName;
7255
+ } else currentPathInput = await getProjectName(input.projectName);
7256
+ let finalPathInput;
7257
+ let shouldClearDirectory;
7258
+ try {
7259
+ if (input.directoryConflict) {
7260
+ const result = await handleDirectoryConflictProgrammatically(currentPathInput, input.directoryConflict);
7261
+ finalPathInput = result.finalPathInput;
7262
+ shouldClearDirectory = result.shouldClearDirectory;
7263
+ } else {
7264
+ const result = await handleDirectoryConflict(currentPathInput);
7265
+ finalPathInput = result.finalPathInput;
7266
+ shouldClearDirectory = result.shouldClearDirectory;
7267
+ }
7268
+ } catch (error) {
7269
+ if (error instanceof UserCancelledError || error instanceof CLIError) throw error;
7270
+ return {
7271
+ success: false,
7272
+ projectConfig: {
7273
+ projectName: "",
7274
+ projectDir: "",
7275
+ relativePath: "",
7276
+ database: "none",
7277
+ orm: "none",
7278
+ backend: "none",
7279
+ runtime: "none",
7280
+ frontend: [],
7281
+ addons: [],
7282
+ examples: [],
7283
+ auth: "none",
7284
+ payments: "none",
7285
+ git: false,
7286
+ packageManager: "npm",
7287
+ install: false,
7288
+ dbSetup: "none",
7289
+ api: "none",
7290
+ webDeploy: "none",
7291
+ serverDeploy: "none"
7292
+ },
7293
+ reproducibleCommand: "",
7294
+ timeScaffolded,
7295
+ elapsedTimeMs: Date.now() - startTime,
7296
+ projectDirectory: "",
7297
+ relativePath: "",
7298
+ error: error instanceof Error ? error.message : String(error)
7299
+ };
6940
7300
  }
6941
- const userOverrides = {};
6942
- for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
6943
- cliInput = {
6944
- ...templateConfig,
6945
- ...userOverrides,
6946
- template: input.template,
6947
- projectDirectory: originalInput.projectDirectory
7301
+ const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(finalPathInput, shouldClearDirectory);
7302
+ const originalInput = {
7303
+ ...input,
7304
+ projectDirectory: input.projectName
6948
7305
  };
7306
+ const providedFlags = getProvidedFlags(originalInput);
7307
+ let cliInput = originalInput;
7308
+ if (input.template && input.template !== "none") {
7309
+ const templateConfig = getTemplateConfig(input.template);
7310
+ if (templateConfig) {
7311
+ const templateName = input.template.toUpperCase();
7312
+ const templateDescription = getTemplateDescription(input.template);
7313
+ if (!isSilent()) {
7314
+ log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
7315
+ log.message(pc.dim(` ${templateDescription}`));
7316
+ }
7317
+ const userOverrides = {};
7318
+ for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
7319
+ cliInput = {
7320
+ ...templateConfig,
7321
+ ...userOverrides,
7322
+ template: input.template,
7323
+ projectDirectory: originalInput.projectDirectory
7324
+ };
7325
+ }
7326
+ }
7327
+ let config;
7328
+ if (cliInput.yes) {
7329
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
7330
+ config = {
7331
+ ...getDefaultConfig(),
7332
+ ...flagConfig,
7333
+ projectName: finalBaseName,
7334
+ projectDir: finalResolvedPath,
7335
+ relativePath: finalPathInput
7336
+ };
7337
+ validateConfigCompatibility(config, providedFlags, cliInput);
7338
+ if (!isSilent()) {
7339
+ log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
7340
+ log.message(displayConfig(config));
7341
+ }
7342
+ } else {
7343
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
7344
+ const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
7345
+ if (!isSilent() && Object.keys(otherFlags).length > 0) {
7346
+ log.info(pc.yellow("Using these pre-selected options:"));
7347
+ log.message(displayConfig(otherFlags));
7348
+ log.message("");
7349
+ }
7350
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
7351
+ }
7352
+ await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
7353
+ const reproducibleCommand = generateReproducibleCommand(config);
7354
+ if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
7355
+ await trackProjectCreation(config, input.disableAnalytics);
7356
+ const elapsedTimeMs = Date.now() - startTime;
7357
+ if (!isSilent()) {
7358
+ const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
7359
+ outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
7360
+ }
7361
+ return {
7362
+ success: true,
7363
+ projectConfig: config,
7364
+ reproducibleCommand,
7365
+ timeScaffolded,
7366
+ elapsedTimeMs,
7367
+ projectDirectory: config.projectDir,
7368
+ relativePath: config.relativePath
7369
+ };
7370
+ } catch (error) {
7371
+ if (error instanceof UserCancelledError) {
7372
+ if (isSilent()) return {
7373
+ success: false,
7374
+ error: error.message,
7375
+ projectConfig: {},
7376
+ reproducibleCommand: "",
7377
+ timeScaffolded,
7378
+ elapsedTimeMs: Date.now() - startTime,
7379
+ projectDirectory: "",
7380
+ relativePath: ""
7381
+ };
7382
+ return;
7383
+ }
7384
+ if (error instanceof CLIError) {
7385
+ if (isSilent()) return {
7386
+ success: false,
7387
+ error: error.message,
7388
+ projectConfig: {},
7389
+ reproducibleCommand: "",
7390
+ timeScaffolded,
7391
+ elapsedTimeMs: Date.now() - startTime,
7392
+ projectDirectory: "",
7393
+ relativePath: ""
7394
+ };
7395
+ throw error;
7396
+ }
7397
+ throw error;
6949
7398
  }
6950
- }
6951
- let config;
6952
- if (cliInput.yes) {
6953
- const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
6954
- config = {
6955
- ...getDefaultConfig(),
6956
- ...flagConfig,
6957
- projectName: finalBaseName,
6958
- projectDir: finalResolvedPath,
6959
- relativePath: finalPathInput
6960
- };
6961
- validateConfigCompatibility(config, providedFlags, cliInput);
6962
- if (!silent) {
6963
- log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
6964
- log.message(displayConfig(config));
6965
- }
6966
- } else {
6967
- const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
6968
- const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
6969
- if (!silent && Object.keys(otherFlags).length > 0) {
6970
- log.info(pc.yellow("Using these pre-selected options:"));
6971
- log.message(displayConfig(otherFlags));
6972
- log.message("");
6973
- }
6974
- config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6975
- }
6976
- await createProject(config, {
6977
- manualDb: cliInput.manualDb ?? input.manualDb,
6978
- silent
6979
7399
  });
6980
- const reproducibleCommand = generateReproducibleCommand(config);
6981
- if (!silent) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
6982
- await trackProjectCreation(config, input.disableAnalytics);
6983
- const elapsedTimeMs = Date.now() - startTime;
6984
- if (!silent) {
6985
- const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
6986
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
6987
- }
6988
- return {
6989
- success: true,
6990
- projectConfig: config,
6991
- reproducibleCommand,
6992
- timeScaffolded,
6993
- elapsedTimeMs,
6994
- projectDirectory: config.projectDir,
6995
- relativePath: config.relativePath
6996
- };
6997
7400
  }
6998
7401
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
6999
7402
  const currentPath = path.resolve(process.cwd(), currentPathInput);
@@ -7088,6 +7491,8 @@ async function addAddonsHandler(input) {
7088
7491
  else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
7089
7492
  outro("Add command completed successfully!");
7090
7493
  } catch (error) {
7494
+ if (error instanceof UserCancelledError) return;
7495
+ if (error instanceof CLIError) throw error;
7091
7496
  handleError(error, "Failed to add addons or deployment");
7092
7497
  }
7093
7498
  }