create-better-t-stack 3.19.4 → 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
2
  import { t as __reExport } from "./chunk-CHc3S52W.mjs";
3
- import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
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
  /**
@@ -177,30 +159,368 @@ var AddonSetupError = class extends TaggedError("AddonSetupError")() {
177
159
  constructor(args) {
178
160
  super(args);
179
161
  }
180
- };
181
- /**
182
- * Create a user cancelled error Result
183
- */
184
- function userCancelled(message) {
185
- return Result.err(new UserCancelledError({ message }));
162
+ };
163
+ /**
164
+ * Create a user cancelled error Result
165
+ */
166
+ function userCancelled(message) {
167
+ return Result.err(new UserCancelledError({ message }));
168
+ }
169
+ /**
170
+ * Create a database setup error Result
171
+ */
172
+ function databaseSetupError(provider, message, cause) {
173
+ return Result.err(new DatabaseSetupError({
174
+ provider,
175
+ message,
176
+ cause
177
+ }));
178
+ }
179
+ /**
180
+ * Display an error to the user (for CLI mode)
181
+ */
182
+ function displayError(error) {
183
+ if (UserCancelledError.is(error)) cancel(pc.red(error.message));
184
+ else consola.error(pc.red(error.message));
185
+ }
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."));
186
455
  }
187
- /**
188
- * Create a database setup error Result
189
- */
190
- function databaseSetupError(provider, message, cause) {
191
- return Result.err(new DatabaseSetupError({
192
- provider,
193
- message,
194
- cause
195
- }));
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);
196
469
  }
197
- /**
198
- * Display an error to the user (for CLI mode)
199
- */
200
- function displayError(error) {
201
- if (UserCancelledError.is(error)) cancel(pc.red(error.message));
202
- else consola.error(pc.red(error.message));
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
+ }
203
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
+ ];
204
524
 
205
525
  //#endregion
206
526
  //#region src/utils/compatibility-rules.ts
@@ -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;
@@ -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) => {
@@ -1102,6 +1393,246 @@ async function setupFumadocs(config) {
1102
1393
  return Result.ok(void 0);
1103
1394
  }
1104
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)))
1588
+ });
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);
1596
+ }
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");
1633
+ return Result.ok(void 0);
1634
+ }
1635
+
1105
1636
  //#endregion
1106
1637
  //#region src/helpers/addons/oxlint-setup.ts
1107
1638
  async function setupOxlint(projectDir, packageManager) {
@@ -1250,58 +1781,20 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
1250
1781
  //#endregion
1251
1782
  //#region src/helpers/addons/skills-setup.ts
1252
1783
  const SKILL_SOURCES = {
1253
- "vercel-labs/agent-skills": {
1254
- source: "vercel-labs/agent-skills",
1255
- label: "Vercel Agent Skills"
1256
- },
1257
- "vercel/ai": {
1258
- source: "vercel/ai",
1259
- label: "Vercel AI SDK"
1260
- },
1261
- "vercel/turborepo": {
1262
- source: "vercel/turborepo",
1263
- label: "Turborepo"
1264
- },
1265
- "yusukebe/hono-skill": {
1266
- source: "yusukebe/hono-skill",
1267
- label: "Hono Backend"
1268
- },
1269
- "vercel-labs/next-skills": {
1270
- source: "vercel-labs/next-skills",
1271
- label: "Next.js Best Practices"
1272
- },
1273
- "heroui-inc/heroui": {
1274
- source: "heroui-inc/heroui",
1275
- label: "HeroUI Native"
1276
- },
1277
- "better-auth/skills": {
1278
- source: "better-auth/skills",
1279
- label: "Better Auth"
1280
- },
1281
- "neondatabase/agent-skills": {
1282
- source: "neondatabase/agent-skills",
1283
- label: "Neon Database"
1284
- },
1285
- "supabase/agent-skills": {
1286
- source: "supabase/agent-skills",
1287
- label: "Supabase"
1288
- },
1289
- "expo/skills": {
1290
- source: "expo/skills",
1291
- label: "Expo"
1292
- },
1293
- "prisma/skills": {
1294
- source: "prisma/skills",
1295
- label: "Prisma"
1296
- },
1297
- "elysiajs/skills": {
1298
- source: "elysiajs/skills",
1299
- label: "ElysiaJS"
1300
- },
1301
- "waynesutton/convexskills": {
1302
- source: "waynesutton/convexskills",
1303
- label: "Convex"
1304
- }
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" }
1305
1798
  };
1306
1799
  const AVAILABLE_AGENTS = [
1307
1800
  {
@@ -1405,19 +1898,24 @@ const AVAILABLE_AGENTS = [
1405
1898
  label: "MCPJam"
1406
1899
  }
1407
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
+ }
1408
1907
  function getRecommendedSourceKeys(config) {
1409
1908
  const sources = [];
1410
1909
  const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
1411
- const hasReactBasedFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
1412
- const hasNativeFrontend = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
1413
- if (hasReactBasedFrontend) sources.push("vercel-labs/agent-skills");
1910
+ if (hasReactBasedFrontend(frontend)) sources.push("vercel-labs/agent-skills");
1414
1911
  if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
1912
+ if (frontend.includes("nuxt")) sources.push("nuxt/ui");
1415
1913
  if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
1416
- if (hasNativeFrontend) sources.push("expo/skills");
1914
+ if (hasNativeFrontend(frontend)) sources.push("expo/skills");
1417
1915
  if (auth === "better-auth") sources.push("better-auth/skills");
1418
1916
  if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
1419
1917
  if (dbSetup === "supabase") sources.push("supabase/agent-skills");
1420
- if (orm === "prisma") sources.push("prisma/skills");
1918
+ if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
1421
1919
  if (examples.includes("ai")) sources.push("vercel/ai");
1422
1920
  if (addons.includes("turborepo")) sources.push("vercel/turborepo");
1423
1921
  if (backend === "hono") sources.push("yusukebe/hono-skill");
@@ -1425,60 +1923,95 @@ function getRecommendedSourceKeys(config) {
1425
1923
  if (backend === "convex") sources.push("waynesutton/convexskills");
1426
1924
  return sources;
1427
1925
  }
1428
- function parseSkillsFromOutput(output) {
1429
- const skills = [];
1430
- const lines = output.split("\n");
1431
- const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
1432
- for (const line of lines) {
1433
- const match = line.replace(ansiRegex, "").match(/^│\s{4}([a-z][a-z0-9-]*)$/);
1434
- if (match) skills.push(match[1]);
1435
- }
1436
- 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);
1437
1978
  }
1438
1979
  function uniqueValues(values) {
1439
1980
  return Array.from(new Set(values));
1440
1981
  }
1441
- async function fetchSkillsFromSource(source, packageManager, projectDir) {
1442
- try {
1443
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source.source} --list`);
1444
- return parseSkillsFromOutput((await $({
1445
- cwd: projectDir,
1446
- env: { CI: "true" }
1447
- })`${args}`).stdout);
1448
- } catch {
1449
- return [];
1450
- }
1451
- }
1452
1982
  async function setupSkills(config) {
1453
1983
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1454
1984
  const { packageManager, projectDir } = config;
1455
1985
  const btsConfig = await readBtsConfig(projectDir);
1456
- const recommendedSourceKeys = getRecommendedSourceKeys(btsConfig ? {
1986
+ const fullConfig = btsConfig ? {
1457
1987
  ...config,
1458
1988
  addons: btsConfig.addons ?? config.addons
1459
- } : config);
1989
+ } : config;
1990
+ const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
1460
1991
  if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
1461
- const sourceKeys = uniqueValues(recommendedSourceKeys);
1462
- const s = spinner();
1463
- s.start("Fetching available skills...");
1464
- const allSkills = [];
1465
- const sources = sourceKeys.map((sourceKey) => SKILL_SOURCES[sourceKey]).filter((source) => Boolean(source));
1466
- const fetchedSkills = await Promise.all(sources.map(async (source) => ({
1467
- source,
1468
- skills: await fetchSkillsFromSource(source, packageManager, projectDir)
1469
- })));
1470
- for (const { source, skills } of fetchedSkills) for (const skillName of skills) allSkills.push({
1471
- name: skillName,
1472
- source: source.source,
1473
- sourceLabel: source.label
1474
- });
1475
- s.stop("Fetched available skills");
1476
- if (allSkills.length === 0) return Result.ok(void 0);
1477
- const skillOptions = allSkills.map((skill) => ({
1478
- value: `${skill.source}::${skill.name}`,
1479
- label: skill.name,
1480
- hint: skill.sourceLabel
1481
- }));
1992
+ const skillOptions = uniqueValues(recommendedSourceKeys).flatMap((sourceKey) => {
1993
+ const source = SKILL_SOURCES[sourceKey];
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" }));
1482
2015
  const selectedSkills = await multiselect({
1483
2016
  message: "Select skills to install",
1484
2017
  options: skillOptions,
@@ -1508,11 +2041,12 @@ async function setupSkills(config) {
1508
2041
  const installSpinner = spinner();
1509
2042
  installSpinner.start("Installing skills...");
1510
2043
  const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
2044
+ const globalFlag = scope === "global" ? "-g" : "";
1511
2045
  for (const [source, skills] of Object.entries(skillsBySource)) {
1512
2046
  const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
1513
2047
  if ((await Result.tryPromise({
1514
2048
  try: async () => {
1515
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${skillFlags} ${agentFlags} -y`);
2049
+ const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
1516
2050
  await $({
1517
2051
  cwd: projectDir,
1518
2052
  env: { CI: "true" }
@@ -2012,6 +2546,7 @@ async function setupAddons(config) {
2012
2546
  if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
2013
2547
  if (addons.includes("ruler")) await runSetup(() => setupRuler(config));
2014
2548
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
2549
+ if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
2015
2550
  }
2016
2551
  async function setupBiome(projectDir) {
2017
2552
  await addPackageDependency({
@@ -3026,37 +3561,19 @@ async function getProjectName(initialName) {
3026
3561
  initialValue: initialName,
3027
3562
  defaultValue: defaultName,
3028
3563
  validate: (value) => {
3029
- const nameToUse = String(value ?? "").trim() || defaultName;
3030
- const validationError = validateDirectoryName(path.basename(nameToUse));
3031
- if (validationError) return validationError;
3032
- if (nameToUse !== ".") {
3033
- if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
3034
- }
3035
- }
3036
- });
3037
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
3038
- projectPath = response || defaultName;
3039
- isValid = true;
3040
- }
3041
- return projectPath;
3042
- }
3043
-
3044
- //#endregion
3045
- //#region src/utils/get-latest-cli-version.ts
3046
- function getLatestCLIVersionResult() {
3047
- const packageJsonPath = path.join(PKG_ROOT, "package.json");
3048
- return Result.try({
3049
- try: () => {
3050
- return fs.readJSONSync(packageJsonPath).version;
3051
- },
3052
- catch: (e) => new CLIError({
3053
- message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
3054
- cause: e
3055
- })
3056
- });
3057
- }
3058
- function getLatestCLIVersion() {
3059
- return getLatestCLIVersionResult().unwrapOr("1.0.0");
3564
+ const nameToUse = String(value ?? "").trim() || defaultName;
3565
+ const validationError = validateDirectoryName(path.basename(nameToUse));
3566
+ if (validationError) return validationError;
3567
+ if (nameToUse !== ".") {
3568
+ if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
3569
+ }
3570
+ }
3571
+ });
3572
+ if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
3573
+ projectPath = response || defaultName;
3574
+ isValid = true;
3575
+ }
3576
+ return projectPath;
3060
3577
  }
3061
3578
 
3062
3579
  //#endregion
@@ -3069,7 +3586,7 @@ function getLatestCLIVersion() {
3069
3586
  */
3070
3587
  function isTelemetryEnabled() {
3071
3588
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
3072
- const BTS_TELEMETRY = "1";
3589
+ const BTS_TELEMETRY = "0";
3073
3590
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
3074
3591
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
3075
3592
  return true;
@@ -3077,17 +3594,7 @@ function isTelemetryEnabled() {
3077
3594
 
3078
3595
  //#endregion
3079
3596
  //#region src/utils/analytics.ts
3080
- const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
3081
- async function sendConvexEvent(payload) {
3082
- await Result.tryPromise({
3083
- try: () => fetch(CONVEX_INGEST_URL, {
3084
- method: "POST",
3085
- headers: { "Content-Type": "application/json" },
3086
- body: JSON.stringify(payload)
3087
- }),
3088
- catch: () => void 0
3089
- });
3090
- }
3597
+ async function sendConvexEvent(payload) {}
3091
3598
  async function trackProjectCreation(config, disableAnalytics = false) {
3092
3599
  if (!isTelemetryEnabled() || disableAnalytics) return;
3093
3600
  const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
@@ -3233,119 +3740,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
3233
3740
  };
3234
3741
  }
3235
3742
 
3236
- //#endregion
3237
- //#region src/utils/project-history.ts
3238
- const paths = envPaths("better-t-stack", { suffix: "" });
3239
- const HISTORY_FILE = "history.json";
3240
- var HistoryError = class extends TaggedError("HistoryError")() {};
3241
- function getHistoryDir() {
3242
- return paths.data;
3243
- }
3244
- function getHistoryPath() {
3245
- return path.join(paths.data, HISTORY_FILE);
3246
- }
3247
- function generateId() {
3248
- return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
3249
- }
3250
- function emptyHistory() {
3251
- return {
3252
- version: 1,
3253
- entries: []
3254
- };
3255
- }
3256
- async function ensureHistoryDir() {
3257
- return Result.tryPromise({
3258
- try: async () => {
3259
- await fs.ensureDir(getHistoryDir());
3260
- },
3261
- catch: (e) => new HistoryError({
3262
- message: `Failed to create history directory: ${e instanceof Error ? e.message : String(e)}`,
3263
- cause: e
3264
- })
3265
- });
3266
- }
3267
- async function readHistory() {
3268
- const historyPath = getHistoryPath();
3269
- const existsResult = await Result.tryPromise({
3270
- try: async () => await fs.pathExists(historyPath),
3271
- catch: (e) => new HistoryError({
3272
- message: `Failed to check history file: ${e instanceof Error ? e.message : String(e)}`,
3273
- cause: e
3274
- })
3275
- });
3276
- if (existsResult.isErr()) return existsResult;
3277
- if (!existsResult.value) return Result.ok(emptyHistory());
3278
- const readResult = await Result.tryPromise({
3279
- try: async () => await fs.readJson(historyPath),
3280
- catch: (e) => new HistoryError({
3281
- message: `Failed to read history file: ${e instanceof Error ? e.message : String(e)}`,
3282
- cause: e
3283
- })
3284
- });
3285
- if (readResult.isErr()) return Result.ok(emptyHistory());
3286
- return Result.ok(readResult.value);
3287
- }
3288
- async function writeHistory(history) {
3289
- const ensureDirResult = await ensureHistoryDir();
3290
- if (ensureDirResult.isErr()) return ensureDirResult;
3291
- return Result.tryPromise({
3292
- try: async () => {
3293
- await fs.writeJson(getHistoryPath(), history, { spaces: 2 });
3294
- },
3295
- catch: (e) => new HistoryError({
3296
- message: `Failed to write history file: ${e instanceof Error ? e.message : String(e)}`,
3297
- cause: e
3298
- })
3299
- });
3300
- }
3301
- async function addToHistory(config, reproducibleCommand) {
3302
- const historyResult = await readHistory();
3303
- if (historyResult.isErr()) return historyResult;
3304
- const history = historyResult.value;
3305
- const entry = {
3306
- id: generateId(),
3307
- projectName: config.projectName,
3308
- projectDir: config.projectDir,
3309
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3310
- stack: {
3311
- frontend: config.frontend,
3312
- backend: config.backend,
3313
- database: config.database,
3314
- orm: config.orm,
3315
- runtime: config.runtime,
3316
- auth: config.auth,
3317
- payments: config.payments,
3318
- api: config.api,
3319
- addons: config.addons,
3320
- examples: config.examples,
3321
- dbSetup: config.dbSetup,
3322
- packageManager: config.packageManager
3323
- },
3324
- cliVersion: getLatestCLIVersion(),
3325
- reproducibleCommand
3326
- };
3327
- history.entries.unshift(entry);
3328
- if (history.entries.length > 100) history.entries = history.entries.slice(0, 100);
3329
- return await writeHistory(history);
3330
- }
3331
- async function getHistory(limit = 10) {
3332
- const historyResult = await readHistory();
3333
- if (historyResult.isErr()) return historyResult;
3334
- return Result.ok(historyResult.value.entries.slice(0, limit));
3335
- }
3336
- async function clearHistory() {
3337
- const historyPath = getHistoryPath();
3338
- return Result.tryPromise({
3339
- try: async () => {
3340
- if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
3341
- },
3342
- catch: (e) => new HistoryError({
3343
- message: `Failed to clear history: ${e instanceof Error ? e.message : String(e)}`,
3344
- cause: e
3345
- })
3346
- });
3347
- }
3348
-
3349
3743
  //#endregion
3350
3744
  //#region src/utils/project-name-validation.ts
3351
3745
  function validateProjectName(name) {
@@ -5072,11 +5466,12 @@ async function displayPostInstallInstructions(config) {
5072
5466
  "solid"
5073
5467
  ].includes(f));
5074
5468
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
5075
- const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
5076
- const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
5077
5469
  const hasReactRouter = frontend?.includes("react-router");
5078
5470
  const hasSvelte = frontend?.includes("svelte");
5079
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() : "";
5080
5475
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
5081
5476
  let stepCounter = 2;
5082
5477
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
@@ -5116,6 +5511,7 @@ async function displayPostInstallInstructions(config) {
5116
5511
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
5117
5512
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
5118
5513
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
5514
+ if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
5119
5515
  if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
5120
5516
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
5121
5517
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -5199,6 +5595,10 @@ function getBunWebNativeWarning() {
5199
5595
  function getClerkInstructions() {
5200
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`;
5201
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
+ }
5202
5602
  function getPolarInstructions(backend) {
5203
5603
  const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
5204
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}`;
@@ -5280,7 +5680,7 @@ async function setPackageManagerVersion(projectDir, packageManager) {
5280
5680
  if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
5281
5681
  const versionResult = await Result.tryPromise({
5282
5682
  try: async () => {
5283
- const { stdout } = await $`${packageManager} -v`;
5683
+ const { stdout } = await $({ cwd: os$1.tmpdir() })`${packageManager} -v`;
5284
5684
  return stdout.trim();
5285
5685
  },
5286
5686
  catch: () => null
@@ -5560,121 +5960,8 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5560
5960
  }
5561
5961
  }
5562
5962
 
5563
- //#endregion
5564
- //#region src/utils/open-url.ts
5565
- async function openUrl(url) {
5566
- const platform = process.platform;
5567
- if ((await Result.tryPromise({
5568
- try: async () => {
5569
- if (platform === "darwin") await $({ stdio: "ignore" })`open ${url}`;
5570
- else if (platform === "win32") {
5571
- const escapedUrl = url.replace(/&/g, "^&");
5572
- await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
5573
- } else await $({ stdio: "ignore" })`xdg-open ${url}`;
5574
- },
5575
- catch: () => void 0
5576
- })).isErr()) log.message(`Please open ${url} in your browser.`);
5577
- }
5578
-
5579
- //#endregion
5580
- //#region src/utils/sponsors.ts
5581
- const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
5582
- async function fetchSponsors(url = SPONSORS_JSON_URL) {
5583
- const s = spinner();
5584
- s.start("Fetching sponsors…");
5585
- const response = await fetch(url);
5586
- if (!response.ok) {
5587
- s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
5588
- throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
5589
- }
5590
- const sponsors = await response.json();
5591
- s.stop("Sponsors fetched successfully!");
5592
- return sponsors;
5593
- }
5594
- function displaySponsors(sponsors) {
5595
- const { total_sponsors } = sponsors.summary;
5596
- if (total_sponsors === 0) {
5597
- log.info("No sponsors found. You can be the first one! ✨");
5598
- outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
5599
- return;
5600
- }
5601
- displaySponsorsBox(sponsors);
5602
- if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
5603
- outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
5604
- }
5605
- function displaySponsorsBox(sponsors) {
5606
- if (sponsors.specialSponsors.length === 0) return;
5607
- let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
5608
- sponsors.specialSponsors.forEach((sponsor, idx) => {
5609
- const displayName = sponsor.name ?? sponsor.githubId;
5610
- const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
5611
- output += `${pc.green(`• ${displayName}`)}${tier}\n`;
5612
- output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
5613
- const website = sponsor.websiteUrl ?? sponsor.githubUrl;
5614
- if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
5615
- if (idx < sponsors.specialSponsors.length - 1) output += "\n";
5616
- });
5617
- consola$1.box(output);
5618
- }
5619
-
5620
5963
  //#endregion
5621
5964
  //#region src/index.ts
5622
- function formatStackSummary(entry) {
5623
- const parts = [];
5624
- if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(entry.stack.frontend.join(", "));
5625
- if (entry.stack.backend && entry.stack.backend !== "none") parts.push(entry.stack.backend);
5626
- if (entry.stack.database && entry.stack.database !== "none") parts.push(entry.stack.database);
5627
- if (entry.stack.orm && entry.stack.orm !== "none") parts.push(entry.stack.orm);
5628
- return parts.length > 0 ? parts.join(" + ") : "minimal";
5629
- }
5630
- function formatDate(isoString) {
5631
- return new Date(isoString).toLocaleDateString("en-US", {
5632
- year: "numeric",
5633
- month: "short",
5634
- day: "numeric",
5635
- hour: "2-digit",
5636
- minute: "2-digit"
5637
- });
5638
- }
5639
- async function historyHandler(input) {
5640
- if (input.clear) {
5641
- const clearResult = await clearHistory();
5642
- if (clearResult.isErr()) {
5643
- log.warn(pc.yellow(clearResult.error.message));
5644
- return;
5645
- }
5646
- log.success(pc.green("Project history cleared."));
5647
- return;
5648
- }
5649
- const historyResult = await getHistory(input.limit);
5650
- if (historyResult.isErr()) {
5651
- log.warn(pc.yellow(historyResult.error.message));
5652
- return;
5653
- }
5654
- const entries = historyResult.value;
5655
- if (entries.length === 0) {
5656
- log.info(pc.dim("No projects in history yet."));
5657
- log.info(pc.dim("Create a project with: create-better-t-stack my-app"));
5658
- return;
5659
- }
5660
- if (input.json) {
5661
- console.log(JSON.stringify(entries, null, 2));
5662
- return;
5663
- }
5664
- renderTitle();
5665
- intro(pc.magenta(`Project History (${entries.length} entries)`));
5666
- for (const [index, entry] of entries.entries()) {
5667
- const num = pc.dim(`${index + 1}.`);
5668
- const name = pc.cyan(pc.bold(entry.projectName));
5669
- const stack = pc.dim(formatStackSummary(entry));
5670
- log.message(`${num} ${name}`);
5671
- log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
5672
- log.message(` ${pc.dim("Path:")} ${entry.projectDir}`);
5673
- log.message(` ${pc.dim("Stack:")} ${stack}`);
5674
- log.message(` ${pc.dim("Command:")} ${pc.dim(entry.reproducibleCommand)}`);
5675
- log.message("");
5676
- }
5677
- }
5678
5965
  const router = os.router({
5679
5966
  create: os.meta({
5680
5967
  description: "Create a new Better-T-Stack project",
@@ -5713,39 +6000,9 @@ const router = os.router({
5713
6000
  });
5714
6001
  if (options.verbose) return result;
5715
6002
  }),
5716
- sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(async () => {
5717
- const result = await Result.tryPromise({
5718
- try: async () => {
5719
- renderTitle();
5720
- intro(pc.magenta("Better-T-Stack Sponsors"));
5721
- displaySponsors(await fetchSponsors());
5722
- },
5723
- catch: (e) => new CLIError({
5724
- message: e instanceof Error ? e.message : "Failed to display sponsors",
5725
- cause: e
5726
- })
5727
- });
5728
- if (result.isErr()) {
5729
- displayError(result.error);
5730
- process.exit(1);
5731
- }
5732
- }),
5733
- docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(async () => {
5734
- const DOCS_URL = "https://better-t-stack.dev/docs";
5735
- if ((await Result.tryPromise({
5736
- try: () => openUrl(DOCS_URL),
5737
- catch: () => null
5738
- })).isOk()) log.success(pc.blue("Opened docs in your default browser."));
5739
- else log.message(`Please visit ${DOCS_URL}`);
5740
- }),
5741
- builder: os.meta({ description: "Open the web-based stack builder" }).handler(async () => {
5742
- const BUILDER_URL = "https://better-t-stack.dev/new";
5743
- if ((await Result.tryPromise({
5744
- try: () => openUrl(BUILDER_URL),
5745
- catch: () => null
5746
- })).isOk()) log.success(pc.blue("Opened builder in your default browser."));
5747
- else log.message(`Please visit ${BUILDER_URL}`);
5748
- }),
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),
5749
6006
  add: os.meta({ description: "Add addons to an existing Better-T-Stack project" }).input(z.object({
5750
6007
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
5751
6008
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),