create-better-t-stack 3.19.4 → 3.19.5-pr874.b75f803
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/{src-mMd-eO_p.mjs → src-D_4oe5H9.mjs} +751 -492
- package/package.json +3 -3
|
@@ -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 {
|
|
10
|
-
import
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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: [
|
|
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,248 @@ 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 getPackageRunnerWithYesFlag(packageManager) {
|
|
1452
|
+
const prefix = getPackageRunnerPrefix(packageManager);
|
|
1453
|
+
const runner = prefix[0];
|
|
1454
|
+
if (runner === "bunx") return [...prefix, "-y"];
|
|
1455
|
+
if (runner === "npx") return [...prefix, "-y"];
|
|
1456
|
+
if (runner === "pnpm") return [...prefix, "--yes"];
|
|
1457
|
+
return prefix;
|
|
1458
|
+
}
|
|
1459
|
+
function getRecommendedMcpServers(config) {
|
|
1460
|
+
const servers = [];
|
|
1461
|
+
servers.push({
|
|
1462
|
+
key: "context7",
|
|
1463
|
+
label: "Context7",
|
|
1464
|
+
name: "context7",
|
|
1465
|
+
target: "@upstash/context7-mcp"
|
|
1466
|
+
});
|
|
1467
|
+
if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") servers.push({
|
|
1468
|
+
key: "cloudflare-docs",
|
|
1469
|
+
label: "Cloudflare Docs",
|
|
1470
|
+
name: "cloudflare-docs",
|
|
1471
|
+
target: "https://docs.mcp.cloudflare.com/sse",
|
|
1472
|
+
transport: "sse"
|
|
1473
|
+
});
|
|
1474
|
+
if (config.backend === "convex") servers.push({
|
|
1475
|
+
key: "convex",
|
|
1476
|
+
label: "Convex",
|
|
1477
|
+
name: "convex",
|
|
1478
|
+
target: "npx -y convex@latest mcp start"
|
|
1479
|
+
});
|
|
1480
|
+
if (hasReactBasedFrontend$1(config.frontend)) servers.push({
|
|
1481
|
+
key: "shadcn",
|
|
1482
|
+
label: "shadcn/ui",
|
|
1483
|
+
name: "shadcn",
|
|
1484
|
+
target: "npx -y shadcn@latest mcp"
|
|
1485
|
+
});
|
|
1486
|
+
if (config.frontend.includes("next")) servers.push({
|
|
1487
|
+
key: "next-devtools",
|
|
1488
|
+
label: "Next Devtools",
|
|
1489
|
+
name: "next-devtools",
|
|
1490
|
+
target: "npx -y next-devtools-mcp@latest"
|
|
1491
|
+
});
|
|
1492
|
+
if (config.frontend.includes("nuxt")) servers.push({
|
|
1493
|
+
key: "nuxt-docs",
|
|
1494
|
+
label: "Nuxt Docs",
|
|
1495
|
+
name: "nuxt",
|
|
1496
|
+
target: "https://nuxt.com/mcp"
|
|
1497
|
+
}, {
|
|
1498
|
+
key: "nuxt-ui-docs",
|
|
1499
|
+
label: "Nuxt UI Docs",
|
|
1500
|
+
name: "nuxt-ui",
|
|
1501
|
+
target: "https://ui.nuxt.com/mcp"
|
|
1502
|
+
});
|
|
1503
|
+
if (config.frontend.includes("svelte")) servers.push({
|
|
1504
|
+
key: "svelte-docs",
|
|
1505
|
+
label: "Svelte Docs",
|
|
1506
|
+
name: "svelte",
|
|
1507
|
+
target: "https://mcp.svelte.dev/mcp"
|
|
1508
|
+
});
|
|
1509
|
+
if (config.frontend.includes("astro")) servers.push({
|
|
1510
|
+
key: "astro-docs",
|
|
1511
|
+
label: "Astro Docs",
|
|
1512
|
+
name: "astro-docs",
|
|
1513
|
+
target: "https://mcp.docs.astro.build/mcp"
|
|
1514
|
+
});
|
|
1515
|
+
if (config.dbSetup === "planetscale") servers.push({
|
|
1516
|
+
key: "planetscale",
|
|
1517
|
+
label: "PlanetScale",
|
|
1518
|
+
name: "planetscale",
|
|
1519
|
+
target: "https://mcp.pscale.dev/mcp/planetscale"
|
|
1520
|
+
});
|
|
1521
|
+
if (config.dbSetup === "neon") servers.push({
|
|
1522
|
+
key: "neon",
|
|
1523
|
+
label: "Neon",
|
|
1524
|
+
name: "neon",
|
|
1525
|
+
target: "https://mcp.neon.tech/mcp"
|
|
1526
|
+
});
|
|
1527
|
+
if (config.dbSetup === "supabase") servers.push({
|
|
1528
|
+
key: "supabase",
|
|
1529
|
+
label: "Supabase",
|
|
1530
|
+
name: "supabase",
|
|
1531
|
+
target: "https://mcp.supabase.com/mcp"
|
|
1532
|
+
});
|
|
1533
|
+
if (config.auth === "better-auth") servers.push({
|
|
1534
|
+
key: "better-auth",
|
|
1535
|
+
label: "Better Auth",
|
|
1536
|
+
name: "better-auth",
|
|
1537
|
+
target: "https://mcp.inkeep.com/better-auth/mcp"
|
|
1538
|
+
});
|
|
1539
|
+
return servers;
|
|
1540
|
+
}
|
|
1541
|
+
function filterAgentsForScope(scope) {
|
|
1542
|
+
return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
|
|
1543
|
+
}
|
|
1544
|
+
async function setupMcp(config) {
|
|
1545
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1546
|
+
const { packageManager, projectDir } = config;
|
|
1547
|
+
log.info("Setting up MCP servers...");
|
|
1548
|
+
const scope = await select({
|
|
1549
|
+
message: "Where should MCP servers be installed?",
|
|
1550
|
+
options: [{
|
|
1551
|
+
value: "project",
|
|
1552
|
+
label: "Project",
|
|
1553
|
+
hint: "Writes to project config files (recommended for teams)"
|
|
1554
|
+
}, {
|
|
1555
|
+
value: "global",
|
|
1556
|
+
label: "Global",
|
|
1557
|
+
hint: "Writes to user-level config files (personal machine)"
|
|
1558
|
+
}],
|
|
1559
|
+
initialValue: "project"
|
|
1560
|
+
});
|
|
1561
|
+
if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1562
|
+
const recommendedServers = getRecommendedMcpServers(config);
|
|
1563
|
+
if (recommendedServers.length === 0) return Result.ok(void 0);
|
|
1564
|
+
const serverOptions = recommendedServers.map((s) => ({
|
|
1565
|
+
value: s.key,
|
|
1566
|
+
label: s.label,
|
|
1567
|
+
hint: s.target
|
|
1568
|
+
}));
|
|
1569
|
+
const selectedServerKeys = await multiselect({
|
|
1570
|
+
message: "Select MCP servers to install",
|
|
1571
|
+
options: serverOptions,
|
|
1572
|
+
required: false,
|
|
1573
|
+
initialValues: serverOptions.map((o) => o.value)
|
|
1574
|
+
});
|
|
1575
|
+
if (isCancel(selectedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1576
|
+
if (selectedServerKeys.length === 0) return Result.ok(void 0);
|
|
1577
|
+
const agentOptions = filterAgentsForScope(scope).map((a) => ({
|
|
1578
|
+
value: a.value,
|
|
1579
|
+
label: a.label
|
|
1580
|
+
}));
|
|
1581
|
+
const selectedAgents = await multiselect({
|
|
1582
|
+
message: "Select agents to install MCP servers to",
|
|
1583
|
+
options: agentOptions,
|
|
1584
|
+
required: false,
|
|
1585
|
+
initialValues: uniqueValues$1([
|
|
1586
|
+
"cursor",
|
|
1587
|
+
"claude-code",
|
|
1588
|
+
"vscode"
|
|
1589
|
+
].filter((a) => agentOptions.some((o) => o.value === a)))
|
|
1590
|
+
});
|
|
1591
|
+
if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1592
|
+
if (selectedAgents.length === 0) return Result.ok(void 0);
|
|
1593
|
+
const serversByKey = new Map(recommendedServers.map((s) => [s.key, s]));
|
|
1594
|
+
const selectedServers = [];
|
|
1595
|
+
for (const key of selectedServerKeys) {
|
|
1596
|
+
const server = serversByKey.get(key);
|
|
1597
|
+
if (server) selectedServers.push(server);
|
|
1598
|
+
}
|
|
1599
|
+
if (selectedServers.length === 0) return Result.ok(void 0);
|
|
1600
|
+
const installSpinner = spinner();
|
|
1601
|
+
installSpinner.start("Installing MCP servers...");
|
|
1602
|
+
const runner = getPackageRunnerWithYesFlag(packageManager);
|
|
1603
|
+
const globalFlags = scope === "global" ? ["-g"] : [];
|
|
1604
|
+
for (const server of selectedServers) {
|
|
1605
|
+
const transportFlags = server.transport ? ["-t", server.transport] : [];
|
|
1606
|
+
const headerFlags = (server.headers ?? []).flatMap((h) => ["--header", h]);
|
|
1607
|
+
const agentFlags = selectedAgents.flatMap((a) => ["-a", a]);
|
|
1608
|
+
const args = [
|
|
1609
|
+
...runner,
|
|
1610
|
+
"add-mcp@latest",
|
|
1611
|
+
server.target,
|
|
1612
|
+
"--name",
|
|
1613
|
+
server.name,
|
|
1614
|
+
...transportFlags,
|
|
1615
|
+
...headerFlags,
|
|
1616
|
+
...agentFlags,
|
|
1617
|
+
...globalFlags,
|
|
1618
|
+
"-y"
|
|
1619
|
+
];
|
|
1620
|
+
if ((await Result.tryPromise({
|
|
1621
|
+
try: async () => {
|
|
1622
|
+
await $({
|
|
1623
|
+
cwd: projectDir,
|
|
1624
|
+
env: { CI: "true" }
|
|
1625
|
+
})`${args}`;
|
|
1626
|
+
},
|
|
1627
|
+
catch: (e) => new AddonSetupError({
|
|
1628
|
+
addon: "mcp",
|
|
1629
|
+
message: `Failed to install MCP server '${server.name}': ${e instanceof Error ? e.message : String(e)}`,
|
|
1630
|
+
cause: e
|
|
1631
|
+
})
|
|
1632
|
+
})).isErr()) log.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
|
|
1633
|
+
}
|
|
1634
|
+
installSpinner.stop("MCP servers installed");
|
|
1635
|
+
return Result.ok(void 0);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1105
1638
|
//#endregion
|
|
1106
1639
|
//#region src/helpers/addons/oxlint-setup.ts
|
|
1107
1640
|
async function setupOxlint(projectDir, packageManager) {
|
|
@@ -1250,58 +1783,20 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
|
|
|
1250
1783
|
//#endregion
|
|
1251
1784
|
//#region src/helpers/addons/skills-setup.ts
|
|
1252
1785
|
const SKILL_SOURCES = {
|
|
1253
|
-
"vercel-labs/agent-skills": {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
},
|
|
1257
|
-
"vercel/
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
},
|
|
1261
|
-
"
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
},
|
|
1265
|
-
"
|
|
1266
|
-
|
|
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
|
-
}
|
|
1786
|
+
"vercel-labs/agent-skills": { label: "Vercel Agent Skills" },
|
|
1787
|
+
"vercel/ai": { label: "Vercel AI SDK" },
|
|
1788
|
+
"vercel/turborepo": { label: "Turborepo" },
|
|
1789
|
+
"yusukebe/hono-skill": { label: "Hono Backend" },
|
|
1790
|
+
"vercel-labs/next-skills": { label: "Next.js Best Practices" },
|
|
1791
|
+
"nuxt/ui": { label: "Nuxt UI" },
|
|
1792
|
+
"heroui-inc/heroui": { label: "HeroUI Native" },
|
|
1793
|
+
"better-auth/skills": { label: "Better Auth" },
|
|
1794
|
+
"neondatabase/agent-skills": { label: "Neon Database" },
|
|
1795
|
+
"supabase/agent-skills": { label: "Supabase" },
|
|
1796
|
+
"expo/skills": { label: "Expo" },
|
|
1797
|
+
"prisma/skills": { label: "Prisma" },
|
|
1798
|
+
"elysiajs/skills": { label: "ElysiaJS" },
|
|
1799
|
+
"waynesutton/convexskills": { label: "Convex" }
|
|
1305
1800
|
};
|
|
1306
1801
|
const AVAILABLE_AGENTS = [
|
|
1307
1802
|
{
|
|
@@ -1405,19 +1900,24 @@ const AVAILABLE_AGENTS = [
|
|
|
1405
1900
|
label: "MCPJam"
|
|
1406
1901
|
}
|
|
1407
1902
|
];
|
|
1903
|
+
function hasReactBasedFrontend(frontend) {
|
|
1904
|
+
return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
|
|
1905
|
+
}
|
|
1906
|
+
function hasNativeFrontend(frontend) {
|
|
1907
|
+
return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
|
|
1908
|
+
}
|
|
1408
1909
|
function getRecommendedSourceKeys(config) {
|
|
1409
1910
|
const sources = [];
|
|
1410
1911
|
const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
|
|
1411
|
-
|
|
1412
|
-
const hasNativeFrontend = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
|
|
1413
|
-
if (hasReactBasedFrontend) sources.push("vercel-labs/agent-skills");
|
|
1912
|
+
if (hasReactBasedFrontend(frontend)) sources.push("vercel-labs/agent-skills");
|
|
1414
1913
|
if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
|
|
1914
|
+
if (frontend.includes("nuxt")) sources.push("nuxt/ui");
|
|
1415
1915
|
if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
|
|
1416
|
-
if (hasNativeFrontend) sources.push("expo/skills");
|
|
1916
|
+
if (hasNativeFrontend(frontend)) sources.push("expo/skills");
|
|
1417
1917
|
if (auth === "better-auth") sources.push("better-auth/skills");
|
|
1418
1918
|
if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
|
|
1419
1919
|
if (dbSetup === "supabase") sources.push("supabase/agent-skills");
|
|
1420
|
-
if (orm === "prisma") sources.push("prisma/skills");
|
|
1920
|
+
if (orm === "prisma" || dbSetup === "prisma-postgres") sources.push("prisma/skills");
|
|
1421
1921
|
if (examples.includes("ai")) sources.push("vercel/ai");
|
|
1422
1922
|
if (addons.includes("turborepo")) sources.push("vercel/turborepo");
|
|
1423
1923
|
if (backend === "hono") sources.push("yusukebe/hono-skill");
|
|
@@ -1425,60 +1925,95 @@ function getRecommendedSourceKeys(config) {
|
|
|
1425
1925
|
if (backend === "convex") sources.push("waynesutton/convexskills");
|
|
1426
1926
|
return sources;
|
|
1427
1927
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1928
|
+
const CURATED_SKILLS_BY_SOURCE = {
|
|
1929
|
+
"vercel-labs/agent-skills": (config) => {
|
|
1930
|
+
const skills = [
|
|
1931
|
+
"web-design-guidelines",
|
|
1932
|
+
"vercel-composition-patterns",
|
|
1933
|
+
"vercel-react-best-practices"
|
|
1934
|
+
];
|
|
1935
|
+
if (hasNativeFrontend(config.frontend)) skills.push("vercel-react-native-skills");
|
|
1936
|
+
return skills;
|
|
1937
|
+
},
|
|
1938
|
+
"vercel/ai": () => ["ai-sdk"],
|
|
1939
|
+
"vercel/turborepo": () => ["turborepo"],
|
|
1940
|
+
"yusukebe/hono-skill": () => ["hono"],
|
|
1941
|
+
"vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
|
|
1942
|
+
"nuxt/ui": () => ["nuxt-ui"],
|
|
1943
|
+
"heroui-inc/heroui": () => ["heroui-native"],
|
|
1944
|
+
"better-auth/skills": () => ["better-auth-best-practices"],
|
|
1945
|
+
"neondatabase/agent-skills": () => ["neon-postgres"],
|
|
1946
|
+
"supabase/agent-skills": () => ["supabase-postgres-best-practices"],
|
|
1947
|
+
"expo/skills": (config) => {
|
|
1948
|
+
const skills = [
|
|
1949
|
+
"expo-dev-client",
|
|
1950
|
+
"building-native-ui",
|
|
1951
|
+
"native-data-fetching",
|
|
1952
|
+
"expo-deployment",
|
|
1953
|
+
"upgrading-expo",
|
|
1954
|
+
"expo-cicd-workflows"
|
|
1955
|
+
];
|
|
1956
|
+
if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
|
|
1957
|
+
return skills;
|
|
1958
|
+
},
|
|
1959
|
+
"prisma/skills": (config) => {
|
|
1960
|
+
const skills = [];
|
|
1961
|
+
if (config.orm === "prisma") skills.push("prisma-cli", "prisma-client-api", "prisma-database-setup");
|
|
1962
|
+
if (config.dbSetup === "prisma-postgres") skills.push("prisma-postgres");
|
|
1963
|
+
return skills;
|
|
1964
|
+
},
|
|
1965
|
+
"elysiajs/skills": () => ["elysiajs"],
|
|
1966
|
+
"waynesutton/convexskills": () => [
|
|
1967
|
+
"convex-best-practices",
|
|
1968
|
+
"convex-functions",
|
|
1969
|
+
"convex-schema-validator",
|
|
1970
|
+
"convex-realtime",
|
|
1971
|
+
"convex-http-actions",
|
|
1972
|
+
"convex-cron-jobs",
|
|
1973
|
+
"convex-file-storage",
|
|
1974
|
+
"convex-migrations",
|
|
1975
|
+
"convex-security-check"
|
|
1976
|
+
]
|
|
1977
|
+
};
|
|
1978
|
+
function getCuratedSkillNamesForSourceKey(sourceKey, config) {
|
|
1979
|
+
return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
|
|
1437
1980
|
}
|
|
1438
1981
|
function uniqueValues(values) {
|
|
1439
1982
|
return Array.from(new Set(values));
|
|
1440
1983
|
}
|
|
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
1984
|
async function setupSkills(config) {
|
|
1453
1985
|
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1454
1986
|
const { packageManager, projectDir } = config;
|
|
1455
1987
|
const btsConfig = await readBtsConfig(projectDir);
|
|
1456
|
-
const
|
|
1988
|
+
const fullConfig = btsConfig ? {
|
|
1457
1989
|
...config,
|
|
1458
1990
|
addons: btsConfig.addons ?? config.addons
|
|
1459
|
-
} : config
|
|
1991
|
+
} : config;
|
|
1992
|
+
const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
|
|
1460
1993
|
if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1994
|
+
const skillOptions = uniqueValues(recommendedSourceKeys).flatMap((sourceKey) => {
|
|
1995
|
+
const source = SKILL_SOURCES[sourceKey];
|
|
1996
|
+
return getCuratedSkillNamesForSourceKey(sourceKey, fullConfig).map((skillName) => ({
|
|
1997
|
+
value: `${sourceKey}::${skillName}`,
|
|
1998
|
+
label: skillName,
|
|
1999
|
+
hint: source.label
|
|
2000
|
+
}));
|
|
2001
|
+
});
|
|
2002
|
+
if (skillOptions.length === 0) return Result.ok(void 0);
|
|
2003
|
+
const scope = await select({
|
|
2004
|
+
message: "Where should skills be installed?",
|
|
2005
|
+
options: [{
|
|
2006
|
+
value: "project",
|
|
2007
|
+
label: "Project",
|
|
2008
|
+
hint: "Writes to project config files (recommended for teams)"
|
|
2009
|
+
}, {
|
|
2010
|
+
value: "global",
|
|
2011
|
+
label: "Global",
|
|
2012
|
+
hint: "Writes to user-level config files (personal machine)"
|
|
2013
|
+
}],
|
|
2014
|
+
initialValue: "project"
|
|
2015
|
+
});
|
|
2016
|
+
if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1482
2017
|
const selectedSkills = await multiselect({
|
|
1483
2018
|
message: "Select skills to install",
|
|
1484
2019
|
options: skillOptions,
|
|
@@ -1508,11 +2043,12 @@ async function setupSkills(config) {
|
|
|
1508
2043
|
const installSpinner = spinner();
|
|
1509
2044
|
installSpinner.start("Installing skills...");
|
|
1510
2045
|
const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
|
|
2046
|
+
const globalFlag = scope === "global" ? "-g" : "";
|
|
1511
2047
|
for (const [source, skills] of Object.entries(skillsBySource)) {
|
|
1512
2048
|
const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
|
|
1513
2049
|
if ((await Result.tryPromise({
|
|
1514
2050
|
try: async () => {
|
|
1515
|
-
const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${skillFlags} ${agentFlags} -y`);
|
|
2051
|
+
const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
|
|
1516
2052
|
await $({
|
|
1517
2053
|
cwd: projectDir,
|
|
1518
2054
|
env: { CI: "true" }
|
|
@@ -2012,6 +2548,7 @@ async function setupAddons(config) {
|
|
|
2012
2548
|
if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
|
|
2013
2549
|
if (addons.includes("ruler")) await runSetup(() => setupRuler(config));
|
|
2014
2550
|
if (addons.includes("skills")) await runSetup(() => setupSkills(config));
|
|
2551
|
+
if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
|
|
2015
2552
|
}
|
|
2016
2553
|
async function setupBiome(projectDir) {
|
|
2017
2554
|
await addPackageDependency({
|
|
@@ -3026,37 +3563,19 @@ async function getProjectName(initialName) {
|
|
|
3026
3563
|
initialValue: initialName,
|
|
3027
3564
|
defaultValue: defaultName,
|
|
3028
3565
|
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");
|
|
3566
|
+
const nameToUse = String(value ?? "").trim() || defaultName;
|
|
3567
|
+
const validationError = validateDirectoryName(path.basename(nameToUse));
|
|
3568
|
+
if (validationError) return validationError;
|
|
3569
|
+
if (nameToUse !== ".") {
|
|
3570
|
+
if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3574
|
+
if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
|
|
3575
|
+
projectPath = response || defaultName;
|
|
3576
|
+
isValid = true;
|
|
3577
|
+
}
|
|
3578
|
+
return projectPath;
|
|
3060
3579
|
}
|
|
3061
3580
|
|
|
3062
3581
|
//#endregion
|
|
@@ -3069,7 +3588,7 @@ function getLatestCLIVersion() {
|
|
|
3069
3588
|
*/
|
|
3070
3589
|
function isTelemetryEnabled() {
|
|
3071
3590
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
3072
|
-
const BTS_TELEMETRY = "
|
|
3591
|
+
const BTS_TELEMETRY = "0";
|
|
3073
3592
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
3074
3593
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
3075
3594
|
return true;
|
|
@@ -3077,17 +3596,7 @@ function isTelemetryEnabled() {
|
|
|
3077
3596
|
|
|
3078
3597
|
//#endregion
|
|
3079
3598
|
//#region src/utils/analytics.ts
|
|
3080
|
-
|
|
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
|
-
}
|
|
3599
|
+
async function sendConvexEvent(payload) {}
|
|
3091
3600
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
3092
3601
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
3093
3602
|
const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
|
|
@@ -3233,119 +3742,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
3233
3742
|
};
|
|
3234
3743
|
}
|
|
3235
3744
|
|
|
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
3745
|
//#endregion
|
|
3350
3746
|
//#region src/utils/project-name-validation.ts
|
|
3351
3747
|
function validateProjectName(name) {
|
|
@@ -5072,11 +5468,12 @@ async function displayPostInstallInstructions(config) {
|
|
|
5072
5468
|
"solid"
|
|
5073
5469
|
].includes(f));
|
|
5074
5470
|
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
5471
|
const hasReactRouter = frontend?.includes("react-router");
|
|
5078
5472
|
const hasSvelte = frontend?.includes("svelte");
|
|
5079
5473
|
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
|
|
5474
|
+
const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager) : "";
|
|
5475
|
+
const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
|
|
5476
|
+
const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
|
5080
5477
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
5081
5478
|
let stepCounter = 2;
|
|
5082
5479
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
|
|
@@ -5116,6 +5513,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
5116
5513
|
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
5117
5514
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
5118
5515
|
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
|
|
5516
|
+
if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
|
|
5119
5517
|
if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
|
|
5120
5518
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
5121
5519
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -5199,6 +5597,10 @@ function getBunWebNativeWarning() {
|
|
|
5199
5597
|
function getClerkInstructions() {
|
|
5200
5598
|
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
5599
|
}
|
|
5600
|
+
function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager) {
|
|
5601
|
+
const cmd = packageManager === "npm" ? "npx" : packageManager;
|
|
5602
|
+
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` : "");
|
|
5603
|
+
}
|
|
5202
5604
|
function getPolarInstructions(backend) {
|
|
5203
5605
|
const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
|
|
5204
5606
|
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 +5682,7 @@ async function setPackageManagerVersion(projectDir, packageManager) {
|
|
|
5280
5682
|
if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
|
|
5281
5683
|
const versionResult = await Result.tryPromise({
|
|
5282
5684
|
try: async () => {
|
|
5283
|
-
const { stdout } = await
|
|
5685
|
+
const { stdout } = await $({ cwd: os$1.tmpdir() })`${packageManager} -v`;
|
|
5284
5686
|
return stdout.trim();
|
|
5285
5687
|
},
|
|
5286
5688
|
catch: () => null
|
|
@@ -5560,121 +5962,8 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
5560
5962
|
}
|
|
5561
5963
|
}
|
|
5562
5964
|
|
|
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
5965
|
//#endregion
|
|
5621
5966
|
//#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
5967
|
const router = os.router({
|
|
5679
5968
|
create: os.meta({
|
|
5680
5969
|
description: "Create a new Better-T-Stack project",
|
|
@@ -5713,39 +6002,9 @@ const router = os.router({
|
|
|
5713
6002
|
});
|
|
5714
6003
|
if (options.verbose) return result;
|
|
5715
6004
|
}),
|
|
5716
|
-
sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(
|
|
5717
|
-
|
|
5718
|
-
|
|
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
|
-
}),
|
|
6005
|
+
sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(showSponsorsCommand),
|
|
6006
|
+
docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(openDocsCommand),
|
|
6007
|
+
builder: os.meta({ description: "Open the web-based stack builder" }).handler(openBuilderCommand),
|
|
5749
6008
|
add: os.meta({ description: "Add addons to an existing Better-T-Stack project" }).input(z.object({
|
|
5750
6009
|
addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
|
|
5751
6010
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
|