create-better-t-stack 3.19.3 → 3.19.5-pr874.92079f0

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,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { t as __reExport } from "./chunk-DPg_XC7m.mjs";
3
- import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
2
+ import { t as __reExport } from "./chunk-CHc3S52W.mjs";
4
3
  import { createRouterClient, os } from "@orpc/server";
5
4
  import { Result, Result as Result$1, TaggedError } from "better-result";
6
- import pc from "picocolors";
7
5
  import { createCli } from "trpc-cli";
8
6
  import z from "zod";
9
- import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
10
- import { writeTree } from "@better-t-stack/template-generator/fs-writer";
7
+ import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import envPaths from "env-paths";
11
10
  import fs from "fs-extra";
12
11
  import path from "node:path";
13
12
  import { fileURLToPath } from "node:url";
13
+ import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
14
14
  import consola, { consola as consola$1 } from "consola";
15
+ import gradient from "gradient-string";
16
+ import { $, execa } from "execa";
17
+ import { writeTree } from "@better-t-stack/template-generator/fs-writer";
15
18
  import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
16
19
  import { AsyncLocalStorage } from "node:async_hooks";
17
20
  import { applyEdits, modify, parse } from "jsonc-parser";
18
- import gradient from "gradient-string";
19
- import { $, execa } from "execa";
20
- import envPaths from "env-paths";
21
- import { format } from "oxfmt";
22
21
  import os$1 from "node:os";
22
+ import { format } from "oxfmt";
23
23
 
24
24
  //#region src/utils/get-package-manager.ts
25
25
  const getUserPkgManager = () => {
@@ -86,6 +86,7 @@ const ADDON_COMPATIBILITY = {
86
86
  starlight: [],
87
87
  ultracite: [],
88
88
  ruler: [],
89
+ mcp: [],
89
90
  oxlint: [],
90
91
  fumadocs: [],
91
92
  opentui: [],
@@ -94,25 +95,6 @@ const ADDON_COMPATIBILITY = {
94
95
  none: []
95
96
  };
96
97
 
97
- //#endregion
98
- //#region src/types.ts
99
- var types_exports = {};
100
- import * as import__better_t_stack_types from "@better-t-stack/types";
101
- __reExport(types_exports, import__better_t_stack_types);
102
-
103
- //#endregion
104
- //#region src/utils/compatibility.ts
105
- const WEB_FRAMEWORKS = [
106
- "tanstack-router",
107
- "react-router",
108
- "tanstack-start",
109
- "next",
110
- "nuxt",
111
- "svelte",
112
- "solid",
113
- "astro"
114
- ];
115
-
116
98
  //#endregion
117
99
  //#region src/utils/errors.ts
118
100
  /**
@@ -202,6 +184,344 @@ function displayError(error) {
202
184
  else consola.error(pc.red(error.message));
203
185
  }
204
186
 
187
+ //#endregion
188
+ //#region src/utils/get-latest-cli-version.ts
189
+ function getLatestCLIVersionResult() {
190
+ const packageJsonPath = path.join(PKG_ROOT, "package.json");
191
+ return Result.try({
192
+ try: () => {
193
+ return fs.readJSONSync(packageJsonPath).version;
194
+ },
195
+ catch: (e) => new CLIError({
196
+ message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
197
+ cause: e
198
+ })
199
+ });
200
+ }
201
+ function getLatestCLIVersion() {
202
+ return getLatestCLIVersionResult().unwrapOr("1.0.0");
203
+ }
204
+
205
+ //#endregion
206
+ //#region src/utils/project-history.ts
207
+ const paths = envPaths("better-t-stack", { suffix: "" });
208
+ const HISTORY_FILE = "history.json";
209
+ var HistoryError = class extends TaggedError("HistoryError")() {};
210
+ function getHistoryDir() {
211
+ return paths.data;
212
+ }
213
+ function getHistoryPath() {
214
+ return path.join(paths.data, HISTORY_FILE);
215
+ }
216
+ function generateId() {
217
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
218
+ }
219
+ function emptyHistory() {
220
+ return {
221
+ version: 1,
222
+ entries: []
223
+ };
224
+ }
225
+ async function ensureHistoryDir() {
226
+ return Result.tryPromise({
227
+ try: async () => {
228
+ await fs.ensureDir(getHistoryDir());
229
+ },
230
+ catch: (e) => new HistoryError({
231
+ message: `Failed to create history directory: ${e instanceof Error ? e.message : String(e)}`,
232
+ cause: e
233
+ })
234
+ });
235
+ }
236
+ async function readHistory() {
237
+ const historyPath = getHistoryPath();
238
+ const existsResult = await Result.tryPromise({
239
+ try: async () => await fs.pathExists(historyPath),
240
+ catch: (e) => new HistoryError({
241
+ message: `Failed to check history file: ${e instanceof Error ? e.message : String(e)}`,
242
+ cause: e
243
+ })
244
+ });
245
+ if (existsResult.isErr()) return existsResult;
246
+ if (!existsResult.value) return Result.ok(emptyHistory());
247
+ const readResult = await Result.tryPromise({
248
+ try: async () => await fs.readJson(historyPath),
249
+ catch: (e) => new HistoryError({
250
+ message: `Failed to read history file: ${e instanceof Error ? e.message : String(e)}`,
251
+ cause: e
252
+ })
253
+ });
254
+ if (readResult.isErr()) return Result.ok(emptyHistory());
255
+ return Result.ok(readResult.value);
256
+ }
257
+ async function writeHistory(history) {
258
+ const ensureDirResult = await ensureHistoryDir();
259
+ if (ensureDirResult.isErr()) return ensureDirResult;
260
+ return Result.tryPromise({
261
+ try: async () => {
262
+ await fs.writeJson(getHistoryPath(), history, { spaces: 2 });
263
+ },
264
+ catch: (e) => new HistoryError({
265
+ message: `Failed to write history file: ${e instanceof Error ? e.message : String(e)}`,
266
+ cause: e
267
+ })
268
+ });
269
+ }
270
+ async function addToHistory(config, reproducibleCommand) {
271
+ const historyResult = await readHistory();
272
+ if (historyResult.isErr()) return historyResult;
273
+ const history = historyResult.value;
274
+ const entry = {
275
+ id: generateId(),
276
+ projectName: config.projectName,
277
+ projectDir: config.projectDir,
278
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
279
+ stack: {
280
+ frontend: config.frontend,
281
+ backend: config.backend,
282
+ database: config.database,
283
+ orm: config.orm,
284
+ runtime: config.runtime,
285
+ auth: config.auth,
286
+ payments: config.payments,
287
+ api: config.api,
288
+ addons: config.addons,
289
+ examples: config.examples,
290
+ dbSetup: config.dbSetup,
291
+ packageManager: config.packageManager
292
+ },
293
+ cliVersion: getLatestCLIVersion(),
294
+ reproducibleCommand
295
+ };
296
+ history.entries.unshift(entry);
297
+ if (history.entries.length > 100) history.entries = history.entries.slice(0, 100);
298
+ return await writeHistory(history);
299
+ }
300
+ async function getHistory(limit = 10) {
301
+ const historyResult = await readHistory();
302
+ if (historyResult.isErr()) return historyResult;
303
+ return Result.ok(historyResult.value.entries.slice(0, limit));
304
+ }
305
+ async function clearHistory() {
306
+ const historyPath = getHistoryPath();
307
+ return Result.tryPromise({
308
+ try: async () => {
309
+ if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
310
+ },
311
+ catch: (e) => new HistoryError({
312
+ message: `Failed to clear history: ${e instanceof Error ? e.message : String(e)}`,
313
+ cause: e
314
+ })
315
+ });
316
+ }
317
+
318
+ //#endregion
319
+ //#region src/utils/render-title.ts
320
+ const TITLE_TEXT = `
321
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
322
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
323
+ ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
324
+ ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
325
+ ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
326
+ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
327
+
328
+ ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
329
+ ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
330
+ ██║ ███████╗ ██║ ███████║██║ █████╔╝
331
+ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
332
+ ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
333
+ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
334
+ `;
335
+ const catppuccinTheme = {
336
+ pink: "#F5C2E7",
337
+ mauve: "#CBA6F7",
338
+ red: "#F38BA8",
339
+ maroon: "#E78284",
340
+ peach: "#FAB387",
341
+ yellow: "#F9E2AF",
342
+ green: "#A6E3A1",
343
+ teal: "#94E2D5",
344
+ sky: "#89DCEB",
345
+ sapphire: "#74C7EC",
346
+ lavender: "#B4BEFE"
347
+ };
348
+ const renderTitle = () => {
349
+ const terminalWidth = process.stdout.columns || 80;
350
+ const titleLines = TITLE_TEXT.split("\n");
351
+ if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
352
+ else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
353
+ };
354
+
355
+ //#endregion
356
+ //#region src/commands/history.ts
357
+ function formatStackSummary(entry) {
358
+ const parts = [];
359
+ if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(entry.stack.frontend.join(", "));
360
+ if (entry.stack.backend && entry.stack.backend !== "none") parts.push(entry.stack.backend);
361
+ if (entry.stack.database && entry.stack.database !== "none") parts.push(entry.stack.database);
362
+ if (entry.stack.orm && entry.stack.orm !== "none") parts.push(entry.stack.orm);
363
+ return parts.length > 0 ? parts.join(" + ") : "minimal";
364
+ }
365
+ function formatDate(isoString) {
366
+ return new Date(isoString).toLocaleDateString("en-US", {
367
+ year: "numeric",
368
+ month: "short",
369
+ day: "numeric",
370
+ hour: "2-digit",
371
+ minute: "2-digit"
372
+ });
373
+ }
374
+ async function historyHandler(input) {
375
+ if (input.clear) {
376
+ const clearResult = await clearHistory();
377
+ if (clearResult.isErr()) {
378
+ log.warn(pc.yellow(clearResult.error.message));
379
+ return;
380
+ }
381
+ log.success(pc.green("Project history cleared."));
382
+ return;
383
+ }
384
+ const historyResult = await getHistory(input.limit);
385
+ if (historyResult.isErr()) {
386
+ log.warn(pc.yellow(historyResult.error.message));
387
+ return;
388
+ }
389
+ const entries = historyResult.value;
390
+ if (entries.length === 0) {
391
+ log.info(pc.dim("No projects in history yet."));
392
+ log.info(pc.dim("Create a project with: create-better-t-stack my-app"));
393
+ return;
394
+ }
395
+ if (input.json) {
396
+ console.log(JSON.stringify(entries, null, 2));
397
+ return;
398
+ }
399
+ renderTitle();
400
+ intro(pc.magenta(`Project History (${entries.length} entries)`));
401
+ for (const [index, entry] of entries.entries()) {
402
+ const num = pc.dim(`${index + 1}.`);
403
+ const name = pc.cyan(pc.bold(entry.projectName));
404
+ const stack = pc.dim(formatStackSummary(entry));
405
+ log.message(`${num} ${name}`);
406
+ log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
407
+ log.message(` ${pc.dim("Path:")} ${entry.projectDir}`);
408
+ log.message(` ${pc.dim("Stack:")} ${stack}`);
409
+ log.message(` ${pc.dim("Command:")} ${pc.dim(entry.reproducibleCommand)}`);
410
+ log.message("");
411
+ }
412
+ }
413
+
414
+ //#endregion
415
+ //#region src/utils/open-url.ts
416
+ async function openUrl(url) {
417
+ const platform = process.platform;
418
+ if (platform === "darwin") {
419
+ await $({ stdio: "ignore" })`open ${url}`;
420
+ return;
421
+ }
422
+ if (platform === "win32") {
423
+ const escapedUrl = url.replace(/&/g, "^&");
424
+ await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
425
+ return;
426
+ }
427
+ await $({ stdio: "ignore" })`xdg-open ${url}`;
428
+ }
429
+
430
+ //#endregion
431
+ //#region src/utils/sponsors.ts
432
+ const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
433
+ async function fetchSponsors(url = SPONSORS_JSON_URL) {
434
+ const s = spinner();
435
+ s.start("Fetching sponsors…");
436
+ const response = await fetch(url);
437
+ if (!response.ok) {
438
+ s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
439
+ throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
440
+ }
441
+ const sponsors = await response.json();
442
+ s.stop("Sponsors fetched successfully!");
443
+ return sponsors;
444
+ }
445
+ function displaySponsors(sponsors) {
446
+ const { total_sponsors } = sponsors.summary;
447
+ if (total_sponsors === 0) {
448
+ log.info("No sponsors found. You can be the first one! ✨");
449
+ outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
450
+ return;
451
+ }
452
+ displaySponsorsBox(sponsors);
453
+ if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
454
+ outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
455
+ }
456
+ function displaySponsorsBox(sponsors) {
457
+ if (sponsors.specialSponsors.length === 0) return;
458
+ let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
459
+ sponsors.specialSponsors.forEach((sponsor, idx) => {
460
+ const displayName = sponsor.name ?? sponsor.githubId;
461
+ const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
462
+ output += `${pc.green(`• ${displayName}`)}${tier}\n`;
463
+ output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
464
+ const website = sponsor.websiteUrl ?? sponsor.githubUrl;
465
+ if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
466
+ if (idx < sponsors.specialSponsors.length - 1) output += "\n";
467
+ });
468
+ consola$1.box(output);
469
+ }
470
+
471
+ //#endregion
472
+ //#region src/commands/meta.ts
473
+ const DOCS_URL = "https://better-t-stack.dev/docs";
474
+ const BUILDER_URL = "https://better-t-stack.dev/new";
475
+ async function openExternalUrl(url, successMessage) {
476
+ if ((await Result.tryPromise({
477
+ try: () => openUrl(url),
478
+ catch: () => null
479
+ })).isOk()) log.success(pc.blue(successMessage));
480
+ else log.message(`Please visit ${url}`);
481
+ }
482
+ async function showSponsorsCommand() {
483
+ const result = await Result.tryPromise({
484
+ try: async () => {
485
+ renderTitle();
486
+ intro(pc.magenta("Better-T-Stack Sponsors"));
487
+ displaySponsors(await fetchSponsors());
488
+ },
489
+ catch: (error) => new CLIError({
490
+ message: error instanceof Error ? error.message : "Failed to display sponsors",
491
+ cause: error
492
+ })
493
+ });
494
+ if (result.isErr()) {
495
+ displayError(result.error);
496
+ process.exit(1);
497
+ }
498
+ }
499
+ async function openDocsCommand() {
500
+ await openExternalUrl(DOCS_URL, "Opened docs in your default browser.");
501
+ }
502
+ async function openBuilderCommand() {
503
+ await openExternalUrl(BUILDER_URL, "Opened builder in your default browser.");
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/types.ts
508
+ var types_exports = {};
509
+ import * as import__better_t_stack_types from "@better-t-stack/types";
510
+ __reExport(types_exports, import__better_t_stack_types);
511
+
512
+ //#endregion
513
+ //#region src/utils/compatibility.ts
514
+ const WEB_FRAMEWORKS = [
515
+ "tanstack-router",
516
+ "react-router",
517
+ "tanstack-start",
518
+ "next",
519
+ "nuxt",
520
+ "svelte",
521
+ "solid",
522
+ "astro"
523
+ ];
524
+
205
525
  //#endregion
206
526
  //#region src/utils/compatibility-rules.ts
207
527
  function validationErr$1(message) {
@@ -705,6 +1025,10 @@ function getAddonDisplay(addon) {
705
1025
  label = "Skills";
706
1026
  hint = "AI coding agent skills for your stack";
707
1027
  break;
1028
+ case "mcp":
1029
+ label = "MCP";
1030
+ hint = "Install MCP servers (docs, databases, SaaS) via add-mcp";
1031
+ break;
708
1032
  default:
709
1033
  label = addon;
710
1034
  hint = `Add ${addon}`;
@@ -730,7 +1054,11 @@ const ADDON_GROUPS = {
730
1054
  "opentui",
731
1055
  "wxt"
732
1056
  ],
733
- AI: ["ruler", "skills"]
1057
+ AI: [
1058
+ "ruler",
1059
+ "skills",
1060
+ "mcp"
1061
+ ]
734
1062
  };
735
1063
  async function getAddonsChoice(addons, frontends, auth) {
736
1064
  if (addons !== void 0) return addons;
@@ -756,11 +1084,11 @@ async function getAddonsChoice(addons, frontends, auth) {
756
1084
  else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
757
1085
  else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
758
1086
  }
759
- Object.keys(groupedOptions).forEach((group$1) => {
760
- if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
1087
+ Object.keys(groupedOptions).forEach((group) => {
1088
+ if (groupedOptions[group].length === 0) delete groupedOptions[group];
761
1089
  else {
762
- const groupOrder = ADDON_GROUPS[group$1] || [];
763
- groupedOptions[group$1].sort((a, b) => {
1090
+ const groupOrder = ADDON_GROUPS[group] || [];
1091
+ groupedOptions[group].sort((a, b) => {
764
1092
  return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
765
1093
  });
766
1094
  }
@@ -795,11 +1123,11 @@ async function getAddonsToAdd(frontend, existingAddons = [], auth) {
795
1123
  else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
796
1124
  else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
797
1125
  }
798
- Object.keys(groupedOptions).forEach((group$1) => {
799
- if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
1126
+ Object.keys(groupedOptions).forEach((group) => {
1127
+ if (groupedOptions[group].length === 0) delete groupedOptions[group];
800
1128
  else {
801
- const groupOrder = ADDON_GROUPS[group$1] || [];
802
- groupedOptions[group$1].sort((a, b) => {
1129
+ const groupOrder = ADDON_GROUPS[group] || [];
1130
+ groupedOptions[group].sort((a, b) => {
803
1131
  return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
804
1132
  });
805
1133
  }
@@ -845,43 +1173,6 @@ async function updateBtsConfig(projectDir, updates) {
845
1173
  } catch {}
846
1174
  }
847
1175
 
848
- //#endregion
849
- //#region src/utils/render-title.ts
850
- const TITLE_TEXT = `
851
- ██████╗ ███████╗████████╗████████╗███████╗██████╗
852
- ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
853
- ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
854
- ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
855
- ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
856
- ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
857
-
858
- ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
859
- ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
860
- ██║ ███████╗ ██║ ███████║██║ █████╔╝
861
- ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
862
- ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
863
- ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
864
- `;
865
- const catppuccinTheme = {
866
- pink: "#F5C2E7",
867
- mauve: "#CBA6F7",
868
- red: "#F38BA8",
869
- maroon: "#E78284",
870
- peach: "#FAB387",
871
- yellow: "#F9E2AF",
872
- green: "#A6E3A1",
873
- teal: "#94E2D5",
874
- sky: "#89DCEB",
875
- sapphire: "#74C7EC",
876
- lavender: "#B4BEFE"
877
- };
878
- const renderTitle = () => {
879
- const terminalWidth = process.stdout.columns || 80;
880
- const titleLines = TITLE_TEXT.split("\n");
881
- if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
882
- else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
883
- };
884
-
885
1176
  //#endregion
886
1177
  //#region src/utils/add-package-deps.ts
887
1178
  const addPackageDependency = async (opts) => {
@@ -905,8 +1196,52 @@ const addPackageDependency = async (opts) => {
905
1196
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
906
1197
  };
907
1198
 
1199
+ //#endregion
1200
+ //#region src/utils/external-commands.ts
1201
+ function shouldSkipExternalCommands() {
1202
+ return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
1203
+ }
1204
+
908
1205
  //#endregion
909
1206
  //#region src/utils/package-runner.ts
1207
+ function splitCommandArgs(commandWithArgs) {
1208
+ const args = [];
1209
+ let current = "";
1210
+ let quote = null;
1211
+ for (let i = 0; i < commandWithArgs.length; i += 1) {
1212
+ const char = commandWithArgs[i];
1213
+ if (quote) {
1214
+ if (char === quote) {
1215
+ quote = null;
1216
+ continue;
1217
+ }
1218
+ if (char === "\\" && i + 1 < commandWithArgs.length) {
1219
+ const nextChar = commandWithArgs[i + 1];
1220
+ if (nextChar === quote || nextChar === "\\") {
1221
+ current += nextChar;
1222
+ i += 1;
1223
+ continue;
1224
+ }
1225
+ }
1226
+ current += char;
1227
+ continue;
1228
+ }
1229
+ if (char === "\"" || char === "'") {
1230
+ quote = char;
1231
+ continue;
1232
+ }
1233
+ if (/\s/.test(char)) {
1234
+ if (current.length > 0) {
1235
+ args.push(current);
1236
+ current = "";
1237
+ }
1238
+ continue;
1239
+ }
1240
+ current += char;
1241
+ }
1242
+ if (current.length > 0) args.push(current);
1243
+ return args;
1244
+ }
910
1245
  /**
911
1246
  * Returns the appropriate command for running a package without installing it globally,
912
1247
  * based on the selected package manager.
@@ -931,7 +1266,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
931
1266
  * @returns An array of [command, ...args] (e.g., ["npx", "prisma", "generate"]).
932
1267
  */
933
1268
  function getPackageExecutionArgs(packageManager, commandWithArgs) {
934
- const args = commandWithArgs.split(" ");
1269
+ const args = splitCommandArgs(commandWithArgs);
935
1270
  switch (packageManager) {
936
1271
  case "pnpm": return [
937
1272
  "pnpm",
@@ -1002,95 +1337,352 @@ const TEMPLATES$2 = {
1002
1337
  }
1003
1338
  };
1004
1339
  async function setupFumadocs(config) {
1340
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1005
1341
  const { packageManager, projectDir } = config;
1006
1342
  log.info("Setting up Fumadocs...");
1007
1343
  const template = await select({
1008
1344
  message: "Choose a template",
1009
- options: Object.entries(TEMPLATES$2).map(([key, template$1]) => ({
1345
+ options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
1010
1346
  value: key,
1011
- label: template$1.label,
1012
- hint: template$1.hint
1347
+ label: template.label,
1348
+ hint: template.hint
1013
1349
  })),
1014
1350
  initialValue: "next-mdx"
1015
1351
  });
1016
- if (isCancel(template)) return userCancelled("Operation cancelled");
1017
- const templateArg = TEMPLATES$2[template].value;
1018
- const isNextTemplate = template.startsWith("next-");
1019
- const options = [
1020
- `--template ${templateArg}`,
1021
- `--pm ${packageManager}`,
1022
- "--no-git"
1023
- ];
1024
- if (isNextTemplate) options.push("--src");
1025
- if (config.addons.includes("biome")) options.push("--linter biome");
1026
- const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
1027
- const appsDir = path.join(projectDir, "apps");
1028
- await fs.ensureDir(appsDir);
1029
- const s = spinner();
1030
- s.start("Running Fumadocs create command...");
1031
- const result = await Result.tryPromise({
1032
- try: async () => {
1033
- await $({
1034
- cwd: appsDir,
1035
- env: { CI: "true" }
1036
- })`${args}`;
1037
- const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
1038
- const packageJsonPath = path.join(fumadocsDir, "package.json");
1039
- if (await fs.pathExists(packageJsonPath)) {
1040
- const packageJson = await fs.readJson(packageJsonPath);
1041
- packageJson.name = "fumadocs";
1042
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
1043
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1044
- }
1045
- },
1046
- catch: (e) => new AddonSetupError({
1047
- addon: "fumadocs",
1048
- message: `Failed to set up Fumadocs: ${e instanceof Error ? e.message : String(e)}`,
1049
- cause: e
1050
- })
1352
+ if (isCancel(template)) return userCancelled("Operation cancelled");
1353
+ const templateArg = TEMPLATES$2[template].value;
1354
+ const isNextTemplate = template.startsWith("next-");
1355
+ const options = [
1356
+ `--template ${templateArg}`,
1357
+ `--pm ${packageManager}`,
1358
+ "--no-git"
1359
+ ];
1360
+ if (isNextTemplate) options.push("--src");
1361
+ if (config.addons.includes("biome")) options.push("--linter biome");
1362
+ const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
1363
+ const appsDir = path.join(projectDir, "apps");
1364
+ await fs.ensureDir(appsDir);
1365
+ const s = spinner();
1366
+ s.start("Running Fumadocs create command...");
1367
+ const result = await Result.tryPromise({
1368
+ try: async () => {
1369
+ await $({
1370
+ cwd: appsDir,
1371
+ env: { CI: "true" }
1372
+ })`${args}`;
1373
+ const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
1374
+ const packageJsonPath = path.join(fumadocsDir, "package.json");
1375
+ if (await fs.pathExists(packageJsonPath)) {
1376
+ const packageJson = await fs.readJson(packageJsonPath);
1377
+ packageJson.name = "fumadocs";
1378
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
1379
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1380
+ }
1381
+ },
1382
+ catch: (e) => new AddonSetupError({
1383
+ addon: "fumadocs",
1384
+ message: `Failed to set up Fumadocs: ${e instanceof Error ? e.message : String(e)}`,
1385
+ cause: e
1386
+ })
1387
+ });
1388
+ if (result.isErr()) {
1389
+ s.stop("Failed to set up Fumadocs");
1390
+ return result;
1391
+ }
1392
+ s.stop("Fumadocs setup complete!");
1393
+ return Result.ok(void 0);
1394
+ }
1395
+
1396
+ //#endregion
1397
+ //#region src/helpers/addons/mcp-setup.ts
1398
+ const MCP_AGENTS = [
1399
+ {
1400
+ value: "cursor",
1401
+ label: "Cursor",
1402
+ scope: "both"
1403
+ },
1404
+ {
1405
+ value: "claude-code",
1406
+ label: "Claude Code",
1407
+ scope: "both"
1408
+ },
1409
+ {
1410
+ value: "codex",
1411
+ label: "Codex",
1412
+ scope: "both"
1413
+ },
1414
+ {
1415
+ value: "opencode",
1416
+ label: "OpenCode",
1417
+ scope: "both"
1418
+ },
1419
+ {
1420
+ value: "gemini-cli",
1421
+ label: "Gemini CLI",
1422
+ scope: "both"
1423
+ },
1424
+ {
1425
+ value: "vscode",
1426
+ label: "VS Code (GitHub Copilot)",
1427
+ scope: "both"
1428
+ },
1429
+ {
1430
+ value: "zed",
1431
+ label: "Zed",
1432
+ scope: "both"
1433
+ },
1434
+ {
1435
+ value: "claude-desktop",
1436
+ label: "Claude Desktop",
1437
+ scope: "global"
1438
+ },
1439
+ {
1440
+ value: "goose",
1441
+ label: "Goose",
1442
+ scope: "global"
1443
+ }
1444
+ ];
1445
+ function uniqueValues$1(values) {
1446
+ return Array.from(new Set(values));
1447
+ }
1448
+ function hasReactBasedFrontend$1(frontend) {
1449
+ return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
1450
+ }
1451
+ function getRecommendedMcpServers(config) {
1452
+ const servers = [];
1453
+ servers.push({
1454
+ key: "context7",
1455
+ label: "Context7",
1456
+ name: "context7",
1457
+ target: "@upstash/context7-mcp"
1458
+ });
1459
+ if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") servers.push({
1460
+ key: "cloudflare-docs",
1461
+ label: "Cloudflare Docs",
1462
+ name: "cloudflare-docs",
1463
+ target: "https://docs.mcp.cloudflare.com/sse",
1464
+ transport: "sse"
1465
+ });
1466
+ if (config.backend === "convex") servers.push({
1467
+ key: "convex",
1468
+ label: "Convex",
1469
+ name: "convex",
1470
+ target: "npx -y convex@latest mcp start"
1471
+ });
1472
+ if (hasReactBasedFrontend$1(config.frontend)) servers.push({
1473
+ key: "shadcn",
1474
+ label: "shadcn/ui",
1475
+ name: "shadcn",
1476
+ target: "npx -y shadcn@latest mcp"
1477
+ });
1478
+ if (config.frontend.includes("next")) servers.push({
1479
+ key: "next-devtools",
1480
+ label: "Next Devtools",
1481
+ name: "next-devtools",
1482
+ target: "npx -y next-devtools-mcp@latest"
1483
+ });
1484
+ if (config.frontend.includes("nuxt")) servers.push({
1485
+ key: "nuxt-docs",
1486
+ label: "Nuxt Docs",
1487
+ name: "nuxt",
1488
+ target: "https://nuxt.com/mcp"
1489
+ }, {
1490
+ key: "nuxt-ui-docs",
1491
+ label: "Nuxt UI Docs",
1492
+ name: "nuxt-ui",
1493
+ target: "https://ui.nuxt.com/mcp"
1494
+ });
1495
+ if (config.frontend.includes("svelte")) servers.push({
1496
+ key: "svelte-docs",
1497
+ label: "Svelte Docs",
1498
+ name: "svelte",
1499
+ target: "https://mcp.svelte.dev/mcp"
1500
+ });
1501
+ if (config.frontend.includes("astro")) servers.push({
1502
+ key: "astro-docs",
1503
+ label: "Astro Docs",
1504
+ name: "astro-docs",
1505
+ target: "https://mcp.docs.astro.build/mcp"
1506
+ });
1507
+ if (config.dbSetup === "planetscale") servers.push({
1508
+ key: "planetscale",
1509
+ label: "PlanetScale",
1510
+ name: "planetscale",
1511
+ target: "https://mcp.pscale.dev/mcp/planetscale"
1512
+ });
1513
+ if (config.dbSetup === "neon") servers.push({
1514
+ key: "neon",
1515
+ label: "Neon",
1516
+ name: "neon",
1517
+ target: "https://mcp.neon.tech/mcp"
1518
+ });
1519
+ if (config.dbSetup === "supabase") servers.push({
1520
+ key: "supabase",
1521
+ label: "Supabase",
1522
+ name: "supabase",
1523
+ target: "https://mcp.supabase.com/mcp"
1524
+ });
1525
+ if (config.auth === "better-auth") servers.push({
1526
+ key: "better-auth",
1527
+ label: "Better Auth",
1528
+ name: "better-auth",
1529
+ target: "https://mcp.inkeep.com/better-auth/mcp"
1530
+ });
1531
+ if (config.payments === "polar") servers.push({
1532
+ key: "polar",
1533
+ label: "Polar",
1534
+ name: "polar",
1535
+ target: "https://mcp.polar.sh/mcp/polar-mcp"
1536
+ });
1537
+ return servers;
1538
+ }
1539
+ function filterAgentsForScope(scope) {
1540
+ return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
1541
+ }
1542
+ async function setupMcp(config) {
1543
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1544
+ const { packageManager, projectDir } = config;
1545
+ log.info("Setting up MCP servers...");
1546
+ const scope = await select({
1547
+ message: "Where should MCP servers be installed?",
1548
+ options: [{
1549
+ value: "project",
1550
+ label: "Project",
1551
+ hint: "Writes to project config files (recommended for teams)"
1552
+ }, {
1553
+ value: "global",
1554
+ label: "Global",
1555
+ hint: "Writes to user-level config files (personal machine)"
1556
+ }],
1557
+ initialValue: "project"
1558
+ });
1559
+ if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1560
+ const recommendedServers = getRecommendedMcpServers(config);
1561
+ if (recommendedServers.length === 0) return Result.ok(void 0);
1562
+ const serverOptions = recommendedServers.map((s) => ({
1563
+ value: s.key,
1564
+ label: s.label,
1565
+ hint: s.target
1566
+ }));
1567
+ const selectedServerKeys = await multiselect({
1568
+ message: "Select MCP servers to install",
1569
+ options: serverOptions,
1570
+ required: false,
1571
+ initialValues: serverOptions.map((o) => o.value)
1572
+ });
1573
+ if (isCancel(selectedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1574
+ if (selectedServerKeys.length === 0) return Result.ok(void 0);
1575
+ const agentOptions = filterAgentsForScope(scope).map((a) => ({
1576
+ value: a.value,
1577
+ label: a.label
1578
+ }));
1579
+ const selectedAgents = await multiselect({
1580
+ message: "Select agents to install MCP servers to",
1581
+ options: agentOptions,
1582
+ required: false,
1583
+ initialValues: uniqueValues$1([
1584
+ "cursor",
1585
+ "claude-code",
1586
+ "vscode"
1587
+ ].filter((a) => agentOptions.some((o) => o.value === a)))
1051
1588
  });
1052
- if (result.isErr()) {
1053
- s.stop("Failed to set up Fumadocs");
1054
- return result;
1589
+ if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1590
+ if (selectedAgents.length === 0) return Result.ok(void 0);
1591
+ const serversByKey = new Map(recommendedServers.map((s) => [s.key, s]));
1592
+ const selectedServers = [];
1593
+ for (const key of selectedServerKeys) {
1594
+ const server = serversByKey.get(key);
1595
+ if (server) selectedServers.push(server);
1055
1596
  }
1056
- s.stop("Fumadocs setup complete!");
1597
+ if (selectedServers.length === 0) return Result.ok(void 0);
1598
+ const installSpinner = spinner();
1599
+ installSpinner.start("Installing MCP servers...");
1600
+ const runner = getPackageRunnerPrefix(packageManager);
1601
+ const globalFlags = scope === "global" ? ["-g"] : [];
1602
+ for (const server of selectedServers) {
1603
+ const transportFlags = server.transport ? ["-t", server.transport] : [];
1604
+ const headerFlags = (server.headers ?? []).flatMap((h) => ["--header", h]);
1605
+ const agentFlags = selectedAgents.flatMap((a) => ["-a", a]);
1606
+ const args = [
1607
+ ...runner,
1608
+ "add-mcp@latest",
1609
+ server.target,
1610
+ "--name",
1611
+ server.name,
1612
+ ...transportFlags,
1613
+ ...headerFlags,
1614
+ ...agentFlags,
1615
+ ...globalFlags,
1616
+ "-y"
1617
+ ];
1618
+ if ((await Result.tryPromise({
1619
+ try: async () => {
1620
+ await $({
1621
+ cwd: projectDir,
1622
+ env: { CI: "true" }
1623
+ })`${args}`;
1624
+ },
1625
+ catch: (e) => new AddonSetupError({
1626
+ addon: "mcp",
1627
+ message: `Failed to install MCP server '${server.name}': ${e instanceof Error ? e.message : String(e)}`,
1628
+ cause: e
1629
+ })
1630
+ })).isErr()) log.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
1631
+ }
1632
+ installSpinner.stop("MCP servers installed");
1057
1633
  return Result.ok(void 0);
1058
1634
  }
1059
1635
 
1060
1636
  //#endregion
1061
1637
  //#region src/helpers/addons/oxlint-setup.ts
1062
1638
  async function setupOxlint(projectDir, packageManager) {
1063
- await addPackageDependency({
1064
- devDependencies: ["oxlint", "oxfmt"],
1065
- projectDir
1639
+ return Result.tryPromise({
1640
+ try: async () => {
1641
+ await addPackageDependency({
1642
+ devDependencies: ["oxlint", "oxfmt"],
1643
+ projectDir
1644
+ });
1645
+ const packageJsonPath = path.join(projectDir, "package.json");
1646
+ if (await fs.pathExists(packageJsonPath)) {
1647
+ const packageJson = await fs.readJson(packageJsonPath);
1648
+ packageJson.scripts = {
1649
+ ...packageJson.scripts,
1650
+ check: "oxlint && oxfmt --write"
1651
+ };
1652
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1653
+ }
1654
+ if (shouldSkipExternalCommands()) return;
1655
+ const s = spinner();
1656
+ s.start("Initializing oxlint and oxfmt...");
1657
+ try {
1658
+ const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
1659
+ await $({
1660
+ cwd: projectDir,
1661
+ env: { CI: "true" }
1662
+ })`${oxlintArgs}`;
1663
+ const oxfmtArgs = getPackageExecutionArgs(packageManager, "oxfmt@latest --init");
1664
+ await $({
1665
+ cwd: projectDir,
1666
+ env: { CI: "true" }
1667
+ })`${oxfmtArgs}`;
1668
+ s.stop("oxlint and oxfmt initialized successfully!");
1669
+ } catch (error) {
1670
+ s.stop("Failed to initialize oxlint and oxfmt");
1671
+ throw error;
1672
+ }
1673
+ },
1674
+ catch: (error) => new AddonSetupError({
1675
+ addon: "oxlint",
1676
+ message: `Failed to set up oxlint: ${error instanceof Error ? error.message : String(error)}`,
1677
+ cause: error
1678
+ })
1066
1679
  });
1067
- const packageJsonPath = path.join(projectDir, "package.json");
1068
- if (await fs.pathExists(packageJsonPath)) {
1069
- const packageJson = await fs.readJson(packageJsonPath);
1070
- packageJson.scripts = {
1071
- ...packageJson.scripts,
1072
- check: "oxlint && oxfmt --write"
1073
- };
1074
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1075
- }
1076
- const s = spinner();
1077
- const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
1078
- s.start("Initializing oxlint and oxfmt...");
1079
- await $({
1080
- cwd: projectDir,
1081
- env: { CI: "true" }
1082
- })`${oxlintArgs}`;
1083
- const oxfmtArgs = getPackageExecutionArgs(packageManager, "oxfmt@latest --init");
1084
- await $({
1085
- cwd: projectDir,
1086
- env: { CI: "true" }
1087
- })`${oxfmtArgs}`;
1088
- s.stop("oxlint and oxfmt initialized successfully!");
1089
1680
  }
1090
1681
 
1091
1682
  //#endregion
1092
1683
  //#region src/helpers/addons/ruler-setup.ts
1093
1684
  async function setupRuler(config) {
1685
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1094
1686
  const { packageManager, projectDir } = config;
1095
1687
  log.info("Setting up Ruler...");
1096
1688
  const rulerDir = path.join(projectDir, ".ruler");
@@ -1189,54 +1781,20 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
1189
1781
  //#endregion
1190
1782
  //#region src/helpers/addons/skills-setup.ts
1191
1783
  const SKILL_SOURCES = {
1192
- "vercel-labs/agent-skills": {
1193
- source: "vercel-labs/agent-skills",
1194
- label: "Vercel Agent Skills"
1195
- },
1196
- "anthropics/skills": {
1197
- source: "https://github.com/anthropics/skills",
1198
- label: "Anthropic Skills"
1199
- },
1200
- "vercel/ai": {
1201
- source: "vercel/ai",
1202
- label: "Vercel AI SDK"
1203
- },
1204
- "vercel/turborepo": {
1205
- source: "vercel/turborepo",
1206
- label: "Turborepo"
1207
- },
1208
- "yusukebe/hono-skill": {
1209
- source: "yusukebe/hono-skill",
1210
- label: "Hono Backend"
1211
- },
1212
- "vercel-labs/next-skills": {
1213
- source: "vercel-labs/next-skills",
1214
- label: "Next.js Best Practices"
1215
- },
1216
- "heroui-inc/heroui": {
1217
- source: "heroui-inc/heroui",
1218
- label: "HeroUI Native"
1219
- },
1220
- "better-auth/skills": {
1221
- source: "better-auth/skills",
1222
- label: "Better Auth"
1223
- },
1224
- "neondatabase/agent-skills": {
1225
- source: "neondatabase/agent-skills",
1226
- label: "Neon Database"
1227
- },
1228
- "supabase/agent-skills": {
1229
- source: "supabase/agent-skills",
1230
- label: "Supabase"
1231
- },
1232
- "elysiajs/skills": {
1233
- source: "elysiajs/skills",
1234
- label: "ElysiaJS"
1235
- },
1236
- "waynesutton/convexskills": {
1237
- source: "waynesutton/convexskills",
1238
- label: "Convex"
1239
- }
1784
+ "vercel-labs/agent-skills": { label: "Vercel Agent Skills" },
1785
+ "vercel/ai": { label: "Vercel AI SDK" },
1786
+ "vercel/turborepo": { label: "Turborepo" },
1787
+ "yusukebe/hono-skill": { label: "Hono Backend" },
1788
+ "vercel-labs/next-skills": { label: "Next.js Best Practices" },
1789
+ "nuxt/ui": { label: "Nuxt UI" },
1790
+ "heroui-inc/heroui": { label: "HeroUI Native" },
1791
+ "better-auth/skills": { label: "Better Auth" },
1792
+ "neondatabase/agent-skills": { label: "Neon Database" },
1793
+ "supabase/agent-skills": { label: "Supabase" },
1794
+ "expo/skills": { label: "Expo" },
1795
+ "prisma/skills": { label: "Prisma" },
1796
+ "elysiajs/skills": { label: "ElysiaJS" },
1797
+ "waynesutton/convexskills": { label: "Convex" }
1240
1798
  };
1241
1799
  const AVAILABLE_AGENTS = [
1242
1800
  {
@@ -1340,15 +1898,24 @@ const AVAILABLE_AGENTS = [
1340
1898
  label: "MCPJam"
1341
1899
  }
1342
1900
  ];
1901
+ function hasReactBasedFrontend(frontend) {
1902
+ return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
1903
+ }
1904
+ function hasNativeFrontend(frontend) {
1905
+ return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
1906
+ }
1343
1907
  function getRecommendedSourceKeys(config) {
1344
1908
  const sources = [];
1345
- const { frontend, backend, dbSetup, auth, examples, addons } = config;
1346
- if (frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next")) sources.push("vercel-labs/agent-skills");
1909
+ const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
1910
+ if (hasReactBasedFrontend(frontend)) sources.push("vercel-labs/agent-skills");
1347
1911
  if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
1912
+ if (frontend.includes("nuxt")) sources.push("nuxt/ui");
1348
1913
  if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
1914
+ if (hasNativeFrontend(frontend)) sources.push("expo/skills");
1349
1915
  if (auth === "better-auth") sources.push("better-auth/skills");
1350
1916
  if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
1351
1917
  if (dbSetup === "supabase") sources.push("supabase/agent-skills");
1918
+ if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
1352
1919
  if (examples.includes("ai")) sources.push("vercel/ai");
1353
1920
  if (addons.includes("turborepo")) sources.push("vercel/turborepo");
1354
1921
  if (backend === "hono") sources.push("yusukebe/hono-skill");
@@ -1356,54 +1923,95 @@ function getRecommendedSourceKeys(config) {
1356
1923
  if (backend === "convex") sources.push("waynesutton/convexskills");
1357
1924
  return sources;
1358
1925
  }
1359
- function parseSkillsFromOutput(output) {
1360
- const skills = [];
1361
- const lines = output.split("\n");
1362
- for (const line of lines) {
1363
- const match = line.replace(/\x1b\[[0-9;]*m/g, "").match(/^│\s{4}([a-z][a-z0-9-]*)$/);
1364
- if (match) skills.push(match[1]);
1365
- }
1366
- return skills;
1926
+ const CURATED_SKILLS_BY_SOURCE = {
1927
+ "vercel-labs/agent-skills": (config) => {
1928
+ const skills = [
1929
+ "web-design-guidelines",
1930
+ "vercel-composition-patterns",
1931
+ "vercel-react-best-practices"
1932
+ ];
1933
+ if (hasNativeFrontend(config.frontend)) skills.push("vercel-react-native-skills");
1934
+ return skills;
1935
+ },
1936
+ "vercel/ai": () => ["ai-sdk"],
1937
+ "vercel/turborepo": () => ["turborepo"],
1938
+ "yusukebe/hono-skill": () => ["hono"],
1939
+ "vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
1940
+ "nuxt/ui": () => ["nuxt-ui"],
1941
+ "heroui-inc/heroui": () => ["heroui-native"],
1942
+ "better-auth/skills": () => ["better-auth-best-practices"],
1943
+ "neondatabase/agent-skills": () => ["neon-postgres"],
1944
+ "supabase/agent-skills": () => ["supabase-postgres-best-practices"],
1945
+ "expo/skills": (config) => {
1946
+ const skills = [
1947
+ "expo-dev-client",
1948
+ "building-native-ui",
1949
+ "native-data-fetching",
1950
+ "expo-deployment",
1951
+ "upgrading-expo",
1952
+ "expo-cicd-workflows"
1953
+ ];
1954
+ if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
1955
+ return skills;
1956
+ },
1957
+ "prisma/skills": (config) => {
1958
+ const skills = [];
1959
+ if (config.orm === "prisma") skills.push("prisma-cli", "prisma-client-api", "prisma-database-setup");
1960
+ if (config.dbSetup === "prisma-postgres") skills.push("prisma-postgres");
1961
+ return skills;
1962
+ },
1963
+ "elysiajs/skills": () => ["elysiajs"],
1964
+ "waynesutton/convexskills": () => [
1965
+ "convex-best-practices",
1966
+ "convex-functions",
1967
+ "convex-schema-validator",
1968
+ "convex-realtime",
1969
+ "convex-http-actions",
1970
+ "convex-cron-jobs",
1971
+ "convex-file-storage",
1972
+ "convex-migrations",
1973
+ "convex-security-check"
1974
+ ]
1975
+ };
1976
+ function getCuratedSkillNamesForSourceKey(sourceKey, config) {
1977
+ return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
1367
1978
  }
1368
- async function fetchSkillsFromSource(source, packageManager, projectDir) {
1369
- try {
1370
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source.source} --list`);
1371
- return parseSkillsFromOutput((await $({
1372
- cwd: projectDir,
1373
- env: { CI: "true" }
1374
- })`${args}`).stdout);
1375
- } catch {
1376
- return [];
1377
- }
1979
+ function uniqueValues(values) {
1980
+ return Array.from(new Set(values));
1378
1981
  }
1379
1982
  async function setupSkills(config) {
1983
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1380
1984
  const { packageManager, projectDir } = config;
1381
1985
  const btsConfig = await readBtsConfig(projectDir);
1382
- const recommendedSourceKeys = getRecommendedSourceKeys(btsConfig ? {
1986
+ const fullConfig = btsConfig ? {
1383
1987
  ...config,
1384
1988
  addons: btsConfig.addons ?? config.addons
1385
- } : config);
1989
+ } : config;
1990
+ const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
1386
1991
  if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
1387
- const s = spinner();
1388
- s.start("Fetching available skills...");
1389
- const allSkills = [];
1390
- for (const sourceKey of recommendedSourceKeys) {
1992
+ const skillOptions = uniqueValues(recommendedSourceKeys).flatMap((sourceKey) => {
1391
1993
  const source = SKILL_SOURCES[sourceKey];
1392
- if (!source) continue;
1393
- const skills = await fetchSkillsFromSource(source, packageManager, projectDir);
1394
- for (const skillName of skills) allSkills.push({
1395
- name: skillName,
1396
- source: source.source,
1397
- sourceLabel: source.label
1398
- });
1399
- }
1400
- s.stop("Fetched available skills");
1401
- if (allSkills.length === 0) return Result.ok(void 0);
1402
- const skillOptions = allSkills.map((skill) => ({
1403
- value: `${skill.source}::${skill.name}`,
1404
- label: skill.name,
1405
- hint: skill.sourceLabel
1406
- }));
1994
+ return getCuratedSkillNamesForSourceKey(sourceKey, fullConfig).map((skillName) => ({
1995
+ value: `${sourceKey}::${skillName}`,
1996
+ label: skillName,
1997
+ hint: source.label
1998
+ }));
1999
+ });
2000
+ if (skillOptions.length === 0) return Result.ok(void 0);
2001
+ const scope = await select({
2002
+ message: "Where should skills be installed?",
2003
+ options: [{
2004
+ value: "project",
2005
+ label: "Project",
2006
+ hint: "Writes to project config files (recommended for teams)"
2007
+ }, {
2008
+ value: "global",
2009
+ label: "Global",
2010
+ hint: "Writes to user-level config files (personal machine)"
2011
+ }],
2012
+ initialValue: "project"
2013
+ });
2014
+ if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1407
2015
  const selectedSkills = await multiselect({
1408
2016
  message: "Select skills to install",
1409
2017
  options: skillOptions,
@@ -1433,11 +2041,12 @@ async function setupSkills(config) {
1433
2041
  const installSpinner = spinner();
1434
2042
  installSpinner.start("Installing skills...");
1435
2043
  const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
2044
+ const globalFlag = scope === "global" ? "-g" : "";
1436
2045
  for (const [source, skills] of Object.entries(skillsBySource)) {
1437
- const skillFlags = skills.map((s$1) => `-s ${s$1}`).join(" ");
2046
+ const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
1438
2047
  if ((await Result.tryPromise({
1439
2048
  try: async () => {
1440
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${skillFlags} ${agentFlags} -y`);
2049
+ const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
1441
2050
  await $({
1442
2051
  cwd: projectDir,
1443
2052
  env: { CI: "true" }
@@ -1457,6 +2066,7 @@ async function setupSkills(config) {
1457
2066
  //#endregion
1458
2067
  //#region src/helpers/addons/starlight-setup.ts
1459
2068
  async function setupStarlight(config) {
2069
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1460
2070
  const { packageManager, projectDir } = config;
1461
2071
  const s = spinner();
1462
2072
  s.start("Setting up Starlight docs...");
@@ -1496,6 +2106,7 @@ async function setupStarlight(config) {
1496
2106
  //#endregion
1497
2107
  //#region src/helpers/addons/tauri-setup.ts
1498
2108
  async function setupTauri(config) {
2109
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1499
2110
  const { packageManager, frontend, projectDir } = config;
1500
2111
  const s = spinner();
1501
2112
  const clientPackageDir = path.join(projectDir, "apps/web");
@@ -1556,14 +2167,15 @@ const TEMPLATES$1 = {
1556
2167
  }
1557
2168
  };
1558
2169
  async function setupTui(config) {
2170
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1559
2171
  const { packageManager, projectDir } = config;
1560
2172
  log.info("Setting up OpenTUI...");
1561
2173
  const template = await select({
1562
2174
  message: "Choose a template",
1563
- options: Object.entries(TEMPLATES$1).map(([key, template$1]) => ({
2175
+ options: Object.entries(TEMPLATES$1).map(([key, template]) => ({
1564
2176
  value: key,
1565
- label: template$1.label,
1566
- hint: template$1.hint
2177
+ label: template.label,
2178
+ hint: template.hint
1567
2179
  })),
1568
2180
  initialValue: "core"
1569
2181
  });
@@ -1681,6 +2293,7 @@ function getFrameworksFromFrontend(frontend) {
1681
2293
  return Array.from(frameworks);
1682
2294
  }
1683
2295
  async function setupUltracite(config, gitHooks) {
2296
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1684
2297
  const { packageManager, projectDir, frontend } = config;
1685
2298
  log.info("Setting up Ultracite...");
1686
2299
  let result;
@@ -1689,10 +2302,10 @@ async function setupUltracite(config, gitHooks) {
1689
2302
  return await group({
1690
2303
  linter: () => select({
1691
2304
  message: "Choose linter/formatter",
1692
- options: Object.entries(LINTERS).map(([key, linter$1]) => ({
2305
+ options: Object.entries(LINTERS).map(([key, linter]) => ({
1693
2306
  value: key,
1694
- label: linter$1.label,
1695
- hint: linter$1.hint
2307
+ label: linter.label,
2308
+ hint: linter.hint
1696
2309
  })),
1697
2310
  initialValue: "biome"
1698
2311
  }),
@@ -1811,14 +2424,15 @@ const TEMPLATES = {
1811
2424
  }
1812
2425
  };
1813
2426
  async function setupWxt(config) {
2427
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1814
2428
  const { packageManager, projectDir } = config;
1815
2429
  log.info("Setting up WXT...");
1816
2430
  const template = await select({
1817
2431
  message: "Choose a template",
1818
- options: Object.entries(TEMPLATES).map(([key, template$1]) => ({
2432
+ options: Object.entries(TEMPLATES).map(([key, template]) => ({
1819
2433
  value: key,
1820
- label: template$1.label,
1821
- hint: template$1.hint
2434
+ label: template.label,
2435
+ hint: template.hint
1822
2436
  })),
1823
2437
  initialValue: "react"
1824
2438
  });
@@ -1886,6 +2500,17 @@ async function runSetup(setupFn) {
1886
2500
  consola.error(pc.red(result.error.message));
1887
2501
  }
1888
2502
  }
2503
+ async function runAddonStep(addon, step) {
2504
+ const result = await Result.tryPromise({
2505
+ try: async () => step(),
2506
+ catch: (e) => new AddonSetupError({
2507
+ addon,
2508
+ message: `Failed to set up ${addon}: ${e instanceof Error ? e.message : String(e)}`,
2509
+ cause: e
2510
+ })
2511
+ });
2512
+ if (result.isErr()) consola.error(pc.red(result.error.message));
2513
+ }
1889
2514
  async function setupAddons(config) {
1890
2515
  const { addons, frontend, projectDir } = config;
1891
2516
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
@@ -1905,14 +2530,14 @@ async function setupAddons(config) {
1905
2530
  if (hasLefthook) gitHooks.push("lefthook");
1906
2531
  await runSetup(() => setupUltracite(config, gitHooks));
1907
2532
  } else {
1908
- if (hasBiome) await setupBiome(projectDir);
1909
- if (hasOxlint) await setupOxlint(projectDir, config.packageManager);
2533
+ if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
2534
+ if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
1910
2535
  if (hasHusky || hasLefthook) {
1911
2536
  let linter;
1912
2537
  if (hasOxlint) linter = "oxlint";
1913
2538
  else if (hasBiome) linter = "biome";
1914
- if (hasHusky) await setupHusky(projectDir, linter);
1915
- if (hasLefthook) await setupLefthook(projectDir);
2539
+ if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
2540
+ if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
1916
2541
  }
1917
2542
  }
1918
2543
  if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
@@ -1921,6 +2546,7 @@ async function setupAddons(config) {
1921
2546
  if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
1922
2547
  if (addons.includes("ruler")) await runSetup(() => setupRuler(config));
1923
2548
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
2549
+ if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
1924
2550
  }
1925
2551
  async function setupBiome(projectDir) {
1926
2552
  await addPackageDependency({
@@ -1996,6 +2622,7 @@ async function detectProjectConfig(projectDir) {
1996
2622
  //#endregion
1997
2623
  //#region src/helpers/core/install-dependencies.ts
1998
2624
  async function installDependencies({ projectDir, packageManager }) {
2625
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
1999
2626
  const s = spinner();
2000
2627
  s.start(`Running ${packageManager} install...`);
2001
2628
  const result = await Result.tryPromise({
@@ -2230,13 +2857,13 @@ async function getAuthChoice(auth, backend, frontend) {
2230
2857
  label: "None",
2231
2858
  hint: "No auth"
2232
2859
  });
2233
- const response$1 = await navigableSelect({
2860
+ const response = await navigableSelect({
2234
2861
  message: "Select authentication provider",
2235
2862
  options,
2236
2863
  initialValue: "none"
2237
2864
  });
2238
- if (isCancel$1(response$1)) throw new UserCancelledError({ message: "Operation cancelled" });
2239
- return response$1;
2865
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
2866
+ return response;
2240
2867
  }
2241
2868
  const response = await navigableSelect({
2242
2869
  message: "Select authentication provider",
@@ -2835,6 +3462,27 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
2835
3462
  //#endregion
2836
3463
  //#region src/prompts/config-prompts.ts
2837
3464
  async function gatherConfig(flags, projectName, projectDir, relativePath) {
3465
+ if (isSilent()) return {
3466
+ projectName,
3467
+ projectDir,
3468
+ relativePath,
3469
+ frontend: flags.frontend ?? [...DEFAULT_CONFIG.frontend],
3470
+ backend: flags.backend ?? DEFAULT_CONFIG.backend,
3471
+ runtime: flags.runtime ?? DEFAULT_CONFIG.runtime,
3472
+ database: flags.database ?? DEFAULT_CONFIG.database,
3473
+ orm: flags.orm ?? DEFAULT_CONFIG.orm,
3474
+ auth: flags.auth ?? DEFAULT_CONFIG.auth,
3475
+ payments: flags.payments ?? DEFAULT_CONFIG.payments,
3476
+ addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
3477
+ examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
3478
+ git: flags.git ?? DEFAULT_CONFIG.git,
3479
+ packageManager: flags.packageManager ?? DEFAULT_CONFIG.packageManager,
3480
+ install: flags.install ?? DEFAULT_CONFIG.install,
3481
+ dbSetup: flags.dbSetup ?? DEFAULT_CONFIG.dbSetup,
3482
+ api: flags.api ?? DEFAULT_CONFIG.api,
3483
+ webDeploy: flags.webDeploy ?? DEFAULT_CONFIG.webDeploy,
3484
+ serverDeploy: flags.serverDeploy ?? DEFAULT_CONFIG.serverDeploy
3485
+ };
2838
3486
  const result = await navigableGroup({
2839
3487
  frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
2840
3488
  backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
@@ -2880,7 +3528,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
2880
3528
 
2881
3529
  //#endregion
2882
3530
  //#region src/prompts/project-name.ts
2883
- function isPathWithinCwd(targetPath) {
3531
+ function isPathWithinCwd$1(targetPath) {
2884
3532
  const resolved = path.resolve(targetPath);
2885
3533
  const rel = path.relative(process.cwd(), resolved);
2886
3534
  return !rel.startsWith("..") && !path.isAbsolute(rel);
@@ -2894,7 +3542,7 @@ async function getProjectName(initialName) {
2894
3542
  if (initialName) {
2895
3543
  if (initialName === ".") return initialName;
2896
3544
  if (!validateDirectoryName(path.basename(initialName))) {
2897
- if (isPathWithinCwd(path.resolve(process.cwd(), initialName))) return initialName;
3545
+ if (isPathWithinCwd$1(path.resolve(process.cwd(), initialName))) return initialName;
2898
3546
  consola.error(pc.red("Project path must be within current directory"));
2899
3547
  }
2900
3548
  }
@@ -2917,7 +3565,7 @@ async function getProjectName(initialName) {
2917
3565
  const validationError = validateDirectoryName(path.basename(nameToUse));
2918
3566
  if (validationError) return validationError;
2919
3567
  if (nameToUse !== ".") {
2920
- if (!isPathWithinCwd(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
3568
+ if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
2921
3569
  }
2922
3570
  }
2923
3571
  });
@@ -2928,13 +3576,6 @@ async function getProjectName(initialName) {
2928
3576
  return projectPath;
2929
3577
  }
2930
3578
 
2931
- //#endregion
2932
- //#region src/utils/get-latest-cli-version.ts
2933
- const getLatestCLIVersion = () => {
2934
- const packageJsonPath = path.join(PKG_ROOT, "package.json");
2935
- return fs.readJSONSync(packageJsonPath).version ?? "1.0.0";
2936
- };
2937
-
2938
3579
  //#endregion
2939
3580
  //#region src/utils/telemetry.ts
2940
3581
  /**
@@ -2945,7 +3586,7 @@ const getLatestCLIVersion = () => {
2945
3586
  */
2946
3587
  function isTelemetryEnabled() {
2947
3588
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
2948
- const BTS_TELEMETRY = "1";
3589
+ const BTS_TELEMETRY = "0";
2949
3590
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
2950
3591
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
2951
3592
  return true;
@@ -2953,17 +3594,7 @@ function isTelemetryEnabled() {
2953
3594
 
2954
3595
  //#endregion
2955
3596
  //#region src/utils/analytics.ts
2956
- const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
2957
- async function sendConvexEvent(payload) {
2958
- await Result.tryPromise({
2959
- try: () => fetch(CONVEX_INGEST_URL, {
2960
- method: "POST",
2961
- headers: { "Content-Type": "application/json" },
2962
- body: JSON.stringify(payload)
2963
- }),
2964
- catch: () => void 0
2965
- });
2966
- }
3597
+ async function sendConvexEvent(payload) {}
2967
3598
  async function trackProjectCreation(config, disableAnalytics = false) {
2968
3599
  if (!isTelemetryEnabled() || disableAnalytics) return;
2969
3600
  const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
@@ -3088,138 +3719,44 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
3088
3719
  finalBaseName = path.basename(finalResolvedPath);
3089
3720
  }
3090
3721
  if (shouldClearDirectory) {
3091
- const s = spinner();
3092
- s.start(`Clearing directory "${finalResolvedPath}"...`);
3093
- const clearResult = await Result.tryPromise({
3094
- try: () => fs.emptyDir(finalResolvedPath),
3095
- catch: (error) => new CLIError({
3096
- message: `Failed to clear directory "${finalResolvedPath}".`,
3097
- cause: error
3098
- })
3099
- });
3100
- if (clearResult.isErr()) {
3101
- s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
3102
- throw clearResult.error;
3103
- }
3104
- s.stop(`Directory "${finalResolvedPath}" cleared.`);
3105
- } else await fs.ensureDir(finalResolvedPath);
3106
- return {
3107
- finalResolvedPath,
3108
- finalBaseName
3109
- };
3110
- }
3111
-
3112
- //#endregion
3113
- //#region src/utils/project-history.ts
3114
- const paths = envPaths("better-t-stack", { suffix: "" });
3115
- const HISTORY_FILE = "history.json";
3116
- var HistoryError = class extends TaggedError("HistoryError")() {};
3117
- function getHistoryDir() {
3118
- return paths.data;
3119
- }
3120
- function getHistoryPath() {
3121
- return path.join(paths.data, HISTORY_FILE);
3122
- }
3123
- function generateId() {
3124
- return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
3125
- }
3126
- function emptyHistory() {
3127
- return {
3128
- version: 1,
3129
- entries: []
3130
- };
3131
- }
3132
- async function ensureHistoryDir() {
3133
- return Result.tryPromise({
3134
- try: async () => {
3135
- await fs.ensureDir(getHistoryDir());
3136
- },
3137
- catch: (e) => new HistoryError({
3138
- message: `Failed to create history directory: ${e instanceof Error ? e.message : String(e)}`,
3139
- cause: e
3140
- })
3141
- });
3142
- }
3143
- async function readHistory() {
3144
- const historyPath = getHistoryPath();
3145
- const existsResult = await Result.tryPromise({
3146
- try: async () => await fs.pathExists(historyPath),
3147
- catch: (e) => new HistoryError({
3148
- message: `Failed to check history file: ${e instanceof Error ? e.message : String(e)}`,
3149
- cause: e
3150
- })
3151
- });
3152
- if (existsResult.isErr()) return existsResult;
3153
- if (!existsResult.value) return Result.ok(emptyHistory());
3154
- const readResult = await Result.tryPromise({
3155
- try: async () => await fs.readJson(historyPath),
3156
- catch: (e) => new HistoryError({
3157
- message: `Failed to read history file: ${e instanceof Error ? e.message : String(e)}`,
3158
- cause: e
3159
- })
3160
- });
3161
- if (readResult.isErr()) return Result.ok(emptyHistory());
3162
- return Result.ok(readResult.value);
3163
- }
3164
- async function writeHistory(history) {
3165
- const ensureDirResult = await ensureHistoryDir();
3166
- if (ensureDirResult.isErr()) return ensureDirResult;
3167
- return Result.tryPromise({
3168
- try: async () => {
3169
- await fs.writeJson(getHistoryPath(), history, { spaces: 2 });
3170
- },
3171
- catch: (e) => new HistoryError({
3172
- message: `Failed to write history file: ${e instanceof Error ? e.message : String(e)}`,
3173
- cause: e
3174
- })
3175
- });
3176
- }
3177
- async function addToHistory(config, reproducibleCommand) {
3178
- const historyResult = await readHistory();
3179
- if (historyResult.isErr()) return historyResult;
3180
- const history = historyResult.value;
3181
- const entry = {
3182
- id: generateId(),
3183
- projectName: config.projectName,
3184
- projectDir: config.projectDir,
3185
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3186
- stack: {
3187
- frontend: config.frontend,
3188
- backend: config.backend,
3189
- database: config.database,
3190
- orm: config.orm,
3191
- runtime: config.runtime,
3192
- auth: config.auth,
3193
- payments: config.payments,
3194
- api: config.api,
3195
- addons: config.addons,
3196
- examples: config.examples,
3197
- dbSetup: config.dbSetup,
3198
- packageManager: config.packageManager
3199
- },
3200
- cliVersion: getLatestCLIVersion(),
3201
- reproducibleCommand
3722
+ const s = spinner();
3723
+ s.start(`Clearing directory "${finalResolvedPath}"...`);
3724
+ const clearResult = await Result.tryPromise({
3725
+ try: () => fs.emptyDir(finalResolvedPath),
3726
+ catch: (error) => new CLIError({
3727
+ message: `Failed to clear directory "${finalResolvedPath}".`,
3728
+ cause: error
3729
+ })
3730
+ });
3731
+ if (clearResult.isErr()) {
3732
+ s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
3733
+ throw clearResult.error;
3734
+ }
3735
+ s.stop(`Directory "${finalResolvedPath}" cleared.`);
3736
+ } else await fs.ensureDir(finalResolvedPath);
3737
+ return {
3738
+ finalResolvedPath,
3739
+ finalBaseName
3202
3740
  };
3203
- history.entries.unshift(entry);
3204
- if (history.entries.length > 100) history.entries = history.entries.slice(0, 100);
3205
- return await writeHistory(history);
3206
3741
  }
3207
- async function getHistory(limit = 10) {
3208
- const historyResult = await readHistory();
3209
- if (historyResult.isErr()) return historyResult;
3210
- return Result.ok(historyResult.value.entries.slice(0, limit));
3742
+
3743
+ //#endregion
3744
+ //#region src/utils/project-name-validation.ts
3745
+ function validateProjectName(name) {
3746
+ const result = types_exports.ProjectNameSchema.safeParse(name);
3747
+ if (!result.success) return Result.err(new ValidationError({
3748
+ field: "projectName",
3749
+ value: name,
3750
+ message: `Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`
3751
+ }));
3752
+ return Result.ok(void 0);
3211
3753
  }
3212
- async function clearHistory() {
3213
- const historyPath = getHistoryPath();
3214
- return Result.tryPromise({
3215
- try: async () => {
3216
- if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
3217
- },
3218
- catch: (e) => new HistoryError({
3219
- message: `Failed to clear history: ${e instanceof Error ? e.message : String(e)}`,
3220
- cause: e
3221
- })
3222
- });
3754
+ function extractAndValidateProjectName(projectName, projectDirectory) {
3755
+ const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
3756
+ if (!derivedName) return Result.ok("");
3757
+ const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
3758
+ if (validationResult.isErr()) return Result.err(validationResult.error);
3759
+ return Result.ok(projectName || derivedName);
3223
3760
  }
3224
3761
 
3225
3762
  //#endregion
@@ -3515,7 +4052,7 @@ function validateFullConfig(config, providedFlags, options) {
3515
4052
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
3516
4053
  yield* validateSelfBackendCompatibility(providedFlags, options, config);
3517
4054
  yield* validateWorkersCompatibility(providedFlags, options, config);
3518
- if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
4055
+ if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
3519
4056
  if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
3520
4057
  if (config.addons && config.addons.length > 0) {
3521
4058
  yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
@@ -3538,25 +4075,6 @@ function validateConfigForProgrammaticUse(config) {
3538
4075
  });
3539
4076
  }
3540
4077
 
3541
- //#endregion
3542
- //#region src/utils/project-name-validation.ts
3543
- function validateProjectName(name) {
3544
- const result = types_exports.ProjectNameSchema.safeParse(name);
3545
- if (!result.success) return Result.err(new ValidationError({
3546
- field: "projectName",
3547
- value: name,
3548
- message: `Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`
3549
- }));
3550
- return Result.ok(void 0);
3551
- }
3552
- function extractAndValidateProjectName(projectName, projectDirectory) {
3553
- const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
3554
- if (!derivedName) return Result.ok("");
3555
- const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
3556
- if (validationResult.isErr()) return Result.err(validationResult.error);
3557
- return Result.ok(projectName || derivedName);
3558
- }
3559
-
3560
4078
  //#endregion
3561
4079
  //#region src/validation.ts
3562
4080
  const CORE_STACK_FLAGS = new Set([
@@ -3584,8 +4102,8 @@ function validateYesFlagCombination(options, providedFlags) {
3584
4102
  function processAndValidateFlags(options, providedFlags, projectName) {
3585
4103
  if (options.yolo) {
3586
4104
  const cfg = processFlags(options, projectName);
3587
- const validatedProjectNameResult$1 = extractAndValidateProjectName(projectName, options.projectDirectory);
3588
- if (validatedProjectNameResult$1.isOk() && validatedProjectNameResult$1.value) cfg.projectName = validatedProjectNameResult$1.value;
4105
+ const validatedProjectNameResult = extractAndValidateProjectName(projectName, options.projectDirectory);
4106
+ if (validatedProjectNameResult.isOk() && validatedProjectNameResult.value) cfg.projectName = validatedProjectNameResult.value;
3589
4107
  return Result.ok(cfg);
3590
4108
  }
3591
4109
  const yesFlagResult = validateYesFlagCombination(options, providedFlags);
@@ -3696,18 +4214,26 @@ async function addEnvVariablesToFile(envPath, variables) {
3696
4214
  //#region src/helpers/database-providers/d1-setup.ts
3697
4215
  async function setupCloudflareD1(config) {
3698
4216
  const { projectDir, serverDeploy, orm, backend } = config;
3699
- if (serverDeploy === "cloudflare" && orm === "prisma") {
3700
- const targetApp2 = backend === "self" ? "apps/web" : "apps/server";
3701
- await addEnvVariablesToFile(path.join(projectDir, targetApp2, ".env"), [{
3702
- key: "DATABASE_URL",
3703
- value: `file:${path.join(projectDir, "apps/server", "local.db")}`,
3704
- condition: true
3705
- }]);
3706
- await addPackageDependency({
3707
- dependencies: ["@prisma/adapter-d1"],
3708
- projectDir: path.join(projectDir, backend === "self" ? "apps/web" : "apps/server")
3709
- });
3710
- }
4217
+ if (!(serverDeploy === "cloudflare" && orm === "prisma")) return Result.ok(void 0);
4218
+ return Result.tryPromise({
4219
+ try: async () => {
4220
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
4221
+ await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
4222
+ key: "DATABASE_URL",
4223
+ value: `file:${path.join(projectDir, targetApp, "local.db")}`,
4224
+ condition: true
4225
+ }]);
4226
+ await addPackageDependency({
4227
+ dependencies: ["@prisma/adapter-d1"],
4228
+ projectDir: path.join(projectDir, backend === "self" ? "apps/web" : "apps/server")
4229
+ });
4230
+ },
4231
+ catch: (e) => new DatabaseSetupError({
4232
+ provider: "d1",
4233
+ message: `Failed to set up Cloudflare D1: ${e instanceof Error ? e.message : String(e)}`,
4234
+ cause: e
4235
+ })
4236
+ });
3711
4237
  }
3712
4238
 
3713
4239
  //#endregion
@@ -3849,8 +4375,8 @@ async function setupMongoDBAtlas(config, cliInput) {
3849
4375
  if (ensureDirResult.isErr()) return ensureDirResult;
3850
4376
  if (manualDb) {
3851
4377
  log.info("MongoDB Atlas manual setup selected");
3852
- const envResult$1 = await writeEnvFile$3(projectDir, backend);
3853
- if (envResult$1.isErr()) return envResult$1;
4378
+ const envResult = await writeEnvFile$3(projectDir, backend);
4379
+ if (envResult.isErr()) return envResult;
3854
4380
  displayManualSetupInstructions$3();
3855
4381
  return Result.ok(void 0);
3856
4382
  }
@@ -3870,15 +4396,15 @@ async function setupMongoDBAtlas(config, cliInput) {
3870
4396
  if (isCancel(mode)) return userCancelled("Operation cancelled");
3871
4397
  if (mode === "manual") {
3872
4398
  log.info("MongoDB Atlas manual setup selected");
3873
- const envResult$1 = await writeEnvFile$3(projectDir, backend);
3874
- if (envResult$1.isErr()) return envResult$1;
4399
+ const envResult = await writeEnvFile$3(projectDir, backend);
4400
+ if (envResult.isErr()) return envResult;
3875
4401
  displayManualSetupInstructions$3();
3876
4402
  return Result.ok(void 0);
3877
4403
  }
3878
4404
  const mongoConfigResult = await initMongoDBAtlas(serverDir);
3879
4405
  if (mongoConfigResult.isOk()) {
3880
- const envResult$1 = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
3881
- if (envResult$1.isErr()) return envResult$1;
4406
+ const envResult = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
4407
+ if (envResult.isErr()) return envResult;
3882
4408
  log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
3883
4409
  return Result.ok(void 0);
3884
4410
  }
@@ -4037,8 +4563,8 @@ async function setupNeonPostgres(config, cliInput) {
4037
4563
  const manualDb = cliInput?.manualDb ?? false;
4038
4564
  const target = backend === "self" ? "apps/web" : "apps/server";
4039
4565
  if (manualDb) {
4040
- const envResult$1 = await writeEnvFile$2(projectDir, backend);
4041
- if (envResult$1.isErr()) return envResult$1;
4566
+ const envResult = await writeEnvFile$2(projectDir, backend);
4567
+ if (envResult.isErr()) return envResult;
4042
4568
  displayManualSetupInstructions$2(target);
4043
4569
  return Result.ok(void 0);
4044
4570
  }
@@ -4057,8 +4583,8 @@ async function setupNeonPostgres(config, cliInput) {
4057
4583
  });
4058
4584
  if (isCancel(mode)) return userCancelled("Operation cancelled");
4059
4585
  if (mode === "manual") {
4060
- const envResult$1 = await writeEnvFile$2(projectDir, backend);
4061
- if (envResult$1.isErr()) return envResult$1;
4586
+ const envResult = await writeEnvFile$2(projectDir, backend);
4587
+ if (envResult.isErr()) return envResult;
4062
4588
  displayManualSetupInstructions$2(target);
4063
4589
  return Result.ok(void 0);
4064
4590
  }
@@ -4080,8 +4606,8 @@ async function setupNeonPostgres(config, cliInput) {
4080
4606
  const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
4081
4607
  if (neonDbResult.isErr()) {
4082
4608
  log.error(pc.red(neonDbResult.error.message));
4083
- const envResult$1 = await writeEnvFile$2(projectDir, backend);
4084
- if (envResult$1.isErr()) return envResult$1;
4609
+ const envResult = await writeEnvFile$2(projectDir, backend);
4610
+ if (envResult.isErr()) return envResult;
4085
4611
  displayManualSetupInstructions$2(target);
4086
4612
  } else log.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
4087
4613
  return neonDbResult;
@@ -4102,8 +4628,8 @@ async function setupNeonPostgres(config, cliInput) {
4102
4628
  const neonConfigResult = await createNeonProject(projectName, regionId, packageManager);
4103
4629
  if (neonConfigResult.isErr()) {
4104
4630
  log.error(pc.red(neonConfigResult.error.message));
4105
- const envResult$1 = await writeEnvFile$2(projectDir, backend);
4106
- if (envResult$1.isErr()) return envResult$1;
4631
+ const envResult = await writeEnvFile$2(projectDir, backend);
4632
+ if (envResult.isErr()) return envResult;
4107
4633
  displayManualSetupInstructions$2(target);
4108
4634
  return Result.ok(void 0);
4109
4635
  }
@@ -4123,61 +4649,71 @@ async function setupNeonPostgres(config, cliInput) {
4123
4649
  //#region src/helpers/database-providers/planetscale-setup.ts
4124
4650
  async function setupPlanetScale(config) {
4125
4651
  const { projectDir, database, orm, backend } = config;
4126
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
4127
- const envPath = path.join(projectDir, targetApp, ".env");
4128
- if (database === "mysql" && orm === "drizzle") {
4129
- const variables = [
4130
- {
4131
- key: "DATABASE_URL",
4132
- value: "mysql://username:password@host/database?ssl={\"rejectUnauthorized\":true}",
4133
- condition: true
4134
- },
4135
- {
4136
- key: "DATABASE_HOST",
4137
- value: "",
4138
- condition: true
4139
- },
4140
- {
4141
- key: "DATABASE_USERNAME",
4142
- value: "",
4143
- condition: true
4144
- },
4145
- {
4146
- key: "DATABASE_PASSWORD",
4147
- value: "",
4148
- condition: true
4652
+ if (!["mysql", "postgres"].includes(database)) return Result.ok(void 0);
4653
+ return Result.tryPromise({
4654
+ try: async () => {
4655
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
4656
+ const envPath = path.join(projectDir, targetApp, ".env");
4657
+ if (database === "mysql" && orm === "drizzle") {
4658
+ const variables = [
4659
+ {
4660
+ key: "DATABASE_URL",
4661
+ value: "mysql://username:password@host/database?ssl={\"rejectUnauthorized\":true}",
4662
+ condition: true
4663
+ },
4664
+ {
4665
+ key: "DATABASE_HOST",
4666
+ value: "",
4667
+ condition: true
4668
+ },
4669
+ {
4670
+ key: "DATABASE_USERNAME",
4671
+ value: "",
4672
+ condition: true
4673
+ },
4674
+ {
4675
+ key: "DATABASE_PASSWORD",
4676
+ value: "",
4677
+ condition: true
4678
+ }
4679
+ ];
4680
+ await fs.ensureDir(path.join(projectDir, targetApp));
4681
+ await addEnvVariablesToFile(envPath, variables);
4149
4682
  }
4150
- ];
4151
- await fs.ensureDir(path.join(projectDir, targetApp));
4152
- await addEnvVariablesToFile(envPath, variables);
4153
- }
4154
- if (database === "postgres" && orm === "prisma") {
4155
- const variables = [{
4156
- key: "DATABASE_URL",
4157
- value: "postgresql://username:password@host/database?sslaccept=strict",
4158
- condition: true
4159
- }];
4160
- await fs.ensureDir(path.join(projectDir, targetApp));
4161
- await addEnvVariablesToFile(envPath, variables);
4162
- }
4163
- if (database === "postgres" && orm === "drizzle") {
4164
- const variables = [{
4165
- key: "DATABASE_URL",
4166
- value: "postgresql://username:password@host/database?sslmode=verify-full",
4167
- condition: true
4168
- }];
4169
- await fs.ensureDir(path.join(projectDir, targetApp));
4170
- await addEnvVariablesToFile(envPath, variables);
4171
- }
4172
- if (database === "mysql" && orm === "prisma") {
4173
- const variables = [{
4174
- key: "DATABASE_URL",
4175
- value: "mysql://username:password@host/database?sslaccept=strict",
4176
- condition: true
4177
- }];
4178
- await fs.ensureDir(path.join(projectDir, targetApp));
4179
- await addEnvVariablesToFile(envPath, variables);
4180
- }
4683
+ if (database === "postgres" && orm === "prisma") {
4684
+ const variables = [{
4685
+ key: "DATABASE_URL",
4686
+ value: "postgresql://username:password@host/database?sslaccept=strict",
4687
+ condition: true
4688
+ }];
4689
+ await fs.ensureDir(path.join(projectDir, targetApp));
4690
+ await addEnvVariablesToFile(envPath, variables);
4691
+ }
4692
+ if (database === "postgres" && orm === "drizzle") {
4693
+ const variables = [{
4694
+ key: "DATABASE_URL",
4695
+ value: "postgresql://username:password@host/database?sslmode=verify-full",
4696
+ condition: true
4697
+ }];
4698
+ await fs.ensureDir(path.join(projectDir, targetApp));
4699
+ await addEnvVariablesToFile(envPath, variables);
4700
+ }
4701
+ if (database === "mysql" && orm === "prisma") {
4702
+ const variables = [{
4703
+ key: "DATABASE_URL",
4704
+ value: "mysql://username:password@host/database?sslaccept=strict",
4705
+ condition: true
4706
+ }];
4707
+ await fs.ensureDir(path.join(projectDir, targetApp));
4708
+ await addEnvVariablesToFile(envPath, variables);
4709
+ }
4710
+ },
4711
+ catch: (e) => new DatabaseSetupError({
4712
+ provider: "planetscale",
4713
+ message: `Failed to set up PlanetScale env: ${e instanceof Error ? e.message : String(e)}`,
4714
+ cause: e
4715
+ })
4716
+ });
4181
4717
  }
4182
4718
 
4183
4719
  //#endregion
@@ -4299,8 +4835,8 @@ async function setupPrismaPostgres(config, cliInput) {
4299
4835
  });
4300
4836
  if (ensureDirResult.isErr()) return ensureDirResult;
4301
4837
  if (manualDb) {
4302
- const envResult$1 = await writeEnvFile$1(projectDir, backend);
4303
- if (envResult$1.isErr()) return envResult$1;
4838
+ const envResult = await writeEnvFile$1(projectDir, backend);
4839
+ if (envResult.isErr()) return envResult;
4304
4840
  displayManualSetupInstructions$1(target);
4305
4841
  return Result.ok(void 0);
4306
4842
  }
@@ -4319,8 +4855,8 @@ async function setupPrismaPostgres(config, cliInput) {
4319
4855
  });
4320
4856
  if (isCancel(setupMode)) return userCancelled("Operation cancelled");
4321
4857
  if (setupMode === "manual") {
4322
- const envResult$1 = await writeEnvFile$1(projectDir, backend);
4323
- if (envResult$1.isErr()) return envResult$1;
4858
+ const envResult = await writeEnvFile$1(projectDir, backend);
4859
+ if (envResult.isErr()) return envResult;
4324
4860
  displayManualSetupInstructions$1(target);
4325
4861
  return Result.ok(void 0);
4326
4862
  }
@@ -4328,8 +4864,8 @@ async function setupPrismaPostgres(config, cliInput) {
4328
4864
  if (prismaConfigResult.isErr()) {
4329
4865
  if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
4330
4866
  log.error(pc.red(prismaConfigResult.error.message));
4331
- const envResult$1 = await writeEnvFile$1(projectDir, backend);
4332
- if (envResult$1.isErr()) return envResult$1;
4867
+ const envResult = await writeEnvFile$1(projectDir, backend);
4868
+ if (envResult.isErr()) return envResult;
4333
4869
  displayManualSetupInstructions$1(target);
4334
4870
  log.info("Setup completed with manual configuration required.");
4335
4871
  return Result.ok(void 0);
@@ -4398,9 +4934,9 @@ async function startSupabase(serverDir, packageManager) {
4398
4934
  const subprocess = execa(supabaseStartArgs[0], supabaseStartArgs.slice(1), { cwd: serverDir });
4399
4935
  let stdoutData = "";
4400
4936
  if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
4401
- const text$1 = data.toString();
4402
- process.stdout.write(text$1);
4403
- stdoutData += text$1;
4937
+ const text = data.toString();
4938
+ process.stdout.write(text);
4939
+ stdoutData += text;
4404
4940
  });
4405
4941
  if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
4406
4942
  await subprocess;
@@ -4587,9 +5123,9 @@ async function selectTursoGroup() {
4587
5123
  }
4588
5124
  const selectedGroup = await select({
4589
5125
  message: "Select a Turso database group:",
4590
- options: groups.map((group$1) => ({
4591
- value: group$1.name,
4592
- label: `${group$1.name} (${group$1.locations})`
5126
+ options: groups.map((group) => ({
5127
+ value: group.name,
5128
+ label: `${group.name} (${group.locations})`
4593
5129
  }))
4594
5130
  });
4595
5131
  if (isCancel(selectedGroup)) return userCancelled("Operation cancelled");
@@ -4767,8 +5303,8 @@ async function setupTurso(config, cliInput) {
4767
5303
  continue;
4768
5304
  }
4769
5305
  log.error(pc.red(createResult.error.message));
4770
- const envResult$1 = await writeEnvFile(projectDir, backend);
4771
- if (envResult$1.isErr()) return envResult$1;
5306
+ const envResult = await writeEnvFile(projectDir, backend);
5307
+ if (envResult.isErr()) return envResult;
4772
5308
  displayManualSetupInstructions();
4773
5309
  log.success("Setup completed with manual configuration required.");
4774
5310
  return Result.ok(void 0);
@@ -4793,23 +5329,23 @@ async function setupDatabase(config, cliInput) {
4793
5329
  }
4794
5330
  const dbPackageDir = path.join(projectDir, "packages/db");
4795
5331
  if (!await fs.pathExists(dbPackageDir)) return;
4796
- async function runSetup$1(setupFn) {
5332
+ async function runSetup(setupFn) {
4797
5333
  const result = await setupFn();
4798
5334
  if (result.isErr()) {
4799
5335
  if (UserCancelledError.is(result.error)) throw result.error;
4800
5336
  consola.error(pc.red(result.error.message));
4801
5337
  }
4802
5338
  }
4803
- if (dbSetup === "docker") await runSetup$1(() => setupDockerCompose(config));
4804
- else if (database === "sqlite" && dbSetup === "turso") await runSetup$1(() => setupTurso(config, cliInput));
4805
- else if (database === "sqlite" && dbSetup === "d1") await setupCloudflareD1(config);
5339
+ if (dbSetup === "docker") await runSetup(() => setupDockerCompose(config));
5340
+ else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, cliInput));
5341
+ else if (database === "sqlite" && dbSetup === "d1") await runSetup(() => setupCloudflareD1(config));
4806
5342
  else if (database === "postgres") {
4807
- if (dbSetup === "prisma-postgres") await runSetup$1(() => setupPrismaPostgres(config, cliInput));
4808
- else if (dbSetup === "neon") await runSetup$1(() => setupNeonPostgres(config, cliInput));
4809
- else if (dbSetup === "planetscale") await setupPlanetScale(config);
4810
- else if (dbSetup === "supabase") await runSetup$1(() => setupSupabase(config, cliInput));
4811
- } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
4812
- else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup$1(() => setupMongoDBAtlas(config, cliInput));
5343
+ if (dbSetup === "prisma-postgres") await runSetup(() => setupPrismaPostgres(config, cliInput));
5344
+ else if (dbSetup === "neon") await runSetup(() => setupNeonPostgres(config, cliInput));
5345
+ else if (dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5346
+ else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, cliInput));
5347
+ } else if (database === "mysql" && dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5348
+ else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup(() => setupMongoDBAtlas(config, cliInput));
4813
5349
  }
4814
5350
 
4815
5351
  //#endregion
@@ -4930,11 +5466,12 @@ async function displayPostInstallInstructions(config) {
4930
5466
  "solid"
4931
5467
  ].includes(f));
4932
5468
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
4933
- const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
4934
- const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
4935
5469
  const hasReactRouter = frontend?.includes("react-router");
4936
5470
  const hasSvelte = frontend?.includes("svelte");
4937
5471
  const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
5472
+ const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager) : "";
5473
+ const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
5474
+ const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
4938
5475
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
4939
5476
  let stepCounter = 2;
4940
5477
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
@@ -4974,6 +5511,7 @@ async function displayPostInstallInstructions(config) {
4974
5511
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
4975
5512
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
4976
5513
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
5514
+ if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
4977
5515
  if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
4978
5516
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
4979
5517
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -5057,6 +5595,10 @@ function getBunWebNativeWarning() {
5057
5595
  function getClerkInstructions() {
5058
5596
  return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
5059
5597
  }
5598
+ function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager) {
5599
+ const cmd = packageManager === "npm" ? "npx" : packageManager;
5600
+ return `${pc.bold("Better Auth + Convex Setup:")}\n${pc.cyan("•")} Set environment variables from ${pc.white("packages/backend")}:\n${pc.white(" cd packages/backend")}\n${pc.white(` ${cmd} convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)`)}\n` + (hasWeb ? `${pc.white(` ${cmd} convex env set SITE_URL http://localhost:${webPort}`)}\n` : "");
5601
+ }
5060
5602
  function getPolarInstructions(backend) {
5061
5603
  const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
5062
5604
  return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in ${envPath}`;
@@ -5064,9 +5606,9 @@ function getPolarInstructions(backend) {
5064
5606
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
5065
5607
  const instructions = [];
5066
5608
  const isBackendSelf = backend === "self";
5067
- if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare") instructions.push(`${pc.bold("Deploy web with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} alchemy dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`);
5068
- else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`);
5069
- else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Alchemy:")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
5609
+ if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare") instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} alchemy dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`);
5610
+ else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`);
5611
+ else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
5070
5612
  return instructions.length ? `\n${instructions.join("\n")}` : "";
5071
5613
  }
5072
5614
 
@@ -5138,7 +5680,7 @@ async function setPackageManagerVersion(projectDir, packageManager) {
5138
5680
  if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
5139
5681
  const versionResult = await Result.tryPromise({
5140
5682
  try: async () => {
5141
- const { stdout } = await $`${packageManager} -v`;
5683
+ const { stdout } = await $({ cwd: os$1.tmpdir() })`${packageManager} -v`;
5142
5684
  return stdout.trim();
5143
5685
  },
5144
5686
  catch: () => null
@@ -5219,7 +5761,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
5219
5761
  if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
5220
5762
  if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
5221
5763
  let currentPathInput;
5222
- if (input.yes && input.projectName) currentPathInput = input.projectName;
5764
+ if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
5765
+ else if (input.yes && input.projectName) currentPathInput = input.projectName;
5223
5766
  else if (input.yes) {
5224
5767
  const defaultConfig = getDefaultConfig();
5225
5768
  let defaultName = defaultConfig.relativePath;
@@ -5349,6 +5892,23 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
5349
5892
  });
5350
5893
  });
5351
5894
  }
5895
+ function isPathWithinCwd(targetPath) {
5896
+ const resolved = path.resolve(targetPath);
5897
+ const rel = path.relative(process.cwd(), resolved);
5898
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
5899
+ }
5900
+ async function resolveProjectNameForSilent(input) {
5901
+ const defaultConfig = getDefaultConfig();
5902
+ const candidate = (input.projectName?.trim() || void 0) ?? defaultConfig.relativePath;
5903
+ if (candidate === ".") return Result.ok(candidate);
5904
+ const validationResult = validateProjectName(path.basename(candidate));
5905
+ if (validationResult.isErr()) return Result.err(new CLIError({
5906
+ message: validationResult.error.message,
5907
+ cause: validationResult.error
5908
+ }));
5909
+ if (!isPathWithinCwd(candidate)) return Result.err(new CLIError({ message: "Project path must be within current directory" }));
5910
+ return Result.ok(candidate);
5911
+ }
5352
5912
  async function handleDirectoryConflictResult(currentPathInput, strategy) {
5353
5913
  if (strategy) return handleDirectoryConflictProgrammatically(currentPathInput, strategy);
5354
5914
  return Result.tryPromise({
@@ -5400,121 +5960,8 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5400
5960
  }
5401
5961
  }
5402
5962
 
5403
- //#endregion
5404
- //#region src/utils/open-url.ts
5405
- async function openUrl(url) {
5406
- const platform = process.platform;
5407
- if ((await Result.tryPromise({
5408
- try: async () => {
5409
- if (platform === "darwin") await $({ stdio: "ignore" })`open ${url}`;
5410
- else if (platform === "win32") {
5411
- const escapedUrl = url.replace(/&/g, "^&");
5412
- await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
5413
- } else await $({ stdio: "ignore" })`xdg-open ${url}`;
5414
- },
5415
- catch: () => void 0
5416
- })).isErr()) log.message(`Please open ${url} in your browser.`);
5417
- }
5418
-
5419
- //#endregion
5420
- //#region src/utils/sponsors.ts
5421
- const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
5422
- async function fetchSponsors(url = SPONSORS_JSON_URL) {
5423
- const s = spinner();
5424
- s.start("Fetching sponsors…");
5425
- const response = await fetch(url);
5426
- if (!response.ok) {
5427
- s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
5428
- throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
5429
- }
5430
- const sponsors$1 = await response.json();
5431
- s.stop("Sponsors fetched successfully!");
5432
- return sponsors$1;
5433
- }
5434
- function displaySponsors(sponsors$1) {
5435
- const { total_sponsors } = sponsors$1.summary;
5436
- if (total_sponsors === 0) {
5437
- log.info("No sponsors found. You can be the first one! ✨");
5438
- outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
5439
- return;
5440
- }
5441
- displaySponsorsBox(sponsors$1);
5442
- if (total_sponsors - sponsors$1.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors$1.specialSponsors.length} more amazing sponsors.\n`));
5443
- outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
5444
- }
5445
- function displaySponsorsBox(sponsors$1) {
5446
- if (sponsors$1.specialSponsors.length === 0) return;
5447
- let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
5448
- sponsors$1.specialSponsors.forEach((sponsor, idx) => {
5449
- const displayName = sponsor.name ?? sponsor.githubId;
5450
- const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
5451
- output += `${pc.green(`• ${displayName}`)}${tier}\n`;
5452
- output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
5453
- const website = sponsor.websiteUrl ?? sponsor.githubUrl;
5454
- if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
5455
- if (idx < sponsors$1.specialSponsors.length - 1) output += "\n";
5456
- });
5457
- consola$1.box(output);
5458
- }
5459
-
5460
5963
  //#endregion
5461
5964
  //#region src/index.ts
5462
- function formatStackSummary(entry) {
5463
- const parts = [];
5464
- if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(entry.stack.frontend.join(", "));
5465
- if (entry.stack.backend && entry.stack.backend !== "none") parts.push(entry.stack.backend);
5466
- if (entry.stack.database && entry.stack.database !== "none") parts.push(entry.stack.database);
5467
- if (entry.stack.orm && entry.stack.orm !== "none") parts.push(entry.stack.orm);
5468
- return parts.length > 0 ? parts.join(" + ") : "minimal";
5469
- }
5470
- function formatDate(isoString) {
5471
- return new Date(isoString).toLocaleDateString("en-US", {
5472
- year: "numeric",
5473
- month: "short",
5474
- day: "numeric",
5475
- hour: "2-digit",
5476
- minute: "2-digit"
5477
- });
5478
- }
5479
- async function historyHandler(input) {
5480
- if (input.clear) {
5481
- const clearResult = await clearHistory();
5482
- if (clearResult.isErr()) {
5483
- log.warn(pc.yellow(clearResult.error.message));
5484
- return;
5485
- }
5486
- log.success(pc.green("Project history cleared."));
5487
- return;
5488
- }
5489
- const historyResult = await getHistory(input.limit);
5490
- if (historyResult.isErr()) {
5491
- log.warn(pc.yellow(historyResult.error.message));
5492
- return;
5493
- }
5494
- const entries = historyResult.value;
5495
- if (entries.length === 0) {
5496
- log.info(pc.dim("No projects in history yet."));
5497
- log.info(pc.dim("Create a project with: create-better-t-stack my-app"));
5498
- return;
5499
- }
5500
- if (input.json) {
5501
- console.log(JSON.stringify(entries, null, 2));
5502
- return;
5503
- }
5504
- renderTitle();
5505
- intro(pc.magenta(`Project History (${entries.length} entries)`));
5506
- for (const [index, entry] of entries.entries()) {
5507
- const num = pc.dim(`${index + 1}.`);
5508
- const name = pc.cyan(pc.bold(entry.projectName));
5509
- const stack = pc.dim(formatStackSummary(entry));
5510
- log.message(`${num} ${name}`);
5511
- log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
5512
- log.message(` ${pc.dim("Path:")} ${entry.projectDir}`);
5513
- log.message(` ${pc.dim("Stack:")} ${stack}`);
5514
- log.message(` ${pc.dim("Command:")} ${pc.dim(entry.reproducibleCommand)}`);
5515
- log.message("");
5516
- }
5517
- }
5518
5965
  const router = os.router({
5519
5966
  create: os.meta({
5520
5967
  description: "Create a new Better-T-Stack project",
@@ -5553,39 +6000,9 @@ const router = os.router({
5553
6000
  });
5554
6001
  if (options.verbose) return result;
5555
6002
  }),
5556
- sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(async () => {
5557
- const result = await Result.tryPromise({
5558
- try: async () => {
5559
- renderTitle();
5560
- intro(pc.magenta("Better-T-Stack Sponsors"));
5561
- displaySponsors(await fetchSponsors());
5562
- },
5563
- catch: (e) => new CLIError({
5564
- message: e instanceof Error ? e.message : "Failed to display sponsors",
5565
- cause: e
5566
- })
5567
- });
5568
- if (result.isErr()) {
5569
- displayError(result.error);
5570
- process.exit(1);
5571
- }
5572
- }),
5573
- docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(async () => {
5574
- const DOCS_URL = "https://better-t-stack.dev/docs";
5575
- if ((await Result.tryPromise({
5576
- try: () => openUrl(DOCS_URL),
5577
- catch: () => null
5578
- })).isOk()) log.success(pc.blue("Opened docs in your default browser."));
5579
- else log.message(`Please visit ${DOCS_URL}`);
5580
- }),
5581
- builder: os.meta({ description: "Open the web-based stack builder" }).handler(async () => {
5582
- const BUILDER_URL = "https://better-t-stack.dev/new";
5583
- if ((await Result.tryPromise({
5584
- try: () => openUrl(BUILDER_URL),
5585
- catch: () => null
5586
- })).isOk()) log.success(pc.blue("Opened builder in your default browser."));
5587
- else log.message(`Please visit ${BUILDER_URL}`);
5588
- }),
6003
+ sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(showSponsorsCommand),
6004
+ docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(openDocsCommand),
6005
+ builder: os.meta({ description: "Open the web-based stack builder" }).handler(openBuilderCommand),
5589
6006
  add: os.meta({ description: "Add addons to an existing Better-T-Stack project" }).input(z.object({
5590
6007
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
5591
6008
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),