create-better-t-stack 3.19.2 → 3.19.4
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
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as DatabaseSetupError, a as VirtualFileSystem, b as UserCancelledError, c as create, d as docs, f as generate, g as CompatibilityError, h as CLIError, i as TEMPLATE_COUNT, l as createBtsCli, m as sponsors, n as GeneratorError, o as add, p as router, r as Result, s as builder, t as EMBEDDED_TEMPLATES, u as createVirtual, v as DirectoryConflictError, x as ValidationError, y as ProjectCreationError } from "./src-
|
|
2
|
+
import { _ as DatabaseSetupError, a as VirtualFileSystem, b as UserCancelledError, c as create, d as docs, f as generate, g as CompatibilityError, h as CLIError, i as TEMPLATE_COUNT, l as createBtsCli, m as sponsors, n as GeneratorError, o as add, p as router, r as Result, s as builder, t as EMBEDDED_TEMPLATES, u as createVirtual, v as DirectoryConflictError, x as ValidationError, y as ProjectCreationError } from "./src-mMd-eO_p.mjs";
|
|
3
3
|
|
|
4
4
|
export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, router, sponsors };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as __reExport } from "./chunk-
|
|
2
|
+
import { t as __reExport } from "./chunk-CHc3S52W.mjs";
|
|
3
3
|
import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
4
4
|
import { createRouterClient, os } from "@orpc/server";
|
|
5
5
|
import { Result, Result as Result$1, TaggedError } from "better-result";
|
|
@@ -756,11 +756,11 @@ async function getAddonsChoice(addons, frontends, auth) {
|
|
|
756
756
|
else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
|
|
757
757
|
else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
|
|
758
758
|
}
|
|
759
|
-
Object.keys(groupedOptions).forEach((group
|
|
760
|
-
if (groupedOptions[group
|
|
759
|
+
Object.keys(groupedOptions).forEach((group) => {
|
|
760
|
+
if (groupedOptions[group].length === 0) delete groupedOptions[group];
|
|
761
761
|
else {
|
|
762
|
-
const groupOrder = ADDON_GROUPS[group
|
|
763
|
-
groupedOptions[group
|
|
762
|
+
const groupOrder = ADDON_GROUPS[group] || [];
|
|
763
|
+
groupedOptions[group].sort((a, b) => {
|
|
764
764
|
return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
|
|
765
765
|
});
|
|
766
766
|
}
|
|
@@ -795,11 +795,11 @@ async function getAddonsToAdd(frontend, existingAddons = [], auth) {
|
|
|
795
795
|
else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
|
|
796
796
|
else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
|
|
797
797
|
}
|
|
798
|
-
Object.keys(groupedOptions).forEach((group
|
|
799
|
-
if (groupedOptions[group
|
|
798
|
+
Object.keys(groupedOptions).forEach((group) => {
|
|
799
|
+
if (groupedOptions[group].length === 0) delete groupedOptions[group];
|
|
800
800
|
else {
|
|
801
|
-
const groupOrder = ADDON_GROUPS[group
|
|
802
|
-
groupedOptions[group
|
|
801
|
+
const groupOrder = ADDON_GROUPS[group] || [];
|
|
802
|
+
groupedOptions[group].sort((a, b) => {
|
|
803
803
|
return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
|
|
804
804
|
});
|
|
805
805
|
}
|
|
@@ -905,8 +905,52 @@ const addPackageDependency = async (opts) => {
|
|
|
905
905
|
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
906
906
|
};
|
|
907
907
|
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/utils/external-commands.ts
|
|
910
|
+
function shouldSkipExternalCommands() {
|
|
911
|
+
return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
|
|
912
|
+
}
|
|
913
|
+
|
|
908
914
|
//#endregion
|
|
909
915
|
//#region src/utils/package-runner.ts
|
|
916
|
+
function splitCommandArgs(commandWithArgs) {
|
|
917
|
+
const args = [];
|
|
918
|
+
let current = "";
|
|
919
|
+
let quote = null;
|
|
920
|
+
for (let i = 0; i < commandWithArgs.length; i += 1) {
|
|
921
|
+
const char = commandWithArgs[i];
|
|
922
|
+
if (quote) {
|
|
923
|
+
if (char === quote) {
|
|
924
|
+
quote = null;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (char === "\\" && i + 1 < commandWithArgs.length) {
|
|
928
|
+
const nextChar = commandWithArgs[i + 1];
|
|
929
|
+
if (nextChar === quote || nextChar === "\\") {
|
|
930
|
+
current += nextChar;
|
|
931
|
+
i += 1;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
current += char;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (char === "\"" || char === "'") {
|
|
939
|
+
quote = char;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (/\s/.test(char)) {
|
|
943
|
+
if (current.length > 0) {
|
|
944
|
+
args.push(current);
|
|
945
|
+
current = "";
|
|
946
|
+
}
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
current += char;
|
|
950
|
+
}
|
|
951
|
+
if (current.length > 0) args.push(current);
|
|
952
|
+
return args;
|
|
953
|
+
}
|
|
910
954
|
/**
|
|
911
955
|
* Returns the appropriate command for running a package without installing it globally,
|
|
912
956
|
* based on the selected package manager.
|
|
@@ -931,7 +975,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
931
975
|
* @returns An array of [command, ...args] (e.g., ["npx", "prisma", "generate"]).
|
|
932
976
|
*/
|
|
933
977
|
function getPackageExecutionArgs(packageManager, commandWithArgs) {
|
|
934
|
-
const args = commandWithArgs
|
|
978
|
+
const args = splitCommandArgs(commandWithArgs);
|
|
935
979
|
switch (packageManager) {
|
|
936
980
|
case "pnpm": return [
|
|
937
981
|
"pnpm",
|
|
@@ -1002,14 +1046,15 @@ const TEMPLATES$2 = {
|
|
|
1002
1046
|
}
|
|
1003
1047
|
};
|
|
1004
1048
|
async function setupFumadocs(config) {
|
|
1049
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1005
1050
|
const { packageManager, projectDir } = config;
|
|
1006
1051
|
log.info("Setting up Fumadocs...");
|
|
1007
1052
|
const template = await select({
|
|
1008
1053
|
message: "Choose a template",
|
|
1009
|
-
options: Object.entries(TEMPLATES$2).map(([key, template
|
|
1054
|
+
options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
|
|
1010
1055
|
value: key,
|
|
1011
|
-
label: template
|
|
1012
|
-
hint: template
|
|
1056
|
+
label: template.label,
|
|
1057
|
+
hint: template.hint
|
|
1013
1058
|
})),
|
|
1014
1059
|
initialValue: "next-mdx"
|
|
1015
1060
|
});
|
|
@@ -1060,37 +1105,53 @@ async function setupFumadocs(config) {
|
|
|
1060
1105
|
//#endregion
|
|
1061
1106
|
//#region src/helpers/addons/oxlint-setup.ts
|
|
1062
1107
|
async function setupOxlint(projectDir, packageManager) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1108
|
+
return Result.tryPromise({
|
|
1109
|
+
try: async () => {
|
|
1110
|
+
await addPackageDependency({
|
|
1111
|
+
devDependencies: ["oxlint", "oxfmt"],
|
|
1112
|
+
projectDir
|
|
1113
|
+
});
|
|
1114
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
1115
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
1116
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
1117
|
+
packageJson.scripts = {
|
|
1118
|
+
...packageJson.scripts,
|
|
1119
|
+
check: "oxlint && oxfmt --write"
|
|
1120
|
+
};
|
|
1121
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1122
|
+
}
|
|
1123
|
+
if (shouldSkipExternalCommands()) return;
|
|
1124
|
+
const s = spinner();
|
|
1125
|
+
s.start("Initializing oxlint and oxfmt...");
|
|
1126
|
+
try {
|
|
1127
|
+
const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
|
|
1128
|
+
await $({
|
|
1129
|
+
cwd: projectDir,
|
|
1130
|
+
env: { CI: "true" }
|
|
1131
|
+
})`${oxlintArgs}`;
|
|
1132
|
+
const oxfmtArgs = getPackageExecutionArgs(packageManager, "oxfmt@latest --init");
|
|
1133
|
+
await $({
|
|
1134
|
+
cwd: projectDir,
|
|
1135
|
+
env: { CI: "true" }
|
|
1136
|
+
})`${oxfmtArgs}`;
|
|
1137
|
+
s.stop("oxlint and oxfmt initialized successfully!");
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
s.stop("Failed to initialize oxlint and oxfmt");
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
catch: (error) => new AddonSetupError({
|
|
1144
|
+
addon: "oxlint",
|
|
1145
|
+
message: `Failed to set up oxlint: ${error instanceof Error ? error.message : String(error)}`,
|
|
1146
|
+
cause: error
|
|
1147
|
+
})
|
|
1066
1148
|
});
|
|
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
1149
|
}
|
|
1090
1150
|
|
|
1091
1151
|
//#endregion
|
|
1092
1152
|
//#region src/helpers/addons/ruler-setup.ts
|
|
1093
1153
|
async function setupRuler(config) {
|
|
1154
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1094
1155
|
const { packageManager, projectDir } = config;
|
|
1095
1156
|
log.info("Setting up Ruler...");
|
|
1096
1157
|
const rulerDir = path.join(projectDir, ".ruler");
|
|
@@ -1193,10 +1254,6 @@ const SKILL_SOURCES = {
|
|
|
1193
1254
|
source: "vercel-labs/agent-skills",
|
|
1194
1255
|
label: "Vercel Agent Skills"
|
|
1195
1256
|
},
|
|
1196
|
-
"anthropics/skills": {
|
|
1197
|
-
source: "https://github.com/anthropics/skills",
|
|
1198
|
-
label: "Anthropic Skills"
|
|
1199
|
-
},
|
|
1200
1257
|
"vercel/ai": {
|
|
1201
1258
|
source: "vercel/ai",
|
|
1202
1259
|
label: "Vercel AI SDK"
|
|
@@ -1229,6 +1286,14 @@ const SKILL_SOURCES = {
|
|
|
1229
1286
|
source: "supabase/agent-skills",
|
|
1230
1287
|
label: "Supabase"
|
|
1231
1288
|
},
|
|
1289
|
+
"expo/skills": {
|
|
1290
|
+
source: "expo/skills",
|
|
1291
|
+
label: "Expo"
|
|
1292
|
+
},
|
|
1293
|
+
"prisma/skills": {
|
|
1294
|
+
source: "prisma/skills",
|
|
1295
|
+
label: "Prisma"
|
|
1296
|
+
},
|
|
1232
1297
|
"elysiajs/skills": {
|
|
1233
1298
|
source: "elysiajs/skills",
|
|
1234
1299
|
label: "ElysiaJS"
|
|
@@ -1342,13 +1407,17 @@ const AVAILABLE_AGENTS = [
|
|
|
1342
1407
|
];
|
|
1343
1408
|
function getRecommendedSourceKeys(config) {
|
|
1344
1409
|
const sources = [];
|
|
1345
|
-
const { frontend, backend, dbSetup, auth, examples, addons } = config;
|
|
1346
|
-
|
|
1410
|
+
const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
|
|
1411
|
+
const hasReactBasedFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
|
|
1412
|
+
const hasNativeFrontend = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
|
|
1413
|
+
if (hasReactBasedFrontend) sources.push("vercel-labs/agent-skills");
|
|
1347
1414
|
if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
|
|
1348
1415
|
if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
|
|
1416
|
+
if (hasNativeFrontend) sources.push("expo/skills");
|
|
1349
1417
|
if (auth === "better-auth") sources.push("better-auth/skills");
|
|
1350
1418
|
if (dbSetup === "neon") sources.push("neondatabase/agent-skills");
|
|
1351
1419
|
if (dbSetup === "supabase") sources.push("supabase/agent-skills");
|
|
1420
|
+
if (orm === "prisma") sources.push("prisma/skills");
|
|
1352
1421
|
if (examples.includes("ai")) sources.push("vercel/ai");
|
|
1353
1422
|
if (addons.includes("turborepo")) sources.push("vercel/turborepo");
|
|
1354
1423
|
if (backend === "hono") sources.push("yusukebe/hono-skill");
|
|
@@ -1359,12 +1428,16 @@ function getRecommendedSourceKeys(config) {
|
|
|
1359
1428
|
function parseSkillsFromOutput(output) {
|
|
1360
1429
|
const skills = [];
|
|
1361
1430
|
const lines = output.split("\n");
|
|
1431
|
+
const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
1362
1432
|
for (const line of lines) {
|
|
1363
|
-
const match = line.replace(
|
|
1433
|
+
const match = line.replace(ansiRegex, "").match(/^│\s{4}([a-z][a-z0-9-]*)$/);
|
|
1364
1434
|
if (match) skills.push(match[1]);
|
|
1365
1435
|
}
|
|
1366
1436
|
return skills;
|
|
1367
1437
|
}
|
|
1438
|
+
function uniqueValues(values) {
|
|
1439
|
+
return Array.from(new Set(values));
|
|
1440
|
+
}
|
|
1368
1441
|
async function fetchSkillsFromSource(source, packageManager, projectDir) {
|
|
1369
1442
|
try {
|
|
1370
1443
|
const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source.source} --list`);
|
|
@@ -1377,6 +1450,7 @@ async function fetchSkillsFromSource(source, packageManager, projectDir) {
|
|
|
1377
1450
|
}
|
|
1378
1451
|
}
|
|
1379
1452
|
async function setupSkills(config) {
|
|
1453
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1380
1454
|
const { packageManager, projectDir } = config;
|
|
1381
1455
|
const btsConfig = await readBtsConfig(projectDir);
|
|
1382
1456
|
const recommendedSourceKeys = getRecommendedSourceKeys(btsConfig ? {
|
|
@@ -1384,19 +1458,20 @@ async function setupSkills(config) {
|
|
|
1384
1458
|
addons: btsConfig.addons ?? config.addons
|
|
1385
1459
|
} : config);
|
|
1386
1460
|
if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
|
|
1461
|
+
const sourceKeys = uniqueValues(recommendedSourceKeys);
|
|
1387
1462
|
const s = spinner();
|
|
1388
1463
|
s.start("Fetching available skills...");
|
|
1389
1464
|
const allSkills = [];
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
}
|
|
1465
|
+
const sources = sourceKeys.map((sourceKey) => SKILL_SOURCES[sourceKey]).filter((source) => Boolean(source));
|
|
1466
|
+
const fetchedSkills = await Promise.all(sources.map(async (source) => ({
|
|
1467
|
+
source,
|
|
1468
|
+
skills: await fetchSkillsFromSource(source, packageManager, projectDir)
|
|
1469
|
+
})));
|
|
1470
|
+
for (const { source, skills } of fetchedSkills) for (const skillName of skills) allSkills.push({
|
|
1471
|
+
name: skillName,
|
|
1472
|
+
source: source.source,
|
|
1473
|
+
sourceLabel: source.label
|
|
1474
|
+
});
|
|
1400
1475
|
s.stop("Fetched available skills");
|
|
1401
1476
|
if (allSkills.length === 0) return Result.ok(void 0);
|
|
1402
1477
|
const skillOptions = allSkills.map((skill) => ({
|
|
@@ -1434,7 +1509,7 @@ async function setupSkills(config) {
|
|
|
1434
1509
|
installSpinner.start("Installing skills...");
|
|
1435
1510
|
const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
|
|
1436
1511
|
for (const [source, skills] of Object.entries(skillsBySource)) {
|
|
1437
|
-
const skillFlags = skills.map((s
|
|
1512
|
+
const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
|
|
1438
1513
|
if ((await Result.tryPromise({
|
|
1439
1514
|
try: async () => {
|
|
1440
1515
|
const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${skillFlags} ${agentFlags} -y`);
|
|
@@ -1457,6 +1532,7 @@ async function setupSkills(config) {
|
|
|
1457
1532
|
//#endregion
|
|
1458
1533
|
//#region src/helpers/addons/starlight-setup.ts
|
|
1459
1534
|
async function setupStarlight(config) {
|
|
1535
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1460
1536
|
const { packageManager, projectDir } = config;
|
|
1461
1537
|
const s = spinner();
|
|
1462
1538
|
s.start("Setting up Starlight docs...");
|
|
@@ -1496,6 +1572,7 @@ async function setupStarlight(config) {
|
|
|
1496
1572
|
//#endregion
|
|
1497
1573
|
//#region src/helpers/addons/tauri-setup.ts
|
|
1498
1574
|
async function setupTauri(config) {
|
|
1575
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1499
1576
|
const { packageManager, frontend, projectDir } = config;
|
|
1500
1577
|
const s = spinner();
|
|
1501
1578
|
const clientPackageDir = path.join(projectDir, "apps/web");
|
|
@@ -1556,14 +1633,15 @@ const TEMPLATES$1 = {
|
|
|
1556
1633
|
}
|
|
1557
1634
|
};
|
|
1558
1635
|
async function setupTui(config) {
|
|
1636
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1559
1637
|
const { packageManager, projectDir } = config;
|
|
1560
1638
|
log.info("Setting up OpenTUI...");
|
|
1561
1639
|
const template = await select({
|
|
1562
1640
|
message: "Choose a template",
|
|
1563
|
-
options: Object.entries(TEMPLATES$1).map(([key, template
|
|
1641
|
+
options: Object.entries(TEMPLATES$1).map(([key, template]) => ({
|
|
1564
1642
|
value: key,
|
|
1565
|
-
label: template
|
|
1566
|
-
hint: template
|
|
1643
|
+
label: template.label,
|
|
1644
|
+
hint: template.hint
|
|
1567
1645
|
})),
|
|
1568
1646
|
initialValue: "core"
|
|
1569
1647
|
});
|
|
@@ -1681,6 +1759,7 @@ function getFrameworksFromFrontend(frontend) {
|
|
|
1681
1759
|
return Array.from(frameworks);
|
|
1682
1760
|
}
|
|
1683
1761
|
async function setupUltracite(config, gitHooks) {
|
|
1762
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1684
1763
|
const { packageManager, projectDir, frontend } = config;
|
|
1685
1764
|
log.info("Setting up Ultracite...");
|
|
1686
1765
|
let result;
|
|
@@ -1689,10 +1768,10 @@ async function setupUltracite(config, gitHooks) {
|
|
|
1689
1768
|
return await group({
|
|
1690
1769
|
linter: () => select({
|
|
1691
1770
|
message: "Choose linter/formatter",
|
|
1692
|
-
options: Object.entries(LINTERS).map(([key, linter
|
|
1771
|
+
options: Object.entries(LINTERS).map(([key, linter]) => ({
|
|
1693
1772
|
value: key,
|
|
1694
|
-
label: linter
|
|
1695
|
-
hint: linter
|
|
1773
|
+
label: linter.label,
|
|
1774
|
+
hint: linter.hint
|
|
1696
1775
|
})),
|
|
1697
1776
|
initialValue: "biome"
|
|
1698
1777
|
}),
|
|
@@ -1811,14 +1890,15 @@ const TEMPLATES = {
|
|
|
1811
1890
|
}
|
|
1812
1891
|
};
|
|
1813
1892
|
async function setupWxt(config) {
|
|
1893
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1814
1894
|
const { packageManager, projectDir } = config;
|
|
1815
1895
|
log.info("Setting up WXT...");
|
|
1816
1896
|
const template = await select({
|
|
1817
1897
|
message: "Choose a template",
|
|
1818
|
-
options: Object.entries(TEMPLATES).map(([key, template
|
|
1898
|
+
options: Object.entries(TEMPLATES).map(([key, template]) => ({
|
|
1819
1899
|
value: key,
|
|
1820
|
-
label: template
|
|
1821
|
-
hint: template
|
|
1900
|
+
label: template.label,
|
|
1901
|
+
hint: template.hint
|
|
1822
1902
|
})),
|
|
1823
1903
|
initialValue: "react"
|
|
1824
1904
|
});
|
|
@@ -1886,6 +1966,17 @@ async function runSetup(setupFn) {
|
|
|
1886
1966
|
consola.error(pc.red(result.error.message));
|
|
1887
1967
|
}
|
|
1888
1968
|
}
|
|
1969
|
+
async function runAddonStep(addon, step) {
|
|
1970
|
+
const result = await Result.tryPromise({
|
|
1971
|
+
try: async () => step(),
|
|
1972
|
+
catch: (e) => new AddonSetupError({
|
|
1973
|
+
addon,
|
|
1974
|
+
message: `Failed to set up ${addon}: ${e instanceof Error ? e.message : String(e)}`,
|
|
1975
|
+
cause: e
|
|
1976
|
+
})
|
|
1977
|
+
});
|
|
1978
|
+
if (result.isErr()) consola.error(pc.red(result.error.message));
|
|
1979
|
+
}
|
|
1889
1980
|
async function setupAddons(config) {
|
|
1890
1981
|
const { addons, frontend, projectDir } = config;
|
|
1891
1982
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
@@ -1905,14 +1996,14 @@ async function setupAddons(config) {
|
|
|
1905
1996
|
if (hasLefthook) gitHooks.push("lefthook");
|
|
1906
1997
|
await runSetup(() => setupUltracite(config, gitHooks));
|
|
1907
1998
|
} else {
|
|
1908
|
-
if (hasBiome) await setupBiome(projectDir);
|
|
1909
|
-
if (hasOxlint) await setupOxlint(projectDir, config.packageManager);
|
|
1999
|
+
if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
|
|
2000
|
+
if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
|
|
1910
2001
|
if (hasHusky || hasLefthook) {
|
|
1911
2002
|
let linter;
|
|
1912
2003
|
if (hasOxlint) linter = "oxlint";
|
|
1913
2004
|
else if (hasBiome) linter = "biome";
|
|
1914
|
-
if (hasHusky) await setupHusky(projectDir, linter);
|
|
1915
|
-
if (hasLefthook) await setupLefthook(projectDir);
|
|
2005
|
+
if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
|
|
2006
|
+
if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
|
|
1916
2007
|
}
|
|
1917
2008
|
}
|
|
1918
2009
|
if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
|
|
@@ -1996,6 +2087,7 @@ async function detectProjectConfig(projectDir) {
|
|
|
1996
2087
|
//#endregion
|
|
1997
2088
|
//#region src/helpers/core/install-dependencies.ts
|
|
1998
2089
|
async function installDependencies({ projectDir, packageManager }) {
|
|
2090
|
+
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1999
2091
|
const s = spinner();
|
|
2000
2092
|
s.start(`Running ${packageManager} install...`);
|
|
2001
2093
|
const result = await Result.tryPromise({
|
|
@@ -2230,13 +2322,13 @@ async function getAuthChoice(auth, backend, frontend) {
|
|
|
2230
2322
|
label: "None",
|
|
2231
2323
|
hint: "No auth"
|
|
2232
2324
|
});
|
|
2233
|
-
const response
|
|
2325
|
+
const response = await navigableSelect({
|
|
2234
2326
|
message: "Select authentication provider",
|
|
2235
2327
|
options,
|
|
2236
2328
|
initialValue: "none"
|
|
2237
2329
|
});
|
|
2238
|
-
if (isCancel$1(response
|
|
2239
|
-
return response
|
|
2330
|
+
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
2331
|
+
return response;
|
|
2240
2332
|
}
|
|
2241
2333
|
const response = await navigableSelect({
|
|
2242
2334
|
message: "Select authentication provider",
|
|
@@ -2835,6 +2927,27 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
2835
2927
|
//#endregion
|
|
2836
2928
|
//#region src/prompts/config-prompts.ts
|
|
2837
2929
|
async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
2930
|
+
if (isSilent()) return {
|
|
2931
|
+
projectName,
|
|
2932
|
+
projectDir,
|
|
2933
|
+
relativePath,
|
|
2934
|
+
frontend: flags.frontend ?? [...DEFAULT_CONFIG.frontend],
|
|
2935
|
+
backend: flags.backend ?? DEFAULT_CONFIG.backend,
|
|
2936
|
+
runtime: flags.runtime ?? DEFAULT_CONFIG.runtime,
|
|
2937
|
+
database: flags.database ?? DEFAULT_CONFIG.database,
|
|
2938
|
+
orm: flags.orm ?? DEFAULT_CONFIG.orm,
|
|
2939
|
+
auth: flags.auth ?? DEFAULT_CONFIG.auth,
|
|
2940
|
+
payments: flags.payments ?? DEFAULT_CONFIG.payments,
|
|
2941
|
+
addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
|
|
2942
|
+
examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
|
|
2943
|
+
git: flags.git ?? DEFAULT_CONFIG.git,
|
|
2944
|
+
packageManager: flags.packageManager ?? DEFAULT_CONFIG.packageManager,
|
|
2945
|
+
install: flags.install ?? DEFAULT_CONFIG.install,
|
|
2946
|
+
dbSetup: flags.dbSetup ?? DEFAULT_CONFIG.dbSetup,
|
|
2947
|
+
api: flags.api ?? DEFAULT_CONFIG.api,
|
|
2948
|
+
webDeploy: flags.webDeploy ?? DEFAULT_CONFIG.webDeploy,
|
|
2949
|
+
serverDeploy: flags.serverDeploy ?? DEFAULT_CONFIG.serverDeploy
|
|
2950
|
+
};
|
|
2838
2951
|
const result = await navigableGroup({
|
|
2839
2952
|
frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
|
|
2840
2953
|
backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
|
|
@@ -2880,7 +2993,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
2880
2993
|
|
|
2881
2994
|
//#endregion
|
|
2882
2995
|
//#region src/prompts/project-name.ts
|
|
2883
|
-
function isPathWithinCwd(targetPath) {
|
|
2996
|
+
function isPathWithinCwd$1(targetPath) {
|
|
2884
2997
|
const resolved = path.resolve(targetPath);
|
|
2885
2998
|
const rel = path.relative(process.cwd(), resolved);
|
|
2886
2999
|
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
@@ -2894,7 +3007,7 @@ async function getProjectName(initialName) {
|
|
|
2894
3007
|
if (initialName) {
|
|
2895
3008
|
if (initialName === ".") return initialName;
|
|
2896
3009
|
if (!validateDirectoryName(path.basename(initialName))) {
|
|
2897
|
-
if (isPathWithinCwd(path.resolve(process.cwd(), initialName))) return initialName;
|
|
3010
|
+
if (isPathWithinCwd$1(path.resolve(process.cwd(), initialName))) return initialName;
|
|
2898
3011
|
consola.error(pc.red("Project path must be within current directory"));
|
|
2899
3012
|
}
|
|
2900
3013
|
}
|
|
@@ -2917,7 +3030,7 @@ async function getProjectName(initialName) {
|
|
|
2917
3030
|
const validationError = validateDirectoryName(path.basename(nameToUse));
|
|
2918
3031
|
if (validationError) return validationError;
|
|
2919
3032
|
if (nameToUse !== ".") {
|
|
2920
|
-
if (!isPathWithinCwd(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
|
|
3033
|
+
if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
|
|
2921
3034
|
}
|
|
2922
3035
|
}
|
|
2923
3036
|
});
|
|
@@ -2930,10 +3043,21 @@ async function getProjectName(initialName) {
|
|
|
2930
3043
|
|
|
2931
3044
|
//#endregion
|
|
2932
3045
|
//#region src/utils/get-latest-cli-version.ts
|
|
2933
|
-
|
|
3046
|
+
function getLatestCLIVersionResult() {
|
|
2934
3047
|
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
2935
|
-
return
|
|
2936
|
-
|
|
3048
|
+
return Result.try({
|
|
3049
|
+
try: () => {
|
|
3050
|
+
return fs.readJSONSync(packageJsonPath).version;
|
|
3051
|
+
},
|
|
3052
|
+
catch: (e) => new CLIError({
|
|
3053
|
+
message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
|
|
3054
|
+
cause: e
|
|
3055
|
+
})
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
function getLatestCLIVersion() {
|
|
3059
|
+
return getLatestCLIVersionResult().unwrapOr("1.0.0");
|
|
3060
|
+
}
|
|
2937
3061
|
|
|
2938
3062
|
//#endregion
|
|
2939
3063
|
//#region src/utils/telemetry.ts
|
|
@@ -3222,6 +3346,25 @@ async function clearHistory() {
|
|
|
3222
3346
|
});
|
|
3223
3347
|
}
|
|
3224
3348
|
|
|
3349
|
+
//#endregion
|
|
3350
|
+
//#region src/utils/project-name-validation.ts
|
|
3351
|
+
function validateProjectName(name) {
|
|
3352
|
+
const result = types_exports.ProjectNameSchema.safeParse(name);
|
|
3353
|
+
if (!result.success) return Result.err(new ValidationError({
|
|
3354
|
+
field: "projectName",
|
|
3355
|
+
value: name,
|
|
3356
|
+
message: `Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`
|
|
3357
|
+
}));
|
|
3358
|
+
return Result.ok(void 0);
|
|
3359
|
+
}
|
|
3360
|
+
function extractAndValidateProjectName(projectName, projectDirectory) {
|
|
3361
|
+
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
3362
|
+
if (!derivedName) return Result.ok("");
|
|
3363
|
+
const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
|
|
3364
|
+
if (validationResult.isErr()) return Result.err(validationResult.error);
|
|
3365
|
+
return Result.ok(projectName || derivedName);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3225
3368
|
//#endregion
|
|
3226
3369
|
//#region src/utils/templates.ts
|
|
3227
3370
|
const TEMPLATE_PRESETS = {
|
|
@@ -3515,7 +3658,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
3515
3658
|
yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
3516
3659
|
yield* validateSelfBackendCompatibility(providedFlags, options, config);
|
|
3517
3660
|
yield* validateWorkersCompatibility(providedFlags, options, config);
|
|
3518
|
-
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose '
|
|
3661
|
+
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
|
|
3519
3662
|
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
3663
|
if (config.addons && config.addons.length > 0) {
|
|
3521
3664
|
yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
@@ -3538,25 +3681,6 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
3538
3681
|
});
|
|
3539
3682
|
}
|
|
3540
3683
|
|
|
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
3684
|
//#endregion
|
|
3561
3685
|
//#region src/validation.ts
|
|
3562
3686
|
const CORE_STACK_FLAGS = new Set([
|
|
@@ -3584,8 +3708,8 @@ function validateYesFlagCombination(options, providedFlags) {
|
|
|
3584
3708
|
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
3585
3709
|
if (options.yolo) {
|
|
3586
3710
|
const cfg = processFlags(options, projectName);
|
|
3587
|
-
const validatedProjectNameResult
|
|
3588
|
-
if (validatedProjectNameResult
|
|
3711
|
+
const validatedProjectNameResult = extractAndValidateProjectName(projectName, options.projectDirectory);
|
|
3712
|
+
if (validatedProjectNameResult.isOk() && validatedProjectNameResult.value) cfg.projectName = validatedProjectNameResult.value;
|
|
3589
3713
|
return Result.ok(cfg);
|
|
3590
3714
|
}
|
|
3591
3715
|
const yesFlagResult = validateYesFlagCombination(options, providedFlags);
|
|
@@ -3696,18 +3820,26 @@ async function addEnvVariablesToFile(envPath, variables) {
|
|
|
3696
3820
|
//#region src/helpers/database-providers/d1-setup.ts
|
|
3697
3821
|
async function setupCloudflareD1(config) {
|
|
3698
3822
|
const { projectDir, serverDeploy, orm, backend } = config;
|
|
3699
|
-
if (serverDeploy === "cloudflare" && orm === "prisma")
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3823
|
+
if (!(serverDeploy === "cloudflare" && orm === "prisma")) return Result.ok(void 0);
|
|
3824
|
+
return Result.tryPromise({
|
|
3825
|
+
try: async () => {
|
|
3826
|
+
const targetApp = backend === "self" ? "apps/web" : "apps/server";
|
|
3827
|
+
await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
|
|
3828
|
+
key: "DATABASE_URL",
|
|
3829
|
+
value: `file:${path.join(projectDir, targetApp, "local.db")}`,
|
|
3830
|
+
condition: true
|
|
3831
|
+
}]);
|
|
3832
|
+
await addPackageDependency({
|
|
3833
|
+
dependencies: ["@prisma/adapter-d1"],
|
|
3834
|
+
projectDir: path.join(projectDir, backend === "self" ? "apps/web" : "apps/server")
|
|
3835
|
+
});
|
|
3836
|
+
},
|
|
3837
|
+
catch: (e) => new DatabaseSetupError({
|
|
3838
|
+
provider: "d1",
|
|
3839
|
+
message: `Failed to set up Cloudflare D1: ${e instanceof Error ? e.message : String(e)}`,
|
|
3840
|
+
cause: e
|
|
3841
|
+
})
|
|
3842
|
+
});
|
|
3711
3843
|
}
|
|
3712
3844
|
|
|
3713
3845
|
//#endregion
|
|
@@ -3849,8 +3981,8 @@ async function setupMongoDBAtlas(config, cliInput) {
|
|
|
3849
3981
|
if (ensureDirResult.isErr()) return ensureDirResult;
|
|
3850
3982
|
if (manualDb) {
|
|
3851
3983
|
log.info("MongoDB Atlas manual setup selected");
|
|
3852
|
-
const envResult
|
|
3853
|
-
if (envResult
|
|
3984
|
+
const envResult = await writeEnvFile$3(projectDir, backend);
|
|
3985
|
+
if (envResult.isErr()) return envResult;
|
|
3854
3986
|
displayManualSetupInstructions$3();
|
|
3855
3987
|
return Result.ok(void 0);
|
|
3856
3988
|
}
|
|
@@ -3870,15 +4002,15 @@ async function setupMongoDBAtlas(config, cliInput) {
|
|
|
3870
4002
|
if (isCancel(mode)) return userCancelled("Operation cancelled");
|
|
3871
4003
|
if (mode === "manual") {
|
|
3872
4004
|
log.info("MongoDB Atlas manual setup selected");
|
|
3873
|
-
const envResult
|
|
3874
|
-
if (envResult
|
|
4005
|
+
const envResult = await writeEnvFile$3(projectDir, backend);
|
|
4006
|
+
if (envResult.isErr()) return envResult;
|
|
3875
4007
|
displayManualSetupInstructions$3();
|
|
3876
4008
|
return Result.ok(void 0);
|
|
3877
4009
|
}
|
|
3878
4010
|
const mongoConfigResult = await initMongoDBAtlas(serverDir);
|
|
3879
4011
|
if (mongoConfigResult.isOk()) {
|
|
3880
|
-
const envResult
|
|
3881
|
-
if (envResult
|
|
4012
|
+
const envResult = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
|
|
4013
|
+
if (envResult.isErr()) return envResult;
|
|
3882
4014
|
log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
|
|
3883
4015
|
return Result.ok(void 0);
|
|
3884
4016
|
}
|
|
@@ -4037,8 +4169,8 @@ async function setupNeonPostgres(config, cliInput) {
|
|
|
4037
4169
|
const manualDb = cliInput?.manualDb ?? false;
|
|
4038
4170
|
const target = backend === "self" ? "apps/web" : "apps/server";
|
|
4039
4171
|
if (manualDb) {
|
|
4040
|
-
const envResult
|
|
4041
|
-
if (envResult
|
|
4172
|
+
const envResult = await writeEnvFile$2(projectDir, backend);
|
|
4173
|
+
if (envResult.isErr()) return envResult;
|
|
4042
4174
|
displayManualSetupInstructions$2(target);
|
|
4043
4175
|
return Result.ok(void 0);
|
|
4044
4176
|
}
|
|
@@ -4057,8 +4189,8 @@ async function setupNeonPostgres(config, cliInput) {
|
|
|
4057
4189
|
});
|
|
4058
4190
|
if (isCancel(mode)) return userCancelled("Operation cancelled");
|
|
4059
4191
|
if (mode === "manual") {
|
|
4060
|
-
const envResult
|
|
4061
|
-
if (envResult
|
|
4192
|
+
const envResult = await writeEnvFile$2(projectDir, backend);
|
|
4193
|
+
if (envResult.isErr()) return envResult;
|
|
4062
4194
|
displayManualSetupInstructions$2(target);
|
|
4063
4195
|
return Result.ok(void 0);
|
|
4064
4196
|
}
|
|
@@ -4080,8 +4212,8 @@ async function setupNeonPostgres(config, cliInput) {
|
|
|
4080
4212
|
const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
|
|
4081
4213
|
if (neonDbResult.isErr()) {
|
|
4082
4214
|
log.error(pc.red(neonDbResult.error.message));
|
|
4083
|
-
const envResult
|
|
4084
|
-
if (envResult
|
|
4215
|
+
const envResult = await writeEnvFile$2(projectDir, backend);
|
|
4216
|
+
if (envResult.isErr()) return envResult;
|
|
4085
4217
|
displayManualSetupInstructions$2(target);
|
|
4086
4218
|
} else log.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
|
|
4087
4219
|
return neonDbResult;
|
|
@@ -4102,8 +4234,8 @@ async function setupNeonPostgres(config, cliInput) {
|
|
|
4102
4234
|
const neonConfigResult = await createNeonProject(projectName, regionId, packageManager);
|
|
4103
4235
|
if (neonConfigResult.isErr()) {
|
|
4104
4236
|
log.error(pc.red(neonConfigResult.error.message));
|
|
4105
|
-
const envResult
|
|
4106
|
-
if (envResult
|
|
4237
|
+
const envResult = await writeEnvFile$2(projectDir, backend);
|
|
4238
|
+
if (envResult.isErr()) return envResult;
|
|
4107
4239
|
displayManualSetupInstructions$2(target);
|
|
4108
4240
|
return Result.ok(void 0);
|
|
4109
4241
|
}
|
|
@@ -4123,61 +4255,71 @@ async function setupNeonPostgres(config, cliInput) {
|
|
|
4123
4255
|
//#region src/helpers/database-providers/planetscale-setup.ts
|
|
4124
4256
|
async function setupPlanetScale(config) {
|
|
4125
4257
|
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
|
-
|
|
4258
|
+
if (!["mysql", "postgres"].includes(database)) return Result.ok(void 0);
|
|
4259
|
+
return Result.tryPromise({
|
|
4260
|
+
try: async () => {
|
|
4261
|
+
const targetApp = backend === "self" ? "apps/web" : "apps/server";
|
|
4262
|
+
const envPath = path.join(projectDir, targetApp, ".env");
|
|
4263
|
+
if (database === "mysql" && orm === "drizzle") {
|
|
4264
|
+
const variables = [
|
|
4265
|
+
{
|
|
4266
|
+
key: "DATABASE_URL",
|
|
4267
|
+
value: "mysql://username:password@host/database?ssl={\"rejectUnauthorized\":true}",
|
|
4268
|
+
condition: true
|
|
4269
|
+
},
|
|
4270
|
+
{
|
|
4271
|
+
key: "DATABASE_HOST",
|
|
4272
|
+
value: "",
|
|
4273
|
+
condition: true
|
|
4274
|
+
},
|
|
4275
|
+
{
|
|
4276
|
+
key: "DATABASE_USERNAME",
|
|
4277
|
+
value: "",
|
|
4278
|
+
condition: true
|
|
4279
|
+
},
|
|
4280
|
+
{
|
|
4281
|
+
key: "DATABASE_PASSWORD",
|
|
4282
|
+
value: "",
|
|
4283
|
+
condition: true
|
|
4284
|
+
}
|
|
4285
|
+
];
|
|
4286
|
+
await fs.ensureDir(path.join(projectDir, targetApp));
|
|
4287
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4149
4288
|
}
|
|
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
|
-
|
|
4289
|
+
if (database === "postgres" && orm === "prisma") {
|
|
4290
|
+
const variables = [{
|
|
4291
|
+
key: "DATABASE_URL",
|
|
4292
|
+
value: "postgresql://username:password@host/database?sslaccept=strict",
|
|
4293
|
+
condition: true
|
|
4294
|
+
}];
|
|
4295
|
+
await fs.ensureDir(path.join(projectDir, targetApp));
|
|
4296
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4297
|
+
}
|
|
4298
|
+
if (database === "postgres" && orm === "drizzle") {
|
|
4299
|
+
const variables = [{
|
|
4300
|
+
key: "DATABASE_URL",
|
|
4301
|
+
value: "postgresql://username:password@host/database?sslmode=verify-full",
|
|
4302
|
+
condition: true
|
|
4303
|
+
}];
|
|
4304
|
+
await fs.ensureDir(path.join(projectDir, targetApp));
|
|
4305
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4306
|
+
}
|
|
4307
|
+
if (database === "mysql" && orm === "prisma") {
|
|
4308
|
+
const variables = [{
|
|
4309
|
+
key: "DATABASE_URL",
|
|
4310
|
+
value: "mysql://username:password@host/database?sslaccept=strict",
|
|
4311
|
+
condition: true
|
|
4312
|
+
}];
|
|
4313
|
+
await fs.ensureDir(path.join(projectDir, targetApp));
|
|
4314
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4315
|
+
}
|
|
4316
|
+
},
|
|
4317
|
+
catch: (e) => new DatabaseSetupError({
|
|
4318
|
+
provider: "planetscale",
|
|
4319
|
+
message: `Failed to set up PlanetScale env: ${e instanceof Error ? e.message : String(e)}`,
|
|
4320
|
+
cause: e
|
|
4321
|
+
})
|
|
4322
|
+
});
|
|
4181
4323
|
}
|
|
4182
4324
|
|
|
4183
4325
|
//#endregion
|
|
@@ -4299,8 +4441,8 @@ async function setupPrismaPostgres(config, cliInput) {
|
|
|
4299
4441
|
});
|
|
4300
4442
|
if (ensureDirResult.isErr()) return ensureDirResult;
|
|
4301
4443
|
if (manualDb) {
|
|
4302
|
-
const envResult
|
|
4303
|
-
if (envResult
|
|
4444
|
+
const envResult = await writeEnvFile$1(projectDir, backend);
|
|
4445
|
+
if (envResult.isErr()) return envResult;
|
|
4304
4446
|
displayManualSetupInstructions$1(target);
|
|
4305
4447
|
return Result.ok(void 0);
|
|
4306
4448
|
}
|
|
@@ -4319,8 +4461,8 @@ async function setupPrismaPostgres(config, cliInput) {
|
|
|
4319
4461
|
});
|
|
4320
4462
|
if (isCancel(setupMode)) return userCancelled("Operation cancelled");
|
|
4321
4463
|
if (setupMode === "manual") {
|
|
4322
|
-
const envResult
|
|
4323
|
-
if (envResult
|
|
4464
|
+
const envResult = await writeEnvFile$1(projectDir, backend);
|
|
4465
|
+
if (envResult.isErr()) return envResult;
|
|
4324
4466
|
displayManualSetupInstructions$1(target);
|
|
4325
4467
|
return Result.ok(void 0);
|
|
4326
4468
|
}
|
|
@@ -4328,8 +4470,8 @@ async function setupPrismaPostgres(config, cliInput) {
|
|
|
4328
4470
|
if (prismaConfigResult.isErr()) {
|
|
4329
4471
|
if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
|
|
4330
4472
|
log.error(pc.red(prismaConfigResult.error.message));
|
|
4331
|
-
const envResult
|
|
4332
|
-
if (envResult
|
|
4473
|
+
const envResult = await writeEnvFile$1(projectDir, backend);
|
|
4474
|
+
if (envResult.isErr()) return envResult;
|
|
4333
4475
|
displayManualSetupInstructions$1(target);
|
|
4334
4476
|
log.info("Setup completed with manual configuration required.");
|
|
4335
4477
|
return Result.ok(void 0);
|
|
@@ -4398,9 +4540,9 @@ async function startSupabase(serverDir, packageManager) {
|
|
|
4398
4540
|
const subprocess = execa(supabaseStartArgs[0], supabaseStartArgs.slice(1), { cwd: serverDir });
|
|
4399
4541
|
let stdoutData = "";
|
|
4400
4542
|
if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
|
|
4401
|
-
const text
|
|
4402
|
-
process.stdout.write(text
|
|
4403
|
-
stdoutData += text
|
|
4543
|
+
const text = data.toString();
|
|
4544
|
+
process.stdout.write(text);
|
|
4545
|
+
stdoutData += text;
|
|
4404
4546
|
});
|
|
4405
4547
|
if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
|
|
4406
4548
|
await subprocess;
|
|
@@ -4587,9 +4729,9 @@ async function selectTursoGroup() {
|
|
|
4587
4729
|
}
|
|
4588
4730
|
const selectedGroup = await select({
|
|
4589
4731
|
message: "Select a Turso database group:",
|
|
4590
|
-
options: groups.map((group
|
|
4591
|
-
value: group
|
|
4592
|
-
label: `${group
|
|
4732
|
+
options: groups.map((group) => ({
|
|
4733
|
+
value: group.name,
|
|
4734
|
+
label: `${group.name} (${group.locations})`
|
|
4593
4735
|
}))
|
|
4594
4736
|
});
|
|
4595
4737
|
if (isCancel(selectedGroup)) return userCancelled("Operation cancelled");
|
|
@@ -4767,8 +4909,8 @@ async function setupTurso(config, cliInput) {
|
|
|
4767
4909
|
continue;
|
|
4768
4910
|
}
|
|
4769
4911
|
log.error(pc.red(createResult.error.message));
|
|
4770
|
-
const envResult
|
|
4771
|
-
if (envResult
|
|
4912
|
+
const envResult = await writeEnvFile(projectDir, backend);
|
|
4913
|
+
if (envResult.isErr()) return envResult;
|
|
4772
4914
|
displayManualSetupInstructions();
|
|
4773
4915
|
log.success("Setup completed with manual configuration required.");
|
|
4774
4916
|
return Result.ok(void 0);
|
|
@@ -4793,23 +4935,23 @@ async function setupDatabase(config, cliInput) {
|
|
|
4793
4935
|
}
|
|
4794
4936
|
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
4795
4937
|
if (!await fs.pathExists(dbPackageDir)) return;
|
|
4796
|
-
async function runSetup
|
|
4938
|
+
async function runSetup(setupFn) {
|
|
4797
4939
|
const result = await setupFn();
|
|
4798
4940
|
if (result.isErr()) {
|
|
4799
4941
|
if (UserCancelledError.is(result.error)) throw result.error;
|
|
4800
4942
|
consola.error(pc.red(result.error.message));
|
|
4801
4943
|
}
|
|
4802
4944
|
}
|
|
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);
|
|
4945
|
+
if (dbSetup === "docker") await runSetup(() => setupDockerCompose(config));
|
|
4946
|
+
else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, cliInput));
|
|
4947
|
+
else if (database === "sqlite" && dbSetup === "d1") await runSetup(() => setupCloudflareD1(config));
|
|
4806
4948
|
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
|
|
4949
|
+
if (dbSetup === "prisma-postgres") await runSetup(() => setupPrismaPostgres(config, cliInput));
|
|
4950
|
+
else if (dbSetup === "neon") await runSetup(() => setupNeonPostgres(config, cliInput));
|
|
4951
|
+
else if (dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
|
|
4952
|
+
else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, cliInput));
|
|
4953
|
+
} else if (database === "mysql" && dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
|
|
4954
|
+
else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup(() => setupMongoDBAtlas(config, cliInput));
|
|
4813
4955
|
}
|
|
4814
4956
|
|
|
4815
4957
|
//#endregion
|
|
@@ -5064,9 +5206,9 @@ function getPolarInstructions(backend) {
|
|
|
5064
5206
|
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
|
|
5065
5207
|
const instructions = [];
|
|
5066
5208
|
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`}`);
|
|
5209
|
+
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`}`);
|
|
5210
|
+
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`}`);
|
|
5211
|
+
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
5212
|
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5071
5213
|
}
|
|
5072
5214
|
|
|
@@ -5219,7 +5361,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
5219
5361
|
if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
|
|
5220
5362
|
if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
|
5221
5363
|
let currentPathInput;
|
|
5222
|
-
if (
|
|
5364
|
+
if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
|
|
5365
|
+
else if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
5223
5366
|
else if (input.yes) {
|
|
5224
5367
|
const defaultConfig = getDefaultConfig();
|
|
5225
5368
|
let defaultName = defaultConfig.relativePath;
|
|
@@ -5349,6 +5492,23 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
5349
5492
|
});
|
|
5350
5493
|
});
|
|
5351
5494
|
}
|
|
5495
|
+
function isPathWithinCwd(targetPath) {
|
|
5496
|
+
const resolved = path.resolve(targetPath);
|
|
5497
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
5498
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
5499
|
+
}
|
|
5500
|
+
async function resolveProjectNameForSilent(input) {
|
|
5501
|
+
const defaultConfig = getDefaultConfig();
|
|
5502
|
+
const candidate = (input.projectName?.trim() || void 0) ?? defaultConfig.relativePath;
|
|
5503
|
+
if (candidate === ".") return Result.ok(candidate);
|
|
5504
|
+
const validationResult = validateProjectName(path.basename(candidate));
|
|
5505
|
+
if (validationResult.isErr()) return Result.err(new CLIError({
|
|
5506
|
+
message: validationResult.error.message,
|
|
5507
|
+
cause: validationResult.error
|
|
5508
|
+
}));
|
|
5509
|
+
if (!isPathWithinCwd(candidate)) return Result.err(new CLIError({ message: "Project path must be within current directory" }));
|
|
5510
|
+
return Result.ok(candidate);
|
|
5511
|
+
}
|
|
5352
5512
|
async function handleDirectoryConflictResult(currentPathInput, strategy) {
|
|
5353
5513
|
if (strategy) return handleDirectoryConflictProgrammatically(currentPathInput, strategy);
|
|
5354
5514
|
return Result.tryPromise({
|
|
@@ -5427,32 +5587,32 @@ async function fetchSponsors(url = SPONSORS_JSON_URL) {
|
|
|
5427
5587
|
s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
|
|
5428
5588
|
throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
|
|
5429
5589
|
}
|
|
5430
|
-
const sponsors
|
|
5590
|
+
const sponsors = await response.json();
|
|
5431
5591
|
s.stop("Sponsors fetched successfully!");
|
|
5432
|
-
return sponsors
|
|
5592
|
+
return sponsors;
|
|
5433
5593
|
}
|
|
5434
|
-
function displaySponsors(sponsors
|
|
5435
|
-
const { total_sponsors } = sponsors
|
|
5594
|
+
function displaySponsors(sponsors) {
|
|
5595
|
+
const { total_sponsors } = sponsors.summary;
|
|
5436
5596
|
if (total_sponsors === 0) {
|
|
5437
5597
|
log.info("No sponsors found. You can be the first one! ✨");
|
|
5438
5598
|
outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
|
|
5439
5599
|
return;
|
|
5440
5600
|
}
|
|
5441
|
-
displaySponsorsBox(sponsors
|
|
5442
|
-
if (total_sponsors - sponsors
|
|
5601
|
+
displaySponsorsBox(sponsors);
|
|
5602
|
+
if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
|
|
5443
5603
|
outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
|
|
5444
5604
|
}
|
|
5445
|
-
function displaySponsorsBox(sponsors
|
|
5446
|
-
if (sponsors
|
|
5605
|
+
function displaySponsorsBox(sponsors) {
|
|
5606
|
+
if (sponsors.specialSponsors.length === 0) return;
|
|
5447
5607
|
let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
|
|
5448
|
-
sponsors
|
|
5608
|
+
sponsors.specialSponsors.forEach((sponsor, idx) => {
|
|
5449
5609
|
const displayName = sponsor.name ?? sponsor.githubId;
|
|
5450
5610
|
const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
|
|
5451
5611
|
output += `${pc.green(`• ${displayName}`)}${tier}\n`;
|
|
5452
5612
|
output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
|
|
5453
5613
|
const website = sponsor.websiteUrl ?? sponsor.githubUrl;
|
|
5454
5614
|
if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
|
|
5455
|
-
if (idx < sponsors
|
|
5615
|
+
if (idx < sponsors.specialSponsors.length - 1) output += "\n";
|
|
5456
5616
|
});
|
|
5457
5617
|
consola$1.box(output);
|
|
5458
5618
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "3.19.
|
|
3
|
+
"version": "3.19.4",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-auth",
|
|
@@ -70,12 +70,12 @@
|
|
|
70
70
|
"prepublishOnly": "npm run build"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@better-t-stack/template-generator": "^3.19.
|
|
74
|
-
"@better-t-stack/types": "^3.19.
|
|
75
|
-
"@clack/core": "^0.
|
|
76
|
-
"@clack/prompts": "^1.0.0
|
|
73
|
+
"@better-t-stack/template-generator": "^3.19.4",
|
|
74
|
+
"@better-t-stack/types": "^3.19.4",
|
|
75
|
+
"@clack/core": "^1.0.0",
|
|
76
|
+
"@clack/prompts": "^1.0.0",
|
|
77
77
|
"@orpc/server": "^1.13.4",
|
|
78
|
-
"better-result": "^2.
|
|
78
|
+
"better-result": "^2.7.0",
|
|
79
79
|
"consola": "^3.4.2",
|
|
80
80
|
"env-paths": "^4.0.0",
|
|
81
81
|
"execa": "^9.6.1",
|
|
@@ -83,20 +83,20 @@
|
|
|
83
83
|
"gradient-string": "^3.0.0",
|
|
84
84
|
"handlebars": "^4.7.8",
|
|
85
85
|
"jsonc-parser": "^3.3.1",
|
|
86
|
-
"oxfmt": "^0.
|
|
86
|
+
"oxfmt": "^0.28.0",
|
|
87
87
|
"picocolors": "^1.1.1",
|
|
88
88
|
"tinyglobby": "^0.2.15",
|
|
89
89
|
"trpc-cli": "^0.12.2",
|
|
90
90
|
"ts-morph": "^27.0.2",
|
|
91
91
|
"yaml": "^2.8.2",
|
|
92
|
-
"zod": "^4.3.
|
|
92
|
+
"zod": "^4.3.6"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@types/bun": "^1.3.
|
|
95
|
+
"@types/bun": "^1.3.8",
|
|
96
96
|
"@types/fs-extra": "^11.0.4",
|
|
97
|
-
"@types/node": "^25.0
|
|
98
|
-
"publint": "^0.3.
|
|
99
|
-
"tsdown": "^0.20.
|
|
97
|
+
"@types/node": "^25.2.0",
|
|
98
|
+
"publint": "^0.3.17",
|
|
99
|
+
"tsdown": "^0.20.1",
|
|
100
100
|
"typescript": "^5.9.3"
|
|
101
101
|
}
|
|
102
102
|
}
|
|
File without changes
|