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