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.
- 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-C4Fa8qXv.mjs} +749 -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,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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|
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
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
}),
|
|
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"),
|