create-better-t-stack 3.36.5 → 3.37.0
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 +6 -10
- package/dist/index.mjs +1 -1
- package/dist/{src-DpdI3cqy.mjs → src-IZv104V2.mjs} +787 -359
- package/package.json +5 -5
|
@@ -5,7 +5,7 @@ import { initTRPC } from "@trpc/server";
|
|
|
5
5
|
import { Result, Result as Result$1, TaggedError } from "better-result";
|
|
6
6
|
import { createCli } from "trpc-cli";
|
|
7
7
|
import z from "zod";
|
|
8
|
-
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
8
|
+
import { box, cancel, confirm, intro, isCancel, limitOptions, log, outro, select, spinner, text } from "@clack/prompts";
|
|
9
9
|
import pc from "picocolors";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import envPaths from "env-paths";
|
|
@@ -22,6 +22,137 @@ import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt,
|
|
|
22
22
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
23
23
|
import os from "node:os";
|
|
24
24
|
import { format } from "oxfmt";
|
|
25
|
+
//#region src/utils/display-config.ts
|
|
26
|
+
const VALUE_LABELS = {
|
|
27
|
+
none: "None",
|
|
28
|
+
"tanstack-router": "TanStack Router",
|
|
29
|
+
"react-router": "React Router",
|
|
30
|
+
"tanstack-start": "TanStack Start",
|
|
31
|
+
next: "Next.js",
|
|
32
|
+
nuxt: "Nuxt",
|
|
33
|
+
svelte: "SvelteKit",
|
|
34
|
+
solid: "SolidStart",
|
|
35
|
+
astro: "Astro",
|
|
36
|
+
"native-bare": "Expo (bare)",
|
|
37
|
+
"native-uniwind": "Expo + Uniwind",
|
|
38
|
+
"native-unistyles": "Expo + Unistyles",
|
|
39
|
+
hono: "Hono",
|
|
40
|
+
express: "Express",
|
|
41
|
+
fastify: "Fastify",
|
|
42
|
+
elysia: "Elysia",
|
|
43
|
+
convex: "Convex",
|
|
44
|
+
self: "Fullstack framework",
|
|
45
|
+
bun: "Bun",
|
|
46
|
+
node: "Node.js",
|
|
47
|
+
workers: "Cloudflare Workers",
|
|
48
|
+
trpc: "tRPC",
|
|
49
|
+
orpc: "oRPC",
|
|
50
|
+
sqlite: "SQLite",
|
|
51
|
+
postgres: "PostgreSQL",
|
|
52
|
+
mysql: "MySQL",
|
|
53
|
+
mongodb: "MongoDB",
|
|
54
|
+
drizzle: "Drizzle",
|
|
55
|
+
prisma: "Prisma",
|
|
56
|
+
mongoose: "Mongoose",
|
|
57
|
+
"better-auth": "Better Auth",
|
|
58
|
+
clerk: "Clerk",
|
|
59
|
+
polar: "Polar",
|
|
60
|
+
pwa: "PWA",
|
|
61
|
+
tauri: "Tauri",
|
|
62
|
+
electrobun: "Electrobun",
|
|
63
|
+
biome: "Biome",
|
|
64
|
+
oxlint: "Oxlint + Oxfmt",
|
|
65
|
+
ultracite: "Ultracite",
|
|
66
|
+
lefthook: "Lefthook",
|
|
67
|
+
husky: "Husky",
|
|
68
|
+
turborepo: "Turborepo",
|
|
69
|
+
nx: "Nx",
|
|
70
|
+
"vite-plus": "Vite+",
|
|
71
|
+
starlight: "Starlight",
|
|
72
|
+
fumadocs: "Fumadocs",
|
|
73
|
+
opentui: "OpenTUI",
|
|
74
|
+
wxt: "WXT",
|
|
75
|
+
skills: "Agent skills",
|
|
76
|
+
mcp: "MCP servers",
|
|
77
|
+
evlog: "evlog",
|
|
78
|
+
todo: "Todo app",
|
|
79
|
+
ai: "AI chat",
|
|
80
|
+
turso: "Turso",
|
|
81
|
+
neon: "Neon",
|
|
82
|
+
planetscale: "PlanetScale",
|
|
83
|
+
supabase: "Supabase",
|
|
84
|
+
"prisma-postgres": "Prisma Postgres",
|
|
85
|
+
"mongodb-atlas": "MongoDB Atlas",
|
|
86
|
+
d1: "Cloudflare D1",
|
|
87
|
+
docker: "Docker",
|
|
88
|
+
cloudflare: "Cloudflare",
|
|
89
|
+
vercel: "Vercel",
|
|
90
|
+
npm: "npm",
|
|
91
|
+
pnpm: "pnpm"
|
|
92
|
+
};
|
|
93
|
+
function formatConfigValue(value) {
|
|
94
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
95
|
+
if (Array.isArray(value)) return value.length > 0 ? value.map(formatConfigValue).join(", ") : "None";
|
|
96
|
+
const text = String(value);
|
|
97
|
+
return VALUE_LABELS[text] ?? text.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
98
|
+
}
|
|
99
|
+
function section(title, entries) {
|
|
100
|
+
const rows = entries.filter(([, value]) => value !== void 0).map(([label, value, format]) => ({
|
|
101
|
+
label,
|
|
102
|
+
value: format === "raw" ? String(value) : formatConfigValue(value)
|
|
103
|
+
}));
|
|
104
|
+
return rows.length > 0 ? {
|
|
105
|
+
title,
|
|
106
|
+
rows
|
|
107
|
+
} : void 0;
|
|
108
|
+
}
|
|
109
|
+
function getConfigSections(config) {
|
|
110
|
+
return [
|
|
111
|
+
section("Project", [[
|
|
112
|
+
"Name",
|
|
113
|
+
config.projectName,
|
|
114
|
+
"raw"
|
|
115
|
+
], [
|
|
116
|
+
"Directory",
|
|
117
|
+
config.relativePath,
|
|
118
|
+
"raw"
|
|
119
|
+
]]),
|
|
120
|
+
section("Application", [
|
|
121
|
+
["Frontend", config.frontend],
|
|
122
|
+
["Backend", config.backend],
|
|
123
|
+
["Runtime", config.runtime],
|
|
124
|
+
["API", config.api]
|
|
125
|
+
]),
|
|
126
|
+
section("Data", [
|
|
127
|
+
["Database", config.database],
|
|
128
|
+
["ORM", config.orm],
|
|
129
|
+
["Setup", config.dbSetup]
|
|
130
|
+
]),
|
|
131
|
+
section("Product", [
|
|
132
|
+
["Auth", config.auth],
|
|
133
|
+
["Payments", config.payments],
|
|
134
|
+
["Addons", config.addons],
|
|
135
|
+
["Examples", config.examples]
|
|
136
|
+
]),
|
|
137
|
+
section("Delivery", [
|
|
138
|
+
["Web deploy", config.webDeploy],
|
|
139
|
+
["Server deploy", config.serverDeploy],
|
|
140
|
+
["Package manager", config.packageManager],
|
|
141
|
+
["Git", config.git],
|
|
142
|
+
["Install deps", config.install]
|
|
143
|
+
])
|
|
144
|
+
].filter((value) => value !== void 0);
|
|
145
|
+
}
|
|
146
|
+
function displayConfig(config) {
|
|
147
|
+
const sections = getConfigSections(config);
|
|
148
|
+
if (sections.length === 0) return pc.yellow("No configuration selected.");
|
|
149
|
+
return sections.map(({ title, rows }) => {
|
|
150
|
+
const labelWidth = Math.max(...rows.map(({ label }) => label.length));
|
|
151
|
+
const renderedRows = rows.map(({ label, value }) => ` ${pc.dim(label.padEnd(labelWidth))} ${value}`).join("\n");
|
|
152
|
+
return `${pc.magenta(pc.bold(title))}\n${renderedRows}`;
|
|
153
|
+
}).join("\n\n");
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
25
156
|
//#region src/utils/get-package-manager.ts
|
|
26
157
|
const getUserPkgManager = () => {
|
|
27
158
|
const userAgent = process.env.npm_config_user_agent;
|
|
@@ -120,6 +251,9 @@ function isFirstPrompt() {
|
|
|
120
251
|
function didLastPromptShowUI() {
|
|
121
252
|
return getContext().navigation.lastPromptShownUI;
|
|
122
253
|
}
|
|
254
|
+
function getPromptProgress() {
|
|
255
|
+
return getContext().navigation.promptProgress;
|
|
256
|
+
}
|
|
123
257
|
function setIsFirstPrompt$1(value) {
|
|
124
258
|
const ctx = tryGetContext();
|
|
125
259
|
if (ctx) ctx.navigation.isFirstPrompt = value;
|
|
@@ -128,6 +262,10 @@ function setLastPromptShownUI(value) {
|
|
|
128
262
|
const ctx = tryGetContext();
|
|
129
263
|
if (ctx) ctx.navigation.lastPromptShownUI = value;
|
|
130
264
|
}
|
|
265
|
+
function setPromptProgress(value) {
|
|
266
|
+
const ctx = tryGetContext();
|
|
267
|
+
if (ctx) ctx.navigation.promptProgress = value;
|
|
268
|
+
}
|
|
131
269
|
async function runWithContextAsync(options, fn) {
|
|
132
270
|
const ctx = {
|
|
133
271
|
navigation: {
|
|
@@ -451,10 +589,10 @@ const renderTitle = () => {
|
|
|
451
589
|
//#region src/commands/history.ts
|
|
452
590
|
function formatStackSummary(entry) {
|
|
453
591
|
const parts = [];
|
|
454
|
-
if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(entry.stack.frontend
|
|
455
|
-
if (entry.stack.backend && entry.stack.backend !== "none") parts.push(entry.stack.backend);
|
|
456
|
-
if (entry.stack.database && entry.stack.database !== "none") parts.push(entry.stack.database);
|
|
457
|
-
if (entry.stack.orm && entry.stack.orm !== "none") parts.push(entry.stack.orm);
|
|
592
|
+
if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(formatConfigValue(entry.stack.frontend));
|
|
593
|
+
if (entry.stack.backend && entry.stack.backend !== "none") parts.push(formatConfigValue(entry.stack.backend));
|
|
594
|
+
if (entry.stack.database && entry.stack.database !== "none") parts.push(formatConfigValue(entry.stack.database));
|
|
595
|
+
if (entry.stack.orm && entry.stack.orm !== "none") parts.push(formatConfigValue(entry.stack.orm));
|
|
458
596
|
return parts.length > 0 ? parts.join(" + ") : "minimal";
|
|
459
597
|
}
|
|
460
598
|
function formatDate(isoString) {
|
|
@@ -466,6 +604,25 @@ function formatDate(isoString) {
|
|
|
466
604
|
minute: "2-digit"
|
|
467
605
|
});
|
|
468
606
|
}
|
|
607
|
+
function formatHistoryEntry(entry, index) {
|
|
608
|
+
const rows = [
|
|
609
|
+
{
|
|
610
|
+
label: "Created",
|
|
611
|
+
value: formatDate(entry.createdAt)
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
label: "Location",
|
|
615
|
+
value: entry.projectDir
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
label: "Stack",
|
|
619
|
+
value: formatStackSummary(entry)
|
|
620
|
+
}
|
|
621
|
+
];
|
|
622
|
+
const labelWidth = Math.max(...rows.map(({ label }) => label.length));
|
|
623
|
+
const details = rows.map(({ label, value }) => `${pc.dim(label.padEnd(labelWidth))} ${value}`).join("\n");
|
|
624
|
+
return `${pc.cyan(pc.bold(`${index + 1}. ${entry.projectName}`))}\n${details}\n${pc.dim("Recreate")}\n${pc.cyan(entry.reproducibleCommand)}`;
|
|
625
|
+
}
|
|
469
626
|
async function historyHandler(input) {
|
|
470
627
|
if (input.clear) {
|
|
471
628
|
const clearResult = await clearHistory();
|
|
@@ -482,28 +639,18 @@ async function historyHandler(input) {
|
|
|
482
639
|
return;
|
|
483
640
|
}
|
|
484
641
|
const entries = historyResult.value;
|
|
485
|
-
if (entries.length === 0) {
|
|
486
|
-
log.info(pc.dim("No projects in history yet."));
|
|
487
|
-
log.info(pc.dim("Create a project with: create-better-t-stack my-app"));
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
642
|
if (input.json) {
|
|
491
643
|
console.log(JSON.stringify(entries, null, 2));
|
|
492
644
|
return;
|
|
493
645
|
}
|
|
494
646
|
renderTitle();
|
|
495
|
-
intro(pc.magenta(`Project
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const stack = pc.dim(formatStackSummary(entry));
|
|
500
|
-
log.message(`${num} ${name}`);
|
|
501
|
-
log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
|
|
502
|
-
log.message(` ${pc.dim("Path:")} ${entry.projectDir}`);
|
|
503
|
-
log.message(` ${pc.dim("Stack:")} ${stack}`);
|
|
504
|
-
log.message(` ${pc.dim("Command:")} ${pc.dim(entry.reproducibleCommand)}`);
|
|
505
|
-
log.message("");
|
|
647
|
+
intro(pc.magenta(`Project history · ${entries.length}`));
|
|
648
|
+
if (entries.length === 0) {
|
|
649
|
+
outro(`${pc.dim("No saved projects yet · create one with")} ${pc.cyan("create-better-t-stack my-app")}`);
|
|
650
|
+
return;
|
|
506
651
|
}
|
|
652
|
+
log.message(entries.map(formatHistoryEntry).join("\n\n"));
|
|
653
|
+
outro(pc.dim("Run a command above to recreate that project"));
|
|
507
654
|
}
|
|
508
655
|
//#endregion
|
|
509
656
|
//#region src/utils/open-url.ts
|
|
@@ -575,27 +722,32 @@ async function fetchSponsorsQuietly({ url = SPONSORS_JSON_URL, timeoutMs = 1500
|
|
|
575
722
|
function displaySponsors(sponsors) {
|
|
576
723
|
const { total_sponsors } = sponsors.summary;
|
|
577
724
|
if (total_sponsors === 0) {
|
|
578
|
-
log.info("No sponsors found
|
|
579
|
-
outro(pc.
|
|
725
|
+
log.info("No sponsors found yet");
|
|
726
|
+
outro(`${pc.dim("Become the first sponsor ·")} ${pc.cyan(GITHUB_SPONSOR_URL)}`);
|
|
580
727
|
return;
|
|
581
728
|
}
|
|
582
729
|
displaySponsorsBox(sponsors);
|
|
583
|
-
if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.
|
|
584
|
-
outro(pc.
|
|
730
|
+
if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.dim(`+${total_sponsors - sponsors.specialSponsors.length} more sponsors`));
|
|
731
|
+
outro(`${pc.dim("Become a sponsor ·")} ${pc.cyan(GITHUB_SPONSOR_URL)}`);
|
|
585
732
|
}
|
|
586
733
|
function displaySponsorsBox(sponsors) {
|
|
587
734
|
if (sponsors.specialSponsors.length === 0) return;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
|
|
594
|
-
const website = sponsor.websiteUrl ?? sponsor.githubUrl;
|
|
595
|
-
if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
|
|
596
|
-
if (idx < sponsors.specialSponsors.length - 1) output += "\n";
|
|
735
|
+
box(formatSpecialSponsorsDetails(sponsors), pc.bold("Special sponsors"), {
|
|
736
|
+
contentPadding: 2,
|
|
737
|
+
formatBorder: pc.dim,
|
|
738
|
+
rounded: true,
|
|
739
|
+
width: "auto"
|
|
597
740
|
});
|
|
598
|
-
|
|
741
|
+
}
|
|
742
|
+
function formatSpecialSponsorsDetails(sponsors) {
|
|
743
|
+
return sponsors.specialSponsors.map((sponsor) => {
|
|
744
|
+
const displayName = sponsor.name ?? sponsor.githubId;
|
|
745
|
+
const tier = sponsor.tierName ? pc.dim(` · ${sponsor.tierName}`) : "";
|
|
746
|
+
const links = [];
|
|
747
|
+
if (sponsor.websiteUrl) links.push(`${pc.dim("Website")} ${pc.cyan(sponsor.websiteUrl)}`);
|
|
748
|
+
links.push(`${pc.dim("GitHub ")} ${pc.cyan(sponsor.githubUrl)}`);
|
|
749
|
+
return `${pc.bold(displayName)}${tier}\n${links.join("\n")}`;
|
|
750
|
+
}).join("\n\n");
|
|
599
751
|
}
|
|
600
752
|
function formatPostInstallSpecialSponsorsSection(sponsors) {
|
|
601
753
|
if (sponsors.specialSponsors.length === 0) return "";
|
|
@@ -658,7 +810,7 @@ async function fetchSponsorsData({ url = SPONSORS_JSON_URL, withSpinner = false,
|
|
|
658
810
|
cause: parseResult.error
|
|
659
811
|
}));
|
|
660
812
|
}
|
|
661
|
-
if (s) s.stop("Sponsors
|
|
813
|
+
if (s) s.stop("Sponsors loaded");
|
|
662
814
|
return Result.ok(parseResult.data);
|
|
663
815
|
} catch (error) {
|
|
664
816
|
const normalizedError = normalizeSponsorFetchError(error);
|
|
@@ -696,7 +848,7 @@ async function openExternalUrl(url, successMessage) {
|
|
|
696
848
|
}
|
|
697
849
|
async function showSponsorsCommand() {
|
|
698
850
|
renderTitle();
|
|
699
|
-
intro(pc.magenta("
|
|
851
|
+
intro(pc.magenta("Sponsors"));
|
|
700
852
|
const sponsorsResult = await fetchSponsors();
|
|
701
853
|
if (sponsorsResult.isErr()) {
|
|
702
854
|
displayError(sponsorsResult.error);
|
|
@@ -984,15 +1136,12 @@ function isGoBack(value) {
|
|
|
984
1136
|
}
|
|
985
1137
|
//#endregion
|
|
986
1138
|
//#region src/prompts/navigable.ts
|
|
987
|
-
/**
|
|
988
|
-
* Navigable prompt wrappers using @clack/core
|
|
989
|
-
* These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
|
|
990
|
-
*/
|
|
991
1139
|
const unicode = process.platform !== "win32";
|
|
992
1140
|
const S_STEP_ACTIVE = unicode ? "◆" : "*";
|
|
993
1141
|
const S_STEP_CANCEL = unicode ? "■" : "x";
|
|
994
1142
|
const S_STEP_ERROR = unicode ? "▲" : "x";
|
|
995
1143
|
const S_STEP_SUBMIT = unicode ? "◇" : "o";
|
|
1144
|
+
const S_STEP_BACK = unicode ? "↶" : "<";
|
|
996
1145
|
const S_BAR = unicode ? "│" : "|";
|
|
997
1146
|
const S_BAR_END = unicode ? "└" : "—";
|
|
998
1147
|
const S_RADIO_ACTIVE = unicode ? "●" : ">";
|
|
@@ -1000,6 +1149,10 @@ const S_RADIO_INACTIVE = unicode ? "○" : " ";
|
|
|
1000
1149
|
const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
|
|
1001
1150
|
const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
|
|
1002
1151
|
const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
|
|
1152
|
+
const promptsNavigatingBack = /* @__PURE__ */ new WeakSet();
|
|
1153
|
+
function keycap(label) {
|
|
1154
|
+
return pc.inverse(` ${label} `);
|
|
1155
|
+
}
|
|
1003
1156
|
function symbol(state) {
|
|
1004
1157
|
switch (state) {
|
|
1005
1158
|
case "initial":
|
|
@@ -1009,10 +1162,10 @@ function symbol(state) {
|
|
|
1009
1162
|
case "submit": return pc.green(S_STEP_SUBMIT);
|
|
1010
1163
|
}
|
|
1011
1164
|
}
|
|
1012
|
-
const KEYBOARD_HINT = pc.dim(`${
|
|
1013
|
-
const KEYBOARD_HINT_FIRST = pc.dim(`${
|
|
1014
|
-
const KEYBOARD_HINT_MULTI = pc.dim(`${
|
|
1015
|
-
const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${
|
|
1165
|
+
const KEYBOARD_HINT = pc.dim(`${keycap("↑↓")} move ${keycap("enter")} choose ${keycap("b")} back ${keycap("^c")} cancel`);
|
|
1166
|
+
const KEYBOARD_HINT_FIRST = pc.dim(`${keycap("↑↓")} move ${keycap("enter")} choose ${keycap("^c")} cancel`);
|
|
1167
|
+
const KEYBOARD_HINT_MULTI = pc.dim(`${keycap("↑↓")} move ${keycap("space")} toggle ${keycap("enter")} choose ${keycap("b")} back ${keycap("^c")} cancel`);
|
|
1168
|
+
const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${keycap("↑↓")} move ${keycap("space")} toggle ${keycap("enter")} choose ${keycap("^c")} cancel`);
|
|
1016
1169
|
const setIsFirstPrompt = setIsFirstPrompt$1;
|
|
1017
1170
|
function getHint() {
|
|
1018
1171
|
return isFirstPrompt() ? KEYBOARD_HINT_FIRST : KEYBOARD_HINT;
|
|
@@ -1020,6 +1173,19 @@ function getHint() {
|
|
|
1020
1173
|
function getMultiHint() {
|
|
1021
1174
|
return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
|
|
1022
1175
|
}
|
|
1176
|
+
function activePromptTitle(message, state = "active") {
|
|
1177
|
+
const progress = getPromptProgress();
|
|
1178
|
+
const eyebrow = progress ? `${pc.magenta(pc.bold(progress.section.toUpperCase()))} ${pc.dim(`· ${progress.current}/${progress.total}`)}` : pc.dim("SETUP");
|
|
1179
|
+
return `${pc.gray(S_BAR)} ${eyebrow}\n${symbol(state)} ${pc.bold(message)}\n`;
|
|
1180
|
+
}
|
|
1181
|
+
function resolvedPrompt(message, value, state) {
|
|
1182
|
+
const promptMessage = state === "cancel" ? pc.strikethrough(pc.dim(message)) : pc.dim(message);
|
|
1183
|
+
return `${symbol(state)} ${promptMessage} ${pc.dim("›")} ${value}`;
|
|
1184
|
+
}
|
|
1185
|
+
function canceledPrompt(prompt, message, value) {
|
|
1186
|
+
if (promptsNavigatingBack.has(prompt)) return `${pc.cyan(S_STEP_BACK)} ${pc.dim(message)}`;
|
|
1187
|
+
return resolvedPrompt(message, value, "cancel");
|
|
1188
|
+
}
|
|
1023
1189
|
function normalizeValidationMessage(validationMessage) {
|
|
1024
1190
|
return validationMessage instanceof Error ? validationMessage.message : validationMessage;
|
|
1025
1191
|
}
|
|
@@ -1028,20 +1194,26 @@ async function runWithNavigation(prompt) {
|
|
|
1028
1194
|
prompt.on("key", (char) => {
|
|
1029
1195
|
if ((char === "b" || char === "B") && !isFirstPrompt()) {
|
|
1030
1196
|
goBack = true;
|
|
1197
|
+
promptsNavigatingBack.add(prompt);
|
|
1031
1198
|
prompt.state = "cancel";
|
|
1032
1199
|
}
|
|
1033
1200
|
});
|
|
1034
1201
|
setLastPromptShownUI(true);
|
|
1035
|
-
|
|
1036
|
-
|
|
1202
|
+
try {
|
|
1203
|
+
const result = await prompt.prompt();
|
|
1204
|
+
return goBack ? GO_BACK_SYMBOL : result;
|
|
1205
|
+
} finally {
|
|
1206
|
+
promptsNavigatingBack.delete(prompt);
|
|
1207
|
+
}
|
|
1037
1208
|
}
|
|
1038
1209
|
async function navigableSelect(opts) {
|
|
1039
1210
|
const opt = (option, state) => {
|
|
1211
|
+
if (!option) return pc.dim("none");
|
|
1040
1212
|
const label = option.label ?? String(option.value);
|
|
1041
1213
|
switch (state) {
|
|
1042
1214
|
case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
|
|
1043
1215
|
case "selected": return `${pc.dim(label)}`;
|
|
1044
|
-
case "active": return `${pc.
|
|
1216
|
+
case "active": return `${pc.cyan(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
1045
1217
|
case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
|
|
1046
1218
|
default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
|
|
1047
1219
|
}
|
|
@@ -1049,15 +1221,25 @@ async function navigableSelect(opts) {
|
|
|
1049
1221
|
return runWithNavigation(new SelectPrompt({
|
|
1050
1222
|
options: opts.options,
|
|
1051
1223
|
initialValue: opts.initialValue,
|
|
1224
|
+
signal: opts.signal,
|
|
1225
|
+
input: opts.input,
|
|
1226
|
+
output: opts.output,
|
|
1052
1227
|
render() {
|
|
1053
|
-
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
1054
1228
|
switch (this.state) {
|
|
1055
|
-
case "submit": return
|
|
1056
|
-
case "cancel": return
|
|
1229
|
+
case "submit": return resolvedPrompt(opts.message, opt(this.options[this.cursor], "selected"), "submit");
|
|
1230
|
+
case "cancel": return canceledPrompt(this, opts.message, opt(this.options[this.cursor], "cancelled"));
|
|
1057
1231
|
default: {
|
|
1058
|
-
const optionsText =
|
|
1059
|
-
|
|
1060
|
-
|
|
1232
|
+
const optionsText = limitOptions({
|
|
1233
|
+
output: opts.output,
|
|
1234
|
+
options: this.options,
|
|
1235
|
+
cursor: this.cursor,
|
|
1236
|
+
maxItems: opts.maxItems,
|
|
1237
|
+
columnPadding: 3,
|
|
1238
|
+
rowPadding: 5,
|
|
1239
|
+
style: (option, active) => opt(option, option.disabled ? "disabled" : active ? "active" : "inactive")
|
|
1240
|
+
}).join(`\n${pc.cyan(S_BAR)} `);
|
|
1241
|
+
const hint = `${pc.gray(S_BAR_END)} ${getHint()}`;
|
|
1242
|
+
return `${activePromptTitle(opts.message)}${pc.cyan(S_BAR)} ${optionsText}\n${hint}\n`;
|
|
1061
1243
|
}
|
|
1062
1244
|
}
|
|
1063
1245
|
}
|
|
@@ -1078,13 +1260,16 @@ async function navigableMultiselect(opts) {
|
|
|
1078
1260
|
return runWithNavigation(new MultiSelectPrompt({
|
|
1079
1261
|
options: opts.options,
|
|
1080
1262
|
initialValues: opts.initialValues,
|
|
1263
|
+
cursorAt: opts.cursorAt,
|
|
1081
1264
|
required,
|
|
1265
|
+
signal: opts.signal,
|
|
1266
|
+
input: opts.input,
|
|
1267
|
+
output: opts.output,
|
|
1082
1268
|
validate(selected) {
|
|
1083
1269
|
if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
|
|
1084
1270
|
return normalizeValidationMessage(opts.validate?.(selected));
|
|
1085
1271
|
},
|
|
1086
1272
|
render() {
|
|
1087
|
-
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
1088
1273
|
const value = this.value ?? [];
|
|
1089
1274
|
const styleOption = (option, active) => {
|
|
1090
1275
|
if (option.disabled) return opt(option, "disabled");
|
|
@@ -1096,21 +1281,37 @@ async function navigableMultiselect(opts) {
|
|
|
1096
1281
|
switch (this.state) {
|
|
1097
1282
|
case "submit": {
|
|
1098
1283
|
const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
|
|
1099
|
-
return
|
|
1284
|
+
return resolvedPrompt(opts.message, submitText, "submit");
|
|
1100
1285
|
}
|
|
1101
1286
|
case "cancel": {
|
|
1102
|
-
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
|
|
1103
|
-
return
|
|
1287
|
+
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", ")) || pc.dim("none");
|
|
1288
|
+
return canceledPrompt(this, opts.message, label);
|
|
1104
1289
|
}
|
|
1105
1290
|
case "error": {
|
|
1106
1291
|
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
|
|
1107
|
-
const optionsText =
|
|
1108
|
-
|
|
1292
|
+
const optionsText = limitOptions({
|
|
1293
|
+
output: opts.output,
|
|
1294
|
+
options: this.options,
|
|
1295
|
+
cursor: this.cursor,
|
|
1296
|
+
maxItems: opts.maxItems,
|
|
1297
|
+
columnPadding: 3,
|
|
1298
|
+
rowPadding: footer.split("\n").length + 4,
|
|
1299
|
+
style: styleOption
|
|
1300
|
+
}).join(`\n${pc.yellow(S_BAR)} `);
|
|
1301
|
+
return `${activePromptTitle(opts.message, "error")}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
|
|
1109
1302
|
}
|
|
1110
1303
|
default: {
|
|
1111
|
-
const optionsText =
|
|
1112
|
-
|
|
1113
|
-
|
|
1304
|
+
const optionsText = limitOptions({
|
|
1305
|
+
output: opts.output,
|
|
1306
|
+
options: this.options,
|
|
1307
|
+
cursor: this.cursor,
|
|
1308
|
+
maxItems: opts.maxItems,
|
|
1309
|
+
columnPadding: 3,
|
|
1310
|
+
rowPadding: 5,
|
|
1311
|
+
style: styleOption
|
|
1312
|
+
}).join(`\n${pc.cyan(S_BAR)} `);
|
|
1313
|
+
const hint = `${pc.gray(S_BAR_END)} ${getMultiHint()}`;
|
|
1314
|
+
return `${activePromptTitle(opts.message)}${pc.cyan(S_BAR)} ${optionsText}\n${hint}\n`;
|
|
1114
1315
|
}
|
|
1115
1316
|
}
|
|
1116
1317
|
}
|
|
@@ -1123,15 +1324,17 @@ async function navigableConfirm(opts) {
|
|
|
1123
1324
|
active,
|
|
1124
1325
|
inactive,
|
|
1125
1326
|
initialValue: opts.initialValue ?? true,
|
|
1327
|
+
signal: opts.signal,
|
|
1328
|
+
input: opts.input,
|
|
1329
|
+
output: opts.output,
|
|
1126
1330
|
render() {
|
|
1127
|
-
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
1128
1331
|
const value = this.value ? active : inactive;
|
|
1129
1332
|
switch (this.state) {
|
|
1130
|
-
case "submit": return
|
|
1131
|
-
case "cancel": return
|
|
1333
|
+
case "submit": return resolvedPrompt(opts.message, pc.dim(value), "submit");
|
|
1334
|
+
case "cancel": return canceledPrompt(this, opts.message, pc.strikethrough(pc.dim(value)));
|
|
1132
1335
|
default: {
|
|
1133
|
-
const hint =
|
|
1134
|
-
return `${
|
|
1336
|
+
const hint = `${pc.gray(S_BAR_END)} ${getHint()}`;
|
|
1337
|
+
return `${activePromptTitle(opts.message)}${pc.cyan(S_BAR)} ${this.value ? `${pc.cyan(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.cyan(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${hint}\n`;
|
|
1135
1338
|
}
|
|
1136
1339
|
}
|
|
1137
1340
|
}
|
|
@@ -1161,52 +1364,59 @@ async function navigableGroupMultiselect(opts) {
|
|
|
1161
1364
|
return runWithNavigation(new GroupMultiSelectPrompt({
|
|
1162
1365
|
options: opts.options,
|
|
1163
1366
|
initialValues: opts.initialValues,
|
|
1367
|
+
cursorAt: opts.cursorAt,
|
|
1164
1368
|
required,
|
|
1165
1369
|
selectableGroups: true,
|
|
1370
|
+
signal: opts.signal,
|
|
1371
|
+
input: opts.input,
|
|
1372
|
+
output: opts.output,
|
|
1166
1373
|
validate(selected) {
|
|
1167
1374
|
if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
|
|
1168
1375
|
return normalizeValidationMessage(opts.validate?.(selected));
|
|
1169
1376
|
},
|
|
1170
1377
|
render() {
|
|
1171
|
-
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
1172
1378
|
const value = this.value ?? [];
|
|
1379
|
+
const styleOption = (option, active) => {
|
|
1380
|
+
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
|
|
1381
|
+
if (!active && typeof option.group === "string" && this.options[this.cursor]?.value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", this.options);
|
|
1382
|
+
if (active && selected) return opt(option, "active-selected", this.options);
|
|
1383
|
+
if (selected) return opt(option, "selected", this.options);
|
|
1384
|
+
return opt(option, active ? "active" : "inactive", this.options);
|
|
1385
|
+
};
|
|
1173
1386
|
switch (this.state) {
|
|
1174
1387
|
case "submit": {
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
return `${title}${pc.gray(S_BAR)}${optionsText}`;
|
|
1388
|
+
const optionsText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
|
|
1389
|
+
return resolvedPrompt(opts.message, optionsText, "submit");
|
|
1178
1390
|
}
|
|
1179
1391
|
case "cancel": {
|
|
1180
|
-
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
|
|
1181
|
-
return
|
|
1392
|
+
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", ")) || pc.dim("none");
|
|
1393
|
+
return canceledPrompt(this, opts.message, label);
|
|
1182
1394
|
}
|
|
1183
1395
|
case "error": {
|
|
1184
1396
|
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
|
|
1185
|
-
const optionsText =
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1397
|
+
const optionsText = limitOptions({
|
|
1398
|
+
output: opts.output,
|
|
1399
|
+
options: this.options,
|
|
1400
|
+
cursor: this.cursor,
|
|
1401
|
+
maxItems: opts.maxItems,
|
|
1402
|
+
columnPadding: 3,
|
|
1403
|
+
rowPadding: footer.split("\n").length + 4,
|
|
1404
|
+
style: styleOption
|
|
1192
1405
|
}).join(`\n${pc.yellow(S_BAR)} `);
|
|
1193
|
-
return `${
|
|
1406
|
+
return `${activePromptTitle(opts.message, "error")}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
|
|
1194
1407
|
}
|
|
1195
1408
|
default: {
|
|
1196
|
-
const optionsText =
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
|
|
1208
|
-
const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
|
|
1209
|
-
return `${title}${pc.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
|
|
1409
|
+
const optionsText = limitOptions({
|
|
1410
|
+
output: opts.output,
|
|
1411
|
+
options: this.options,
|
|
1412
|
+
cursor: this.cursor,
|
|
1413
|
+
maxItems: opts.maxItems,
|
|
1414
|
+
columnPadding: 3,
|
|
1415
|
+
rowPadding: 5,
|
|
1416
|
+
style: styleOption
|
|
1417
|
+
}).join(`\n${pc.cyan(S_BAR)} `);
|
|
1418
|
+
const hint = `${pc.gray(S_BAR_END)} ${getMultiHint()}`;
|
|
1419
|
+
return `${activePromptTitle(opts.message)}${pc.cyan(S_BAR)} ${optionsText}\n${hint}\n`;
|
|
1210
1420
|
}
|
|
1211
1421
|
}
|
|
1212
1422
|
}
|
|
@@ -1372,7 +1582,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previo
|
|
|
1372
1582
|
}
|
|
1373
1583
|
sortAndPruneGroupedOptions(groupedOptions);
|
|
1374
1584
|
const response = await navigableGroupMultiselect({
|
|
1375
|
-
message: "
|
|
1585
|
+
message: "Pick addons",
|
|
1376
1586
|
options: groupedOptions,
|
|
1377
1587
|
initialValues: (previousValue ?? DEFAULT_CONFIG.addons).filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
|
|
1378
1588
|
required: false,
|
|
@@ -2037,45 +2247,56 @@ async function navigableGroup(prompts, opts) {
|
|
|
2037
2247
|
delete results[prevName];
|
|
2038
2248
|
currentIndex--;
|
|
2039
2249
|
};
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2250
|
+
try {
|
|
2251
|
+
while (currentIndex < promptNames.length) {
|
|
2252
|
+
const name = promptNames[currentIndex];
|
|
2253
|
+
const prompt = prompts[name];
|
|
2254
|
+
const section = opts?.sections?.find(({ prompts: sectionPrompts }) => sectionPrompts.includes(name));
|
|
2255
|
+
const sectionPromptIndex = section?.prompts.indexOf(name) ?? -1;
|
|
2256
|
+
setPromptProgress({
|
|
2257
|
+
current: currentIndex + 1,
|
|
2258
|
+
total: promptNames.length,
|
|
2259
|
+
section: section?.label ?? "Setup",
|
|
2260
|
+
sectionCurrent: sectionPromptIndex + 1,
|
|
2261
|
+
sectionTotal: section?.prompts.length ?? promptNames.length
|
|
2262
|
+
});
|
|
2263
|
+
setIsFirstPrompt$1(currentIndex === 0);
|
|
2264
|
+
setLastPromptShownUI(false);
|
|
2265
|
+
const presetResult = opts?.preselected?.[name];
|
|
2266
|
+
const result = presetResult !== void 0 ? presetResult : await prompt({
|
|
2267
|
+
results,
|
|
2268
|
+
previousAnswer: previousAnswers[name]
|
|
2269
|
+
});
|
|
2270
|
+
if (isGoBack(result)) {
|
|
2271
|
+
goingBack = true;
|
|
2272
|
+
if (currentIndex > 0) {
|
|
2273
|
+
stepBack();
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
goingBack = false;
|
|
2055
2277
|
continue;
|
|
2056
2278
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
results
|
|
2063
|
-
opts.onCancel({ results });
|
|
2279
|
+
if (isCancel$1(result)) {
|
|
2280
|
+
if (typeof opts?.onCancel === "function") {
|
|
2281
|
+
results[name] = "canceled";
|
|
2282
|
+
opts.onCancel({ results });
|
|
2283
|
+
}
|
|
2284
|
+
return results;
|
|
2064
2285
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
stepBack();
|
|
2071
|
-
continue;
|
|
2286
|
+
if (goingBack && !didLastPromptShowUI()) {
|
|
2287
|
+
if (currentIndex > 0) {
|
|
2288
|
+
stepBack();
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2072
2291
|
}
|
|
2292
|
+
goingBack = false;
|
|
2293
|
+
results[name] = result;
|
|
2294
|
+
currentIndex++;
|
|
2073
2295
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2296
|
+
} finally {
|
|
2297
|
+
setIsFirstPrompt$1(false);
|
|
2298
|
+
setPromptProgress(void 0);
|
|
2077
2299
|
}
|
|
2078
|
-
setIsFirstPrompt$1(false);
|
|
2079
2300
|
return results;
|
|
2080
2301
|
}
|
|
2081
2302
|
//#endregion
|
|
@@ -3846,7 +4067,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
3846
4067
|
cause: e
|
|
3847
4068
|
})
|
|
3848
4069
|
});
|
|
3849
|
-
if (result.isOk()) s.stop("Dependencies installed
|
|
4070
|
+
if (result.isOk()) s.stop("Dependencies installed");
|
|
3850
4071
|
else s.stop(pc.red("Failed to install dependencies"));
|
|
3851
4072
|
return result;
|
|
3852
4073
|
}
|
|
@@ -3951,7 +4172,7 @@ async function addHandlerInternal(input) {
|
|
|
3951
4172
|
}));
|
|
3952
4173
|
if (!isSilent()) {
|
|
3953
4174
|
renderTitle();
|
|
3954
|
-
intro(pc.magenta("Add
|
|
4175
|
+
intro(pc.magenta("Add to your project"));
|
|
3955
4176
|
}
|
|
3956
4177
|
const existingConfig = await detectProjectConfig(projectDir);
|
|
3957
4178
|
if (!existingConfig) return Result.err(new CLIError({ message: `No Better-T-Stack project found in ${projectDir}. Make sure bts.jsonc exists.` }));
|
|
@@ -3960,7 +4181,10 @@ async function addHandlerInternal(input) {
|
|
|
3960
4181
|
if (input.addons && input.addons.length > 0) {
|
|
3961
4182
|
addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingConfig.addons.includes(addon));
|
|
3962
4183
|
if (addonsToAdd.length === 0) {
|
|
3963
|
-
if (!isSilent())
|
|
4184
|
+
if (!isSilent()) {
|
|
4185
|
+
log.warn(pc.yellow("Nothing to add — those addons are already installed"));
|
|
4186
|
+
outro(pc.dim("Project unchanged"));
|
|
4187
|
+
}
|
|
3964
4188
|
return Result.ok({
|
|
3965
4189
|
success: true,
|
|
3966
4190
|
addedAddons: [],
|
|
@@ -3982,10 +4206,7 @@ async function addHandlerInternal(input) {
|
|
|
3982
4206
|
if (promptResult.isErr()) return Result.err(promptResult.error);
|
|
3983
4207
|
const selectedAddons = promptResult.value;
|
|
3984
4208
|
if (selectedAddons.length === 0) {
|
|
3985
|
-
if (!isSilent())
|
|
3986
|
-
log.info(pc.dim("No addons selected."));
|
|
3987
|
-
outro(pc.magenta("Nothing to add."));
|
|
3988
|
-
}
|
|
4209
|
+
if (!isSilent()) outro(pc.dim("Nothing selected · project unchanged"));
|
|
3989
4210
|
return Result.ok({
|
|
3990
4211
|
success: true,
|
|
3991
4212
|
addedAddons: [],
|
|
@@ -3997,7 +4218,7 @@ async function addHandlerInternal(input) {
|
|
|
3997
4218
|
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3998
4219
|
const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
|
|
3999
4220
|
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
4000
|
-
if (!isSilent()) log.info(pc.
|
|
4221
|
+
if (!isSilent()) log.info(`${pc.dim("Adding")} ${pc.cyan(formatConfigValue(addonsToAdd))}`);
|
|
4001
4222
|
const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
|
|
4002
4223
|
const config = {
|
|
4003
4224
|
projectName: existingConfig.projectName,
|
|
@@ -4025,7 +4246,7 @@ async function addHandlerInternal(input) {
|
|
|
4025
4246
|
...config,
|
|
4026
4247
|
addons: updatedAddons
|
|
4027
4248
|
};
|
|
4028
|
-
if (!isSilent()) log.info(pc.dim("
|
|
4249
|
+
if (!isSilent()) log.info(pc.dim("Preparing addon files…"));
|
|
4029
4250
|
const vfs = new VirtualFileSystem();
|
|
4030
4251
|
for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
|
|
4031
4252
|
const fullPath = path.join(projectDir, pkgPath);
|
|
@@ -4057,9 +4278,9 @@ async function addHandlerInternal(input) {
|
|
|
4057
4278
|
};
|
|
4058
4279
|
if (input.dryRun) {
|
|
4059
4280
|
if (!isSilent()) {
|
|
4060
|
-
log.success(pc.green("Dry run
|
|
4061
|
-
log.
|
|
4062
|
-
outro(pc.
|
|
4281
|
+
log.success(pc.green("Dry run passed · no files written"));
|
|
4282
|
+
log.message(pc.dim(`${vfs.getFileCount()} addon files planned`));
|
|
4283
|
+
outro(pc.dim("Project unchanged"));
|
|
4063
4284
|
}
|
|
4064
4285
|
return Result.ok({
|
|
4065
4286
|
success: true,
|
|
@@ -4090,17 +4311,17 @@ async function addHandlerInternal(input) {
|
|
|
4090
4311
|
addons: updatedAddons,
|
|
4091
4312
|
addonOptions: updatedConfig.addonOptions
|
|
4092
4313
|
});
|
|
4093
|
-
if (input.install) {
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
packageManager: config.packageManager
|
|
4098
|
-
});
|
|
4099
|
-
}
|
|
4314
|
+
if (input.install) await installDependencies({
|
|
4315
|
+
projectDir,
|
|
4316
|
+
packageManager: config.packageManager
|
|
4317
|
+
});
|
|
4100
4318
|
if (!isSilent()) {
|
|
4101
|
-
log.success(pc.green(`
|
|
4102
|
-
if (!input.install)
|
|
4103
|
-
|
|
4319
|
+
log.success(pc.green(`Added ${formatConfigValue(addonsToAdd)}`));
|
|
4320
|
+
if (!input.install) {
|
|
4321
|
+
const installCommand = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
4322
|
+
log.message(`${pc.dim("Next step")}\n${pc.cyan(installCommand)}`);
|
|
4323
|
+
}
|
|
4324
|
+
outro(pc.magenta("Project updated"));
|
|
4104
4325
|
}
|
|
4105
4326
|
return Result.ok({
|
|
4106
4327
|
success: true,
|
|
@@ -4133,7 +4354,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
|
|
|
4133
4354
|
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
4134
4355
|
});
|
|
4135
4356
|
const apiType = await navigableSelect({
|
|
4136
|
-
message: "
|
|
4357
|
+
message: "Choose an API layer",
|
|
4137
4358
|
options: apiOptions,
|
|
4138
4359
|
initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
|
|
4139
4360
|
});
|
|
@@ -4185,7 +4406,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
|
|
|
4185
4406
|
}
|
|
4186
4407
|
});
|
|
4187
4408
|
const response = await navigableSelect({
|
|
4188
|
-
message: "
|
|
4409
|
+
message: "Choose authentication",
|
|
4189
4410
|
options,
|
|
4190
4411
|
initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
|
|
4191
4412
|
});
|
|
@@ -4239,7 +4460,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
|
|
|
4239
4460
|
hint: "No backend server"
|
|
4240
4461
|
});
|
|
4241
4462
|
const response = await navigableSelect({
|
|
4242
|
-
message: "
|
|
4463
|
+
message: "Choose a backend",
|
|
4243
4464
|
options: backendOptions,
|
|
4244
4465
|
initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
|
|
4245
4466
|
});
|
|
@@ -4279,7 +4500,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
|
|
|
4279
4500
|
hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
|
|
4280
4501
|
});
|
|
4281
4502
|
const response = await navigableSelect({
|
|
4282
|
-
message: "
|
|
4503
|
+
message: "Choose a database",
|
|
4283
4504
|
options: databaseOptions,
|
|
4284
4505
|
initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
|
|
4285
4506
|
});
|
|
@@ -4378,7 +4599,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
|
|
|
4378
4599
|
];
|
|
4379
4600
|
else return "none";
|
|
4380
4601
|
const response = await navigableSelect({
|
|
4381
|
-
message: `
|
|
4602
|
+
message: `Choose a ${databaseType} setup`,
|
|
4382
4603
|
options,
|
|
4383
4604
|
initialValue: preferValidInitial(options, previousValue, "none")
|
|
4384
4605
|
});
|
|
@@ -4404,7 +4625,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
|
|
|
4404
4625
|
});
|
|
4405
4626
|
if (options.length === 0) return [];
|
|
4406
4627
|
response = await navigableMultiselect({
|
|
4407
|
-
message: "Include examples",
|
|
4628
|
+
message: "Include starter examples?",
|
|
4408
4629
|
options,
|
|
4409
4630
|
required: false,
|
|
4410
4631
|
initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
|
|
@@ -4431,7 +4652,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4431
4652
|
while (true) {
|
|
4432
4653
|
const wasFirstPrompt = isFirstPrompt();
|
|
4433
4654
|
const frontendTypes = await navigableMultiselect({
|
|
4434
|
-
message: "
|
|
4655
|
+
message: "What are you building?",
|
|
4435
4656
|
options: [{
|
|
4436
4657
|
value: "web",
|
|
4437
4658
|
label: "Web",
|
|
@@ -4493,7 +4714,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4493
4714
|
}
|
|
4494
4715
|
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
|
|
4495
4716
|
const webFramework = await navigableSelect({
|
|
4496
|
-
message: "Choose web",
|
|
4717
|
+
message: "Choose a web framework",
|
|
4497
4718
|
options: webOptions,
|
|
4498
4719
|
initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
|
|
4499
4720
|
});
|
|
@@ -4507,7 +4728,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4507
4728
|
}
|
|
4508
4729
|
if (frontendTypes.includes("native")) {
|
|
4509
4730
|
const nativeFramework = await navigableSelect({
|
|
4510
|
-
message: "Choose native",
|
|
4731
|
+
message: "Choose a native setup",
|
|
4511
4732
|
options: [
|
|
4512
4733
|
{
|
|
4513
4734
|
value: "native-bare",
|
|
@@ -4547,7 +4768,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4547
4768
|
async function getGitChoice(git, previousValue) {
|
|
4548
4769
|
if (git !== void 0) return git;
|
|
4549
4770
|
const response = await navigableConfirm({
|
|
4550
|
-
message: "Initialize
|
|
4771
|
+
message: "Initialize a Git repository?",
|
|
4551
4772
|
initialValue: previousValue ?? DEFAULT_CONFIG.git
|
|
4552
4773
|
});
|
|
4553
4774
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
@@ -4799,7 +5020,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
|
|
|
4799
5020
|
}
|
|
4800
5021
|
const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
|
|
4801
5022
|
const response = await navigableSelect({
|
|
4802
|
-
message: "
|
|
5023
|
+
message: "Choose an ORM",
|
|
4803
5024
|
options,
|
|
4804
5025
|
initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
|
|
4805
5026
|
});
|
|
@@ -4851,7 +5072,7 @@ async function getPaymentsChoice(payments, auth, backend, _frontends, previousVa
|
|
|
4851
5072
|
hint: "No payments integration"
|
|
4852
5073
|
}];
|
|
4853
5074
|
const response = await navigableSelect({
|
|
4854
|
-
message: "
|
|
5075
|
+
message: "Add payments?",
|
|
4855
5076
|
options,
|
|
4856
5077
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
|
|
4857
5078
|
});
|
|
@@ -4878,7 +5099,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
|
|
|
4878
5099
|
hint: "Edge runtime on Cloudflare's global network"
|
|
4879
5100
|
});
|
|
4880
5101
|
const response = await navigableSelect({
|
|
4881
|
-
message: "
|
|
5102
|
+
message: "Choose a runtime",
|
|
4882
5103
|
options: runtimeOptions,
|
|
4883
5104
|
initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
|
|
4884
5105
|
});
|
|
@@ -4932,7 +5153,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
|
|
|
4932
5153
|
};
|
|
4933
5154
|
});
|
|
4934
5155
|
const response = await navigableSelect({
|
|
4935
|
-
message: "
|
|
5156
|
+
message: "Choose server deployment",
|
|
4936
5157
|
options,
|
|
4937
5158
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
|
|
4938
5159
|
});
|
|
@@ -4980,7 +5201,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
|
|
|
4980
5201
|
};
|
|
4981
5202
|
});
|
|
4982
5203
|
const response = await navigableSelect({
|
|
4983
|
-
message: "
|
|
5204
|
+
message: "Choose web deployment",
|
|
4984
5205
|
options,
|
|
4985
5206
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
|
|
4986
5207
|
});
|
|
@@ -4989,7 +5210,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
|
|
|
4989
5210
|
}
|
|
4990
5211
|
//#endregion
|
|
4991
5212
|
//#region src/prompts/config-prompts.ts
|
|
4992
|
-
async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
5213
|
+
async function gatherConfig(flags, projectName, projectDir, relativePath, options = {}) {
|
|
4993
5214
|
if (isSilent()) return {
|
|
4994
5215
|
projectName,
|
|
4995
5216
|
projectDir,
|
|
@@ -5017,22 +5238,63 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
5017
5238
|
frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
|
|
5018
5239
|
backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
|
|
5019
5240
|
runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
|
|
5241
|
+
api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
|
|
5020
5242
|
database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
|
|
5021
5243
|
orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
|
|
5022
|
-
|
|
5244
|
+
dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
|
|
5023
5245
|
auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
|
|
5024
5246
|
payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
|
|
5025
5247
|
addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
|
|
5026
5248
|
examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
|
|
5027
|
-
dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
|
|
5028
5249
|
webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
|
|
5029
5250
|
serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
|
|
5030
5251
|
git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
|
|
5031
5252
|
packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
|
|
5032
5253
|
install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
|
|
5033
|
-
}, {
|
|
5034
|
-
|
|
5035
|
-
|
|
5254
|
+
}, {
|
|
5255
|
+
preselected: options.skipCompatibilityChecks ? flags : void 0,
|
|
5256
|
+
sections: [
|
|
5257
|
+
{
|
|
5258
|
+
label: "App",
|
|
5259
|
+
prompts: [
|
|
5260
|
+
"frontend",
|
|
5261
|
+
"backend",
|
|
5262
|
+
"runtime",
|
|
5263
|
+
"api"
|
|
5264
|
+
]
|
|
5265
|
+
},
|
|
5266
|
+
{
|
|
5267
|
+
label: "Data",
|
|
5268
|
+
prompts: [
|
|
5269
|
+
"database",
|
|
5270
|
+
"orm",
|
|
5271
|
+
"dbSetup"
|
|
5272
|
+
]
|
|
5273
|
+
},
|
|
5274
|
+
{
|
|
5275
|
+
label: "Product",
|
|
5276
|
+
prompts: [
|
|
5277
|
+
"auth",
|
|
5278
|
+
"payments",
|
|
5279
|
+
"addons",
|
|
5280
|
+
"examples"
|
|
5281
|
+
]
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
label: "Ship",
|
|
5285
|
+
prompts: [
|
|
5286
|
+
"webDeploy",
|
|
5287
|
+
"serverDeploy",
|
|
5288
|
+
"git",
|
|
5289
|
+
"packageManager",
|
|
5290
|
+
"install"
|
|
5291
|
+
]
|
|
5292
|
+
}
|
|
5293
|
+
],
|
|
5294
|
+
onCancel: () => {
|
|
5295
|
+
throw new UserCancelledError({ message: "Operation cancelled" });
|
|
5296
|
+
}
|
|
5297
|
+
});
|
|
5036
5298
|
return {
|
|
5037
5299
|
projectName,
|
|
5038
5300
|
projectDir,
|
|
@@ -5081,13 +5343,22 @@ async function getProjectName(initialName) {
|
|
|
5081
5343
|
let projectPath = "";
|
|
5082
5344
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
5083
5345
|
let counter = 1;
|
|
5084
|
-
while (
|
|
5346
|
+
while (true) {
|
|
5347
|
+
const defaultPath = path.resolve(process.cwd(), defaultName);
|
|
5348
|
+
let stats;
|
|
5349
|
+
try {
|
|
5350
|
+
stats = await fs.lstat(defaultPath);
|
|
5351
|
+
} catch (error) {
|
|
5352
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
|
|
5353
|
+
throw error;
|
|
5354
|
+
}
|
|
5355
|
+
if (stats.isDirectory() && (await fs.readdir(defaultPath)).length === 0) break;
|
|
5085
5356
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
5086
5357
|
counter++;
|
|
5087
5358
|
}
|
|
5088
5359
|
while (!isValid) {
|
|
5089
5360
|
const response = await text({
|
|
5090
|
-
message: "
|
|
5361
|
+
message: "Where should we create your project?",
|
|
5091
5362
|
placeholder: defaultName,
|
|
5092
5363
|
initialValue: initialName,
|
|
5093
5364
|
defaultValue: defaultName,
|
|
@@ -5148,90 +5419,83 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
5148
5419
|
});
|
|
5149
5420
|
}
|
|
5150
5421
|
//#endregion
|
|
5151
|
-
//#region src/utils/
|
|
5152
|
-
function
|
|
5153
|
-
const
|
|
5154
|
-
|
|
5155
|
-
if (config.frontend !== void 0) {
|
|
5156
|
-
const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
|
|
5157
|
-
const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
|
|
5158
|
-
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
|
5159
|
-
}
|
|
5160
|
-
if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
|
|
5161
|
-
if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
|
|
5162
|
-
if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
|
|
5163
|
-
if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
|
|
5164
|
-
if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
|
|
5165
|
-
if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
|
5166
|
-
if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
|
|
5167
|
-
if (config.addons !== void 0) {
|
|
5168
|
-
const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
|
|
5169
|
-
const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
|
|
5170
|
-
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
|
|
5171
|
-
}
|
|
5172
|
-
if (config.examples !== void 0) {
|
|
5173
|
-
const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
|
|
5174
|
-
const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
|
|
5175
|
-
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
|
|
5176
|
-
}
|
|
5177
|
-
if (config.git !== void 0) {
|
|
5178
|
-
const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
|
|
5179
|
-
configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
|
|
5180
|
-
}
|
|
5181
|
-
if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
|
|
5182
|
-
if (config.install !== void 0) {
|
|
5183
|
-
const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
|
|
5184
|
-
configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
|
|
5185
|
-
}
|
|
5186
|
-
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
5187
|
-
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
5188
|
-
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
5189
|
-
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
5190
|
-
return configDisplay.join("\n");
|
|
5422
|
+
//#region src/utils/cli-invocation.ts
|
|
5423
|
+
function getCliSubcommandCommand(subcommand, fallbackPackageManager, userAgent = process.env.npm_config_user_agent) {
|
|
5424
|
+
const normalizedUserAgent = userAgent?.toLowerCase();
|
|
5425
|
+
return getPackageExecutionCommand(normalizedUserAgent?.startsWith("bun") ? "bun" : normalizedUserAgent?.startsWith("pnpm") ? "pnpm" : normalizedUserAgent?.startsWith("npm") ? "npm" : fallbackPackageManager, `create-better-t-stack@latest ${subcommand}`);
|
|
5191
5426
|
}
|
|
5192
5427
|
//#endregion
|
|
5193
5428
|
//#region src/utils/project-directory.ts
|
|
5194
5429
|
async function handleDirectoryConflict(currentPathInput) {
|
|
5195
5430
|
while (true) {
|
|
5196
|
-
const
|
|
5197
|
-
if (
|
|
5431
|
+
const pathStateResult = await inspectProjectPath(path.resolve(process.cwd(), currentPathInput));
|
|
5432
|
+
if (pathStateResult.isErr()) throw pathStateResult.error;
|
|
5433
|
+
const pathState = pathStateResult.value;
|
|
5434
|
+
if (pathState === "missing" || pathState === "empty-directory") return {
|
|
5198
5435
|
finalPathInput: currentPathInput,
|
|
5199
5436
|
shouldClearDirectory: false
|
|
5200
5437
|
};
|
|
5201
|
-
if (isSilent()) throw new CLIError({ message: `
|
|
5202
|
-
log.warn(`
|
|
5438
|
+
if (isSilent()) throw new CLIError({ message: `Project path "${currentPathInput}" is unavailable. In silent mode, provide a different project path or an explicit directoryConflict strategy.` });
|
|
5439
|
+
if (pathState === "symbolic-link") log.warn(`Project path "${pc.yellow(currentPathInput)}" is a symbolic link.`);
|
|
5440
|
+
else if (pathState === "non-directory") log.warn(`Project path "${pc.yellow(currentPathInput)}" exists and is not a directory.`);
|
|
5441
|
+
else log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
|
|
5442
|
+
let incrementedPath;
|
|
5443
|
+
if (currentPathInput !== ".") {
|
|
5444
|
+
const incrementResult = await findAvailableIncrementedPath(currentPathInput);
|
|
5445
|
+
if (incrementResult.isErr()) throw incrementResult.error;
|
|
5446
|
+
incrementedPath = incrementResult.value;
|
|
5447
|
+
}
|
|
5448
|
+
const options = [];
|
|
5449
|
+
if (incrementedPath) options.push({
|
|
5450
|
+
value: "increment",
|
|
5451
|
+
label: `Create as "${incrementedPath}"`,
|
|
5452
|
+
hint: "Keep the existing path untouched"
|
|
5453
|
+
});
|
|
5454
|
+
options.push({
|
|
5455
|
+
value: "rename",
|
|
5456
|
+
label: "Choose another path",
|
|
5457
|
+
hint: "Enter a different project directory"
|
|
5458
|
+
});
|
|
5459
|
+
if (pathState === "non-empty-directory") options.push({
|
|
5460
|
+
value: "merge",
|
|
5461
|
+
label: "Merge into this directory",
|
|
5462
|
+
hint: "Keep unrelated files; replace conflicts"
|
|
5463
|
+
}, {
|
|
5464
|
+
value: "overwrite",
|
|
5465
|
+
label: "Delete and overwrite",
|
|
5466
|
+
hint: "Permanently remove existing contents"
|
|
5467
|
+
});
|
|
5468
|
+
options.push({
|
|
5469
|
+
value: "cancel",
|
|
5470
|
+
label: "Cancel",
|
|
5471
|
+
hint: "Leave everything unchanged"
|
|
5472
|
+
});
|
|
5203
5473
|
const action = await select({
|
|
5204
|
-
message: "
|
|
5205
|
-
options
|
|
5206
|
-
|
|
5207
|
-
value: "overwrite",
|
|
5208
|
-
label: "Overwrite",
|
|
5209
|
-
hint: "Empty the directory and create the project"
|
|
5210
|
-
},
|
|
5211
|
-
{
|
|
5212
|
-
value: "merge",
|
|
5213
|
-
label: "Merge",
|
|
5214
|
-
hint: "Create project files inside, potentially overwriting conflicts"
|
|
5215
|
-
},
|
|
5216
|
-
{
|
|
5217
|
-
value: "rename",
|
|
5218
|
-
label: "Choose a different name/path",
|
|
5219
|
-
hint: "Keep the existing directory and create a new one"
|
|
5220
|
-
},
|
|
5221
|
-
{
|
|
5222
|
-
value: "cancel",
|
|
5223
|
-
label: "Cancel",
|
|
5224
|
-
hint: "Abort the process"
|
|
5225
|
-
}
|
|
5226
|
-
],
|
|
5227
|
-
initialValue: "rename"
|
|
5474
|
+
message: "How should we continue?",
|
|
5475
|
+
options,
|
|
5476
|
+
initialValue: incrementedPath ? "increment" : "rename"
|
|
5228
5477
|
});
|
|
5229
5478
|
if (isCancel(action)) throw new UserCancelledError({ message: "Operation cancelled." });
|
|
5230
5479
|
switch (action) {
|
|
5231
|
-
case "
|
|
5232
|
-
finalPathInput:
|
|
5233
|
-
shouldClearDirectory:
|
|
5480
|
+
case "increment": return {
|
|
5481
|
+
finalPathInput: incrementedPath,
|
|
5482
|
+
shouldClearDirectory: false
|
|
5234
5483
|
};
|
|
5484
|
+
case "overwrite": {
|
|
5485
|
+
const confirmed = await confirm({
|
|
5486
|
+
message: `Permanently delete every file in "${currentPathInput}"?`,
|
|
5487
|
+
initialValue: false
|
|
5488
|
+
});
|
|
5489
|
+
if (isCancel(confirmed)) throw new UserCancelledError({ message: "Operation cancelled." });
|
|
5490
|
+
if (!confirmed) {
|
|
5491
|
+
log.info("Nothing was deleted. Choose another option.");
|
|
5492
|
+
continue;
|
|
5493
|
+
}
|
|
5494
|
+
return {
|
|
5495
|
+
finalPathInput: currentPathInput,
|
|
5496
|
+
shouldClearDirectory: true
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5235
5499
|
case "merge":
|
|
5236
5500
|
log.info(`Proceeding into existing directory "${pc.yellow(currentPathInput)}". Files may be overwritten.`);
|
|
5237
5501
|
return {
|
|
@@ -5239,8 +5503,8 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
5239
5503
|
shouldClearDirectory: false
|
|
5240
5504
|
};
|
|
5241
5505
|
case "rename":
|
|
5242
|
-
|
|
5243
|
-
|
|
5506
|
+
currentPathInput = await getProjectName(void 0);
|
|
5507
|
+
continue;
|
|
5244
5508
|
case "cancel": throw new UserCancelledError({ message: "Operation cancelled." });
|
|
5245
5509
|
}
|
|
5246
5510
|
}
|
|
@@ -5255,9 +5519,11 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
5255
5519
|
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
|
5256
5520
|
finalBaseName = path.basename(finalResolvedPath);
|
|
5257
5521
|
}
|
|
5522
|
+
const pathSafetyResult = await validateSafeProjectDirectoryPath(finalPathInput);
|
|
5523
|
+
if (pathSafetyResult.isErr()) throw pathSafetyResult.error;
|
|
5258
5524
|
if (shouldClearDirectory) {
|
|
5259
|
-
const s = spinner();
|
|
5260
|
-
s
|
|
5525
|
+
const s = isSilent() ? void 0 : spinner();
|
|
5526
|
+
s?.start(`Clearing directory "${finalResolvedPath}"...`);
|
|
5261
5527
|
const clearResult = await Result.tryPromise({
|
|
5262
5528
|
try: () => fs.emptyDir(finalResolvedPath),
|
|
5263
5529
|
catch: (error) => new CLIError({
|
|
@@ -5266,16 +5532,78 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
5266
5532
|
})
|
|
5267
5533
|
});
|
|
5268
5534
|
if (clearResult.isErr()) {
|
|
5269
|
-
s
|
|
5535
|
+
s?.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
|
5270
5536
|
throw clearResult.error;
|
|
5271
5537
|
}
|
|
5272
|
-
s
|
|
5538
|
+
s?.stop(`Directory "${finalResolvedPath}" cleared.`);
|
|
5273
5539
|
} else await fs.ensureDir(finalResolvedPath);
|
|
5274
5540
|
return {
|
|
5275
5541
|
finalResolvedPath,
|
|
5276
5542
|
finalBaseName
|
|
5277
5543
|
};
|
|
5278
5544
|
}
|
|
5545
|
+
async function validateSafeProjectDirectoryPath(finalPathInput) {
|
|
5546
|
+
return Result.tryPromise({
|
|
5547
|
+
try: async () => {
|
|
5548
|
+
const cwd = path.resolve(process.cwd());
|
|
5549
|
+
const targetPath = finalPathInput === "." ? cwd : path.resolve(cwd, finalPathInput);
|
|
5550
|
+
const relativeTarget = path.relative(cwd, targetPath);
|
|
5551
|
+
if (relativeTarget === ".." || relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) throw new CLIError({ message: `Project path "${finalPathInput}" resolves outside the current working directory.` });
|
|
5552
|
+
const pathSegments = relativeTarget.split(path.sep).filter(Boolean);
|
|
5553
|
+
let nearestExistingPath = cwd;
|
|
5554
|
+
for (const segment of pathSegments) {
|
|
5555
|
+
const candidatePath = path.join(nearestExistingPath, segment);
|
|
5556
|
+
let stats;
|
|
5557
|
+
try {
|
|
5558
|
+
stats = await fs.lstat(candidatePath);
|
|
5559
|
+
} catch (error) {
|
|
5560
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
|
|
5561
|
+
throw error;
|
|
5562
|
+
}
|
|
5563
|
+
if (stats.isSymbolicLink()) throw new CLIError({ message: `Project path "${finalPathInput}" passes through symbolic link "${path.relative(cwd, candidatePath)}". Choose a real directory within the current working directory.` });
|
|
5564
|
+
if (!stats.isDirectory()) throw new CLIError({ message: `Project path "${finalPathInput}" passes through "${path.relative(cwd, candidatePath)}", which is not a directory.` });
|
|
5565
|
+
nearestExistingPath = candidatePath;
|
|
5566
|
+
}
|
|
5567
|
+
const [realCwd, realExistingPath] = await Promise.all([fs.realpath(cwd), fs.realpath(nearestExistingPath)]);
|
|
5568
|
+
const relativeRealPath = path.relative(realCwd, realExistingPath);
|
|
5569
|
+
if (relativeRealPath === ".." || relativeRealPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeRealPath)) throw new CLIError({ message: `Project path "${finalPathInput}" resolves outside the current working directory.` });
|
|
5570
|
+
},
|
|
5571
|
+
catch: (error) => CLIError.is(error) ? error : new CLIError({
|
|
5572
|
+
message: `Unable to validate project path "${finalPathInput}".`,
|
|
5573
|
+
cause: error
|
|
5574
|
+
})
|
|
5575
|
+
});
|
|
5576
|
+
}
|
|
5577
|
+
async function inspectProjectPath(targetPath) {
|
|
5578
|
+
return Result.tryPromise({
|
|
5579
|
+
try: async () => {
|
|
5580
|
+
let stats;
|
|
5581
|
+
try {
|
|
5582
|
+
stats = await fs.lstat(targetPath);
|
|
5583
|
+
} catch (error) {
|
|
5584
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "missing";
|
|
5585
|
+
throw error;
|
|
5586
|
+
}
|
|
5587
|
+
if (stats.isSymbolicLink()) return "symbolic-link";
|
|
5588
|
+
if (!stats.isDirectory()) return "non-directory";
|
|
5589
|
+
return (await fs.readdir(targetPath)).length === 0 ? "empty-directory" : "non-empty-directory";
|
|
5590
|
+
},
|
|
5591
|
+
catch: (error) => new CLIError({
|
|
5592
|
+
message: `Unable to inspect project path "${targetPath}".`,
|
|
5593
|
+
cause: error
|
|
5594
|
+
})
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
async function findAvailableIncrementedPath(currentPathInput) {
|
|
5598
|
+
let counter = 1;
|
|
5599
|
+
while (true) {
|
|
5600
|
+
const candidate = `${currentPathInput}-${counter}`;
|
|
5601
|
+
const candidateStateResult = await inspectProjectPath(path.resolve(process.cwd(), candidate));
|
|
5602
|
+
if (candidateStateResult.isErr()) return Result.err(candidateStateResult.error);
|
|
5603
|
+
if (candidateStateResult.value === "missing" || candidateStateResult.value === "empty-directory") return Result.ok(candidate);
|
|
5604
|
+
counter++;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5279
5607
|
//#endregion
|
|
5280
5608
|
//#region src/utils/project-name-validation.ts
|
|
5281
5609
|
function validateProjectName(name) {
|
|
@@ -6985,7 +7313,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
6985
7313
|
const polarInstructions = config.payments === "polar" && config.auth === "better-auth" ? getPolarInstructions(backend, packageManager) : "";
|
|
6986
7314
|
const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
|
|
6987
7315
|
const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
|
6988
|
-
let output = `${pc.
|
|
7316
|
+
let output = `${pc.cyan("1.")} ${cdCmd}\n`;
|
|
6989
7317
|
let stepCounter = 2;
|
|
6990
7318
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
|
|
6991
7319
|
if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
|
|
@@ -7003,19 +7331,44 @@ async function displayPostInstallInstructions(config) {
|
|
|
7003
7331
|
}
|
|
7004
7332
|
const hasStandaloneBackend = backend !== "none";
|
|
7005
7333
|
if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7334
|
+
const localServices = [];
|
|
7335
|
+
let localDevelopmentNote = "";
|
|
7336
|
+
if (hasWeb) localServices.push({
|
|
7337
|
+
label: "Frontend",
|
|
7338
|
+
url: `http://localhost:${webPort}`
|
|
7339
|
+
});
|
|
7340
|
+
else if (!hasNative && !addons?.includes("starlight")) localDevelopmentNote = "Backend-only app — no frontend selected";
|
|
7009
7341
|
if (!isConvex && !isBackendSelf && hasStandaloneBackend) {
|
|
7010
|
-
|
|
7011
|
-
|
|
7342
|
+
localServices.push({
|
|
7343
|
+
label: "API",
|
|
7344
|
+
url: "http://localhost:3000"
|
|
7345
|
+
});
|
|
7346
|
+
if (api === "orpc") localServices.push({
|
|
7347
|
+
label: "API reference",
|
|
7348
|
+
url: "http://localhost:3000/api-reference"
|
|
7349
|
+
});
|
|
7012
7350
|
}
|
|
7013
7351
|
if (isBackendSelf && api === "orpc") {
|
|
7014
7352
|
const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
|
|
7015
|
-
|
|
7353
|
+
localServices.push({
|
|
7354
|
+
label: "API reference",
|
|
7355
|
+
url: `http://localhost:${webPort}${rpcPath}/api-reference`
|
|
7356
|
+
});
|
|
7357
|
+
}
|
|
7358
|
+
if (addons?.includes("starlight")) localServices.push({
|
|
7359
|
+
label: "Docs",
|
|
7360
|
+
url: "http://localhost:4321"
|
|
7361
|
+
});
|
|
7362
|
+
if (addons?.includes("fumadocs")) localServices.push({
|
|
7363
|
+
label: "Fumadocs",
|
|
7364
|
+
url: "http://localhost:4000"
|
|
7365
|
+
});
|
|
7366
|
+
output += `\n${pc.bold("Local development")}\n`;
|
|
7367
|
+
if (localDevelopmentNote) output += `${pc.dim(localDevelopmentNote)}\n`;
|
|
7368
|
+
if (localServices.length > 0) {
|
|
7369
|
+
const labelWidth = Math.max(...localServices.map(({ label }) => label.length));
|
|
7370
|
+
for (const { label, url } of localServices) output += `${pc.dim(label.padEnd(labelWidth))} ${pc.cyan(url)}\n`;
|
|
7016
7371
|
}
|
|
7017
|
-
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
7018
|
-
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
7019
7372
|
}
|
|
7020
7373
|
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
|
7021
7374
|
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
|
@@ -7035,10 +7388,15 @@ async function displayPostInstallInstructions(config) {
|
|
|
7035
7388
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
7036
7389
|
const sponsorsResult = await fetchSponsorsQuietly();
|
|
7037
7390
|
const specialSponsorsSection = sponsorsResult.isOk() ? formatPostInstallSpecialSponsorsSection(sponsorsResult.value) : "";
|
|
7038
|
-
|
|
7039
|
-
output
|
|
7040
|
-
|
|
7041
|
-
|
|
7391
|
+
log.message([], { spacing: 1 });
|
|
7392
|
+
box(output.trimEnd(), pc.bold("Next steps"), {
|
|
7393
|
+
contentPadding: 2,
|
|
7394
|
+
formatBorder: pc.dim,
|
|
7395
|
+
rounded: true,
|
|
7396
|
+
width: "auto"
|
|
7397
|
+
});
|
|
7398
|
+
if (specialSponsorsSection) cliLog.message(specialSponsorsSection);
|
|
7399
|
+
cliLog.message(`${pc.bold("Like Better T Stack?")} ${pc.dim("Star the project on GitHub")}\n${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`);
|
|
7042
7400
|
}
|
|
7043
7401
|
function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
|
|
7044
7402
|
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
|
|
@@ -7065,43 +7423,82 @@ function getVitePlusNativeHooksInstructions(runCmd) {
|
|
|
7065
7423
|
return `${pc.bold("Vite+ native Git hooks:")}\n${pc.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
|
|
7066
7424
|
}
|
|
7067
7425
|
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
|
|
7068
|
-
const
|
|
7426
|
+
const notes = [];
|
|
7427
|
+
const commands = [];
|
|
7069
7428
|
const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
|
|
7070
7429
|
if (dbSetup === "docker") {
|
|
7071
7430
|
const dockerStatus = await getDockerStatus(database);
|
|
7072
|
-
if (dockerStatus.message)
|
|
7073
|
-
instructions.push(dockerStatus.message);
|
|
7074
|
-
instructions.push("");
|
|
7075
|
-
}
|
|
7431
|
+
if (dockerStatus.message) notes.push(dockerStatus.message);
|
|
7076
7432
|
}
|
|
7077
7433
|
if (isD1Alchemy) {
|
|
7078
|
-
if (orm === "drizzle")
|
|
7434
|
+
if (orm === "drizzle") commands.push({
|
|
7435
|
+
label: "Generate migrations",
|
|
7436
|
+
command: `${runCmd} db:generate`
|
|
7437
|
+
});
|
|
7079
7438
|
else if (orm === "prisma") {
|
|
7080
|
-
|
|
7081
|
-
|
|
7439
|
+
commands.push({
|
|
7440
|
+
label: "Generate client",
|
|
7441
|
+
command: `${runCmd} db:generate`
|
|
7442
|
+
});
|
|
7443
|
+
commands.push({
|
|
7444
|
+
label: "Apply migrations",
|
|
7445
|
+
command: `${runCmd} db:migrate`
|
|
7446
|
+
});
|
|
7082
7447
|
}
|
|
7083
7448
|
}
|
|
7084
7449
|
if (dbSetup === "planetscale") {
|
|
7085
|
-
if (database === "mysql" && orm === "drizzle")
|
|
7086
|
-
if (database === "mysql" && orm === "prisma")
|
|
7450
|
+
if (database === "mysql" && orm === "drizzle") notes.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
|
|
7451
|
+
if (database === "mysql" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
|
|
7087
7452
|
}
|
|
7088
|
-
if (dbSetup === "turso" && orm === "prisma")
|
|
7453
|
+
if (dbSetup === "turso" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} Follow Turso's Prisma guide for migrations via the Turso CLI:\n https://docs.turso.tech/sdk/ts/orm/prisma`);
|
|
7089
7454
|
if (orm === "prisma") {
|
|
7090
|
-
if (database === "mongodb" && dbSetup === "docker")
|
|
7091
|
-
if (dbSetup === "docker")
|
|
7455
|
+
if (database === "mongodb" && dbSetup === "docker") notes.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
7456
|
+
if (dbSetup === "docker") commands.push({
|
|
7457
|
+
label: "Start database",
|
|
7458
|
+
command: `${runCmd} db:start`
|
|
7459
|
+
});
|
|
7092
7460
|
if (!isD1Alchemy) {
|
|
7093
|
-
|
|
7094
|
-
|
|
7461
|
+
commands.push({
|
|
7462
|
+
label: "Generate client",
|
|
7463
|
+
command: `${runCmd} db:generate`
|
|
7464
|
+
});
|
|
7465
|
+
commands.push({
|
|
7466
|
+
label: "Apply schema",
|
|
7467
|
+
command: `${runCmd} db:push`
|
|
7468
|
+
});
|
|
7095
7469
|
}
|
|
7096
|
-
if (!isD1Alchemy)
|
|
7470
|
+
if (!isD1Alchemy) commands.push({
|
|
7471
|
+
label: "Open studio",
|
|
7472
|
+
command: `${runCmd} db:studio`
|
|
7473
|
+
});
|
|
7097
7474
|
} else if (orm === "drizzle") {
|
|
7098
|
-
if (dbSetup === "docker")
|
|
7099
|
-
|
|
7100
|
-
|
|
7475
|
+
if (dbSetup === "docker") commands.push({
|
|
7476
|
+
label: "Start database",
|
|
7477
|
+
command: `${runCmd} db:start`
|
|
7478
|
+
});
|
|
7479
|
+
if (!isD1Alchemy) commands.push({
|
|
7480
|
+
label: "Apply schema",
|
|
7481
|
+
command: `${runCmd} db:push`
|
|
7482
|
+
});
|
|
7483
|
+
if (!isD1Alchemy) commands.push({
|
|
7484
|
+
label: "Open studio",
|
|
7485
|
+
command: `${runCmd} db:studio`
|
|
7486
|
+
});
|
|
7101
7487
|
} else if (orm === "mongoose") {
|
|
7102
|
-
if (dbSetup === "docker")
|
|
7103
|
-
|
|
7104
|
-
|
|
7488
|
+
if (dbSetup === "docker") commands.push({
|
|
7489
|
+
label: "Start database",
|
|
7490
|
+
command: `${runCmd} db:start`
|
|
7491
|
+
});
|
|
7492
|
+
} else if (orm === "none") notes.push(`${pc.yellow("NOTE:")} Manual database schema setup required.`);
|
|
7493
|
+
if (notes.length === 0 && commands.length === 0) return "";
|
|
7494
|
+
const sections = [pc.bold("Database")];
|
|
7495
|
+
if (notes.length > 0) sections.push(notes.join("\n"));
|
|
7496
|
+
if (commands.length > 0) {
|
|
7497
|
+
const labelWidth = Math.max(...commands.map(({ label }) => label.length));
|
|
7498
|
+
const commandRows = commands.map(({ label, command }) => `${pc.dim(label.padEnd(labelWidth))} ${pc.cyan(command)}`);
|
|
7499
|
+
sections.push(commandRows.join("\n"));
|
|
7500
|
+
}
|
|
7501
|
+
return sections.join("\n");
|
|
7105
7502
|
}
|
|
7106
7503
|
function getTauriInstructions(runCmd, frontend) {
|
|
7107
7504
|
const staticBuildNote = getDesktopStaticBuildNote(frontend);
|
|
@@ -7268,7 +7665,7 @@ async function createProject(options, cliInput = {}) {
|
|
|
7268
7665
|
})
|
|
7269
7666
|
}));
|
|
7270
7667
|
yield* Result.await(formatProject(projectDir));
|
|
7271
|
-
if (!isSilent()) log.success("Project
|
|
7668
|
+
if (!isSilent()) log.success("Project scaffolded");
|
|
7272
7669
|
if (options.install) yield* Result.await(installDependencies({
|
|
7273
7670
|
projectDir,
|
|
7274
7671
|
packageManager: options.packageManager
|
|
@@ -7363,19 +7760,16 @@ async function createProjectHandler(input, options = {}) {
|
|
|
7363
7760
|
async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
7364
7761
|
return Result.gen(async function* () {
|
|
7365
7762
|
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
7366
|
-
if (!isSilent()) intro(pc.magenta("
|
|
7367
|
-
if (!isSilent() && input.yolo)
|
|
7763
|
+
if (!isSilent()) intro(pc.magenta("Configure your new project"));
|
|
7764
|
+
if (!isSilent() && input.yolo) log.warn(pc.yellow("YOLO mode enabled — compatibility checks are disabled."));
|
|
7368
7765
|
let currentPathInput;
|
|
7369
7766
|
if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
|
|
7370
7767
|
else if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
7371
7768
|
else if (input.yes) {
|
|
7372
7769
|
const defaultConfig = getDefaultConfig();
|
|
7373
7770
|
let defaultName = defaultConfig.relativePath;
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
7377
|
-
counter++;
|
|
7378
|
-
}
|
|
7771
|
+
const defaultPathState = yield* Result.await(inspectProjectPath(path.resolve(process.cwd(), defaultName)));
|
|
7772
|
+
if (defaultPathState !== "missing" && defaultPathState !== "empty-directory") defaultName = yield* Result.await(findAvailableIncrementedPath(defaultConfig.projectName));
|
|
7379
7773
|
currentPathInput = defaultName;
|
|
7380
7774
|
} else currentPathInput = yield* Result.await(Result.tryPromise({
|
|
7381
7775
|
try: async () => getProjectName(input.projectName),
|
|
@@ -7394,6 +7788,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7394
7788
|
finalPathInput = conflictResult.finalPathInput;
|
|
7395
7789
|
shouldClearDirectory = conflictResult.shouldClearDirectory;
|
|
7396
7790
|
yield* validateResolvedProjectPathInput(finalPathInput);
|
|
7791
|
+
yield* Result.await(validateSafeProjectDirectoryPath(finalPathInput));
|
|
7397
7792
|
let finalResolvedPath;
|
|
7398
7793
|
let finalBaseName;
|
|
7399
7794
|
if (input.dryRun) {
|
|
@@ -7424,10 +7819,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7424
7819
|
if (templateConfig) {
|
|
7425
7820
|
const templateName = input.template.toUpperCase();
|
|
7426
7821
|
const templateDescription = getTemplateDescription(input.template);
|
|
7427
|
-
if (!isSilent()) {
|
|
7428
|
-
log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
|
|
7429
|
-
log.message(pc.dim(` ${templateDescription}`));
|
|
7430
|
-
}
|
|
7822
|
+
if (!isSilent()) log.info(`${pc.dim("Template")} ${pc.bold(pc.cyan(templateName))}\n${pc.dim(templateDescription)}`);
|
|
7431
7823
|
const userOverrides = {};
|
|
7432
7824
|
for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
|
|
7433
7825
|
cliInput = {
|
|
@@ -7458,10 +7850,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7458
7850
|
message: validationResult.error.message,
|
|
7459
7851
|
cause: validationResult.error
|
|
7460
7852
|
}));
|
|
7461
|
-
if (!isSilent())
|
|
7462
|
-
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
7463
|
-
log.message(displayConfig(config));
|
|
7464
|
-
}
|
|
7853
|
+
if (!isSilent()) log.info(pc.dim("Quick setup selected — using defaults and provided flags."));
|
|
7465
7854
|
} else {
|
|
7466
7855
|
const flagConfigResult = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
7467
7856
|
if (flagConfigResult.isErr()) return Result.err(new CLIError({
|
|
@@ -7470,13 +7859,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7470
7859
|
}));
|
|
7471
7860
|
const flagConfig = flagConfigResult.value;
|
|
7472
7861
|
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
log.message(displayConfig(otherFlags));
|
|
7476
|
-
log.message("");
|
|
7477
|
-
}
|
|
7862
|
+
const isTemplateSetup = input.template && input.template !== "none";
|
|
7863
|
+
if (!isSilent() && !isTemplateSetup && Object.keys(otherFlags).length > 0) log.info(pc.dim("Command-line options applied."));
|
|
7478
7864
|
config = yield* Result.await(Result.tryPromise({
|
|
7479
|
-
try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput),
|
|
7865
|
+
try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput, { skipCompatibilityChecks: cliInput.yolo }),
|
|
7480
7866
|
catch: (e) => {
|
|
7481
7867
|
if (e instanceof UserCancelledError) return e;
|
|
7482
7868
|
return new CLIError({
|
|
@@ -7498,12 +7884,16 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7498
7884
|
const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
7499
7885
|
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
7500
7886
|
}
|
|
7887
|
+
if (!isSilent()) {
|
|
7888
|
+
log.info(pc.magenta(pc.bold("Stack ready")));
|
|
7889
|
+
log.message(displayConfig(config));
|
|
7890
|
+
}
|
|
7501
7891
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
7502
7892
|
if (input.dryRun) {
|
|
7503
7893
|
const elapsedTimeMs = Date.now() - startTime;
|
|
7504
7894
|
if (!isSilent()) {
|
|
7505
7895
|
if (shouldClearDirectory) log.warn(pc.yellow(`Dry run: directory "${finalPathInput}" would be cleared due to overwrite strategy.`));
|
|
7506
|
-
log.success(pc.green("
|
|
7896
|
+
log.success(pc.green("Configuration ready. No files were written."));
|
|
7507
7897
|
log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
|
|
7508
7898
|
log.message(pc.dim(`Run without --dry-run to create the project.`));
|
|
7509
7899
|
outro(pc.magenta("Dry run complete."));
|
|
@@ -7522,14 +7912,19 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7522
7912
|
manualDb: cliInput.manualDb ?? input.manualDb,
|
|
7523
7913
|
dbSetupOptions: effectiveDbSetupOptions
|
|
7524
7914
|
}));
|
|
7525
|
-
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
7526
7915
|
await trackProjectCreation(config, input.disableAnalytics);
|
|
7527
7916
|
const historyResult = await addToHistory(config, reproducibleCommand);
|
|
7528
|
-
if (historyResult.isErr() && !isSilent())
|
|
7917
|
+
if (historyResult.isErr() && !isSilent()) {
|
|
7918
|
+
log.warn(pc.yellow(historyResult.error.message));
|
|
7919
|
+
log.message(`${pc.dim("Recreate this stack")}\n${pc.cyan(reproducibleCommand)}`);
|
|
7920
|
+
} else if (!isSilent()) {
|
|
7921
|
+
const historyCommand = getCliSubcommandCommand("history", config.packageManager);
|
|
7922
|
+
log.message(`${pc.dim("Setup saved to history")}\n${pc.cyan(historyCommand)}`);
|
|
7923
|
+
}
|
|
7529
7924
|
const elapsedTimeMs = Date.now() - startTime;
|
|
7530
7925
|
if (!isSilent()) {
|
|
7531
|
-
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(
|
|
7532
|
-
outro(pc.magenta(`Project
|
|
7926
|
+
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(1);
|
|
7927
|
+
outro(pc.magenta(`Project ready in ${pc.bold(`${elapsedTimeInSeconds}s`)}`));
|
|
7533
7928
|
}
|
|
7534
7929
|
return Result.ok({
|
|
7535
7930
|
success: true,
|
|
@@ -7582,15 +7977,23 @@ async function handleDirectoryConflictResult(currentPathInput, strategy) {
|
|
|
7582
7977
|
});
|
|
7583
7978
|
}
|
|
7584
7979
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
7585
|
-
const
|
|
7586
|
-
if (
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
});
|
|
7590
|
-
if (!((await fs.readdir(currentPath)).length > 0)) return Result.ok({
|
|
7980
|
+
const pathStateResult = await inspectProjectPath(path.resolve(process.cwd(), currentPathInput));
|
|
7981
|
+
if (pathStateResult.isErr()) return Result.err(pathStateResult.error);
|
|
7982
|
+
const pathState = pathStateResult.value;
|
|
7983
|
+
if (pathState === "missing" || pathState === "empty-directory") return Result.ok({
|
|
7591
7984
|
finalPathInput: currentPathInput,
|
|
7592
7985
|
shouldClearDirectory: false
|
|
7593
7986
|
});
|
|
7987
|
+
if (strategy === "increment") {
|
|
7988
|
+
const incrementResult = await findAvailableIncrementedPath(currentPathInput);
|
|
7989
|
+
if (incrementResult.isErr()) return Result.err(incrementResult.error);
|
|
7990
|
+
return Result.ok({
|
|
7991
|
+
finalPathInput: incrementResult.value,
|
|
7992
|
+
shouldClearDirectory: false
|
|
7993
|
+
});
|
|
7994
|
+
}
|
|
7995
|
+
if (pathState === "symbolic-link") return Result.err(new CLIError({ message: `Project path "${currentPathInput}" is a symbolic link. Choose a real directory or use directoryConflict: "increment".` }));
|
|
7996
|
+
if (pathState === "non-directory") return Result.err(new CLIError({ message: `Project path "${currentPathInput}" exists and is not a directory. Choose a different path or use directoryConflict: "increment".` }));
|
|
7594
7997
|
switch (strategy) {
|
|
7595
7998
|
case "overwrite": return Result.ok({
|
|
7596
7999
|
finalPathInput: currentPathInput,
|
|
@@ -7600,19 +8003,6 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
7600
8003
|
finalPathInput: currentPathInput,
|
|
7601
8004
|
shouldClearDirectory: false
|
|
7602
8005
|
});
|
|
7603
|
-
case "increment": {
|
|
7604
|
-
let counter = 1;
|
|
7605
|
-
const baseName = currentPathInput;
|
|
7606
|
-
let finalPathInput = `${baseName}-${counter}`;
|
|
7607
|
-
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
7608
|
-
counter++;
|
|
7609
|
-
finalPathInput = `${baseName}-${counter}`;
|
|
7610
|
-
}
|
|
7611
|
-
return Result.ok({
|
|
7612
|
-
finalPathInput,
|
|
7613
|
-
shouldClearDirectory: false
|
|
7614
|
-
});
|
|
7615
|
-
}
|
|
7616
8006
|
case "error": return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
|
|
7617
8007
|
default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
|
|
7618
8008
|
}
|
|
@@ -7647,6 +8037,10 @@ const SchemaNameSchema = z.enum([
|
|
|
7647
8037
|
"betterTStackConfigFile",
|
|
7648
8038
|
"initResult"
|
|
7649
8039
|
]).default("all");
|
|
8040
|
+
const CreateVirtualInputSchema = types_exports.ProjectConfigSchema.omit({
|
|
8041
|
+
projectDir: true,
|
|
8042
|
+
relativePath: true
|
|
8043
|
+
}).partial().strict();
|
|
7650
8044
|
const t = initTRPC.meta().create();
|
|
7651
8045
|
function getCliSchemaJson() {
|
|
7652
8046
|
return createCli({
|
|
@@ -7702,7 +8096,7 @@ const router = t.router({
|
|
|
7702
8096
|
projectName,
|
|
7703
8097
|
...options
|
|
7704
8098
|
});
|
|
7705
|
-
if (options.verbose
|
|
8099
|
+
if (options.verbose) return result;
|
|
7706
8100
|
}),
|
|
7707
8101
|
createJson: t.procedure.meta({
|
|
7708
8102
|
description: "Create a project from a raw JSON payload (agent-friendly)",
|
|
@@ -7721,7 +8115,8 @@ const router = t.router({
|
|
|
7721
8115
|
addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
|
|
7722
8116
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
|
|
7723
8117
|
packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
|
|
7724
|
-
projectDir: z.string().optional().describe("Project directory (defaults to current)")
|
|
8118
|
+
projectDir: z.string().optional().describe("Project directory (defaults to current)"),
|
|
8119
|
+
dryRun: z.boolean().optional().default(false).describe("Preview addon changes without writing files")
|
|
7725
8120
|
})).mutation(async ({ input }) => {
|
|
7726
8121
|
await addHandler(input);
|
|
7727
8122
|
}),
|
|
@@ -7749,6 +8144,12 @@ function createBtsCli() {
|
|
|
7749
8144
|
version: getLatestCLIVersion()
|
|
7750
8145
|
});
|
|
7751
8146
|
}
|
|
8147
|
+
function formatInputValidationError(label, error) {
|
|
8148
|
+
return `Invalid ${label} input: ${error.issues.map((issue) => {
|
|
8149
|
+
const field = issue.path.join(".");
|
|
8150
|
+
return field ? `${field}: ${issue.message}` : issue.message;
|
|
8151
|
+
}).join("; ")}`;
|
|
8152
|
+
}
|
|
7752
8153
|
/**
|
|
7753
8154
|
* Programmatic API to create a new Better-T-Stack project.
|
|
7754
8155
|
* Returns a Result type - no console output, no interactive prompts.
|
|
@@ -7775,13 +8176,21 @@ function createBtsCli() {
|
|
|
7775
8176
|
* ```
|
|
7776
8177
|
*/
|
|
7777
8178
|
async function create(projectName, options) {
|
|
7778
|
-
const
|
|
8179
|
+
const rawInput = options === void 0 || typeof options === "object" && options !== null ? {
|
|
7779
8180
|
...options,
|
|
7780
|
-
projectName
|
|
8181
|
+
projectName
|
|
8182
|
+
} : options;
|
|
8183
|
+
const parsedInput = types_exports.CreateInputSchema.safeParse(rawInput);
|
|
8184
|
+
if (!parsedInput.success) return Result.err(new CLIError({
|
|
8185
|
+
message: formatInputValidationError("create", parsedInput.error),
|
|
8186
|
+
cause: parsedInput.error
|
|
8187
|
+
}));
|
|
8188
|
+
const input = {
|
|
8189
|
+
...parsedInput.data,
|
|
7781
8190
|
renderTitle: false,
|
|
7782
8191
|
verbose: true,
|
|
7783
|
-
disableAnalytics:
|
|
7784
|
-
directoryConflict:
|
|
8192
|
+
disableAnalytics: parsedInput.data.disableAnalytics ?? true,
|
|
8193
|
+
directoryConflict: parsedInput.data.directoryConflict ?? "error"
|
|
7785
8194
|
};
|
|
7786
8195
|
return Result.tryPromise({
|
|
7787
8196
|
try: async () => {
|
|
@@ -7834,28 +8243,35 @@ async function builder() {
|
|
|
7834
8243
|
* ```
|
|
7835
8244
|
*/
|
|
7836
8245
|
async function createVirtual(options) {
|
|
8246
|
+
const parsedInput = CreateVirtualInputSchema.safeParse(options);
|
|
8247
|
+
if (!parsedInput.success) return Result.err(new GeneratorError({
|
|
8248
|
+
message: formatInputValidationError("virtual create", parsedInput.error),
|
|
8249
|
+
phase: "validation",
|
|
8250
|
+
cause: parsedInput.error
|
|
8251
|
+
}));
|
|
8252
|
+
const virtualOptions = parsedInput.data;
|
|
7837
8253
|
const config = {
|
|
7838
|
-
projectName:
|
|
8254
|
+
projectName: virtualOptions.projectName || "my-project",
|
|
7839
8255
|
projectDir: "/virtual",
|
|
7840
8256
|
relativePath: "./virtual",
|
|
7841
|
-
addonOptions:
|
|
7842
|
-
dbSetupOptions:
|
|
7843
|
-
database:
|
|
7844
|
-
orm:
|
|
7845
|
-
backend:
|
|
7846
|
-
runtime:
|
|
7847
|
-
frontend:
|
|
7848
|
-
addons:
|
|
7849
|
-
examples:
|
|
7850
|
-
auth:
|
|
7851
|
-
payments:
|
|
7852
|
-
git:
|
|
7853
|
-
packageManager:
|
|
8257
|
+
addonOptions: virtualOptions.addonOptions,
|
|
8258
|
+
dbSetupOptions: virtualOptions.dbSetupOptions,
|
|
8259
|
+
database: virtualOptions.database || "none",
|
|
8260
|
+
orm: virtualOptions.orm || "none",
|
|
8261
|
+
backend: virtualOptions.backend || "hono",
|
|
8262
|
+
runtime: virtualOptions.runtime || "bun",
|
|
8263
|
+
frontend: virtualOptions.frontend || ["tanstack-router"],
|
|
8264
|
+
addons: virtualOptions.addons || [],
|
|
8265
|
+
examples: virtualOptions.examples || [],
|
|
8266
|
+
auth: virtualOptions.auth || "none",
|
|
8267
|
+
payments: virtualOptions.payments || "none",
|
|
8268
|
+
git: virtualOptions.git ?? false,
|
|
8269
|
+
packageManager: virtualOptions.packageManager || "bun",
|
|
7854
8270
|
install: false,
|
|
7855
|
-
dbSetup:
|
|
7856
|
-
api:
|
|
7857
|
-
webDeploy:
|
|
7858
|
-
serverDeploy:
|
|
8271
|
+
dbSetup: virtualOptions.dbSetup || "none",
|
|
8272
|
+
api: virtualOptions.api || "trpc",
|
|
8273
|
+
webDeploy: virtualOptions.webDeploy || "none",
|
|
8274
|
+
serverDeploy: virtualOptions.serverDeploy || "none"
|
|
7859
8275
|
};
|
|
7860
8276
|
const validationResult = validateConfigCompatibility(config, new Set([
|
|
7861
8277
|
"database",
|
|
@@ -7894,13 +8310,25 @@ async function createVirtual(options) {
|
|
|
7894
8310
|
* install: true,
|
|
7895
8311
|
* });
|
|
7896
8312
|
*
|
|
7897
|
-
* if (result
|
|
8313
|
+
* if (result.success) {
|
|
7898
8314
|
* console.log(`Added: ${result.addedAddons.join(", ")}`);
|
|
7899
8315
|
* }
|
|
7900
8316
|
* ```
|
|
7901
8317
|
*/
|
|
7902
8318
|
async function add(options = {}) {
|
|
7903
|
-
|
|
8319
|
+
const parsedInput = types_exports.AddInputSchema.safeParse(options);
|
|
8320
|
+
if (!parsedInput.success) return {
|
|
8321
|
+
success: false,
|
|
8322
|
+
addedAddons: [],
|
|
8323
|
+
projectDir: "",
|
|
8324
|
+
error: formatInputValidationError("add", parsedInput.error)
|
|
8325
|
+
};
|
|
8326
|
+
return await addHandler(parsedInput.data, { silent: true }) ?? {
|
|
8327
|
+
success: false,
|
|
8328
|
+
addedAddons: [],
|
|
8329
|
+
projectDir: parsedInput.data.projectDir ?? "",
|
|
8330
|
+
error: "Operation cancelled"
|
|
8331
|
+
};
|
|
7904
8332
|
}
|
|
7905
8333
|
//#endregion
|
|
7906
8334
|
export { ProjectCreationError as C, DirectoryConflictError as S, ValidationError as T, types_exports as _, TEMPLATE_COUNT as a, CompatibilityError as b, builder as c, createVirtual as d, docs as f, sponsors as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, getLatestCLIVersion as v, UserCancelledError as w, DatabaseSetupError as x, CLIError as y };
|