create-better-t-stack 3.36.4 → 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-AlTJbWsW.mjs → src-IZv104V2.mjs} +821 -364
- 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,
|
|
@@ -1546,6 +1756,13 @@ function getAuthImportLine(config) {
|
|
|
1546
1756
|
function getAuthExpression(config) {
|
|
1547
1757
|
return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
|
|
1548
1758
|
}
|
|
1759
|
+
function addAiSdkEvlogTelemetry(content, loggerExpression) {
|
|
1760
|
+
let nextContent = addNamedImport(content, "evlog/ai", ["createAILogger", "createEvlogIntegration"]);
|
|
1761
|
+
if (!nextContent.includes("const ai = createAILogger(")) nextContent = nextContent.replace(/^(\s*)const model = wrapLanguageModel\({/m, (_match, indent) => `${indent}const ai = createAILogger(${loggerExpression});\n${indent}const model = wrapLanguageModel({`);
|
|
1762
|
+
if (!nextContent.includes("model: ai.wrap(model)")) nextContent = nextContent.replace(/(const result = streamText\({\n\s*)model,/, "$1model: ai.wrap(model),");
|
|
1763
|
+
if (!nextContent.includes("createEvlogIntegration(ai)")) nextContent = nextContent.replace(/^(\s*)(messages:\s*await convertToModelMessages\([^)]+\),?)/m, (_match, indent, messages) => `${indent}${messages.endsWith(",") ? messages : `${messages},`}\n${indent}telemetry: {\n${indent}\tisEnabled: true,\n${indent}\tintegrations: [createEvlogIntegration(ai)],\n${indent}},`);
|
|
1764
|
+
return nextContent;
|
|
1765
|
+
}
|
|
1549
1766
|
function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
|
|
1550
1767
|
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1551
1768
|
const usesAuthFactory = authExpression.endsWith("()");
|
|
@@ -1558,15 +1775,17 @@ function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
|
|
|
1558
1775
|
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use("*", async (c, next) => {${identifyUserSetup}\n\tawait identifyUser(c.get("log"), c.req.raw.headers, c.req.path);\n\tawait next();\n});`, "identifyUser(c.get(\"log\")");
|
|
1559
1776
|
}
|
|
1560
1777
|
if (backend === "express") {
|
|
1778
|
+
nextContent = addNamedImport(nextContent, "evlog/express", ["useLogger"]);
|
|
1561
1779
|
nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
|
|
1562
|
-
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(
|
|
1780
|
+
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), req.headers, req.path);\n\tnext();\n});`, "identifyUser(useLogger()");
|
|
1563
1781
|
}
|
|
1564
1782
|
if (backend === "fastify") {
|
|
1565
1783
|
nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
|
|
1566
1784
|
nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
|
|
1567
1785
|
return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
|
|
1568
1786
|
}
|
|
1569
|
-
|
|
1787
|
+
const elysiaMarker = nextContent.includes("const app = new Elysia") ? "const app = new Elysia" : "new Elysia";
|
|
1788
|
+
nextContent = insertBeforeOnce(nextContent, elysiaMarker, identifySnippet, "createAuthMiddleware(");
|
|
1570
1789
|
return insertAfterOnce(nextContent, ".use(evlog())", `\n\t.derive(async ({ request, log }) => {${identifyUserSetup.replace(/\n\t/g, "\n ")}\n\t\tawait identifyUser(log, request.headers, new URL(request.url).pathname);\n\t\treturn {};\n\t})`, "identifyUser(log");
|
|
1571
1790
|
}
|
|
1572
1791
|
function addEvlogServerSetup(content, backend, serviceName) {
|
|
@@ -1589,7 +1808,8 @@ function addEvlogServerSetup(content, backend, serviceName) {
|
|
|
1589
1808
|
return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
|
|
1590
1809
|
}
|
|
1591
1810
|
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
|
|
1592
|
-
|
|
1811
|
+
const elysiaMarker = nextContent.includes("const app = new Elysia") ? "const app = new Elysia" : "new Elysia";
|
|
1812
|
+
nextContent = insertBeforeOnce(nextContent, elysiaMarker, initSnippet, "initLogger({");
|
|
1593
1813
|
for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
|
|
1594
1814
|
return nextContent;
|
|
1595
1815
|
}
|
|
@@ -1671,12 +1891,27 @@ function addNextRouteWrappers(content) {
|
|
|
1671
1891
|
return nextContent;
|
|
1672
1892
|
}
|
|
1673
1893
|
function addNextAiEvlogSetup(content) {
|
|
1674
|
-
let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
|
|
1894
|
+
let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog", "useLogger"]);
|
|
1675
1895
|
if (!nextContent.includes("withEvlog(async (req: Request)")) {
|
|
1676
1896
|
nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
|
|
1677
1897
|
if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
|
|
1678
1898
|
}
|
|
1679
|
-
return nextContent;
|
|
1899
|
+
return addAiSdkEvlogTelemetry(nextContent, "useLogger()");
|
|
1900
|
+
}
|
|
1901
|
+
function addNuxtAiEvlogSetup(content) {
|
|
1902
|
+
return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/nitro", ["useLogger"]), "useLogger(event)");
|
|
1903
|
+
}
|
|
1904
|
+
function addSvelteAiEvlogSetup(content) {
|
|
1905
|
+
return addAiSdkEvlogTelemetry(content.replace("export const POST: RequestHandler = async ({ request }) => {", "export const POST: RequestHandler = async ({ request, locals }) => {"), "locals.log");
|
|
1906
|
+
}
|
|
1907
|
+
function addTanstackStartAiEvlogSetup(content) {
|
|
1908
|
+
return addAiSdkEvlogTelemetry(prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";", "import { useRequest } from \"nitro/context\";"]), "useRequest().context.log as RequestLogger");
|
|
1909
|
+
}
|
|
1910
|
+
function addBackendAiEvlogSetup(content, backend) {
|
|
1911
|
+
if (backend === "hono") return addAiSdkEvlogTelemetry(content, "c.get(\"log\")");
|
|
1912
|
+
if (backend === "express") return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/express", ["useLogger"]), "useLogger()");
|
|
1913
|
+
if (backend === "fastify") return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/fastify", ["useLogger"]), "useLogger()");
|
|
1914
|
+
return addAiSdkEvlogTelemetry(content, "context.log");
|
|
1680
1915
|
}
|
|
1681
1916
|
function addNextBetterAuthToRoute(content) {
|
|
1682
1917
|
let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
|
|
@@ -1921,6 +2156,7 @@ async function setupNuxtEvlog(config, serviceName) {
|
|
|
1921
2156
|
const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
|
|
1922
2157
|
if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
|
|
1923
2158
|
}
|
|
2159
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "server/api/ai.post.ts"), addNuxtAiEvlogSetup);
|
|
1924
2160
|
}
|
|
1925
2161
|
async function setupSvelteEvlog(config, serviceName) {
|
|
1926
2162
|
const webDir = path.join(config.projectDir, "apps/web");
|
|
@@ -1933,6 +2169,7 @@ export const { handle, handleError } = createEvlogHooks();
|
|
|
1933
2169
|
`);
|
|
1934
2170
|
await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
|
|
1935
2171
|
if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
|
|
2172
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/+server.ts"), addSvelteAiEvlogSetup);
|
|
1936
2173
|
}
|
|
1937
2174
|
async function setupTanstackStartEvlog(config, serviceName) {
|
|
1938
2175
|
const webDir = path.join(config.projectDir, "apps/web");
|
|
@@ -1943,6 +2180,7 @@ async function setupTanstackStartEvlog(config, serviceName) {
|
|
|
1943
2180
|
const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
|
|
1944
2181
|
if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
|
|
1945
2182
|
}
|
|
2183
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/$.ts"), addTanstackStartAiEvlogSetup);
|
|
1946
2184
|
}
|
|
1947
2185
|
async function setupAstroEvlog(config, serviceName) {
|
|
1948
2186
|
const webDir = path.join(config.projectDir, "apps/web");
|
|
@@ -1973,6 +2211,7 @@ async function setupEvlog(config) {
|
|
|
1973
2211
|
const content = await fs.readFile(serverIndexPath, "utf-8");
|
|
1974
2212
|
let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
|
|
1975
2213
|
if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
|
|
2214
|
+
if (config.examples.includes("ai")) nextContent = addBackendAiEvlogSetup(nextContent, config.backend);
|
|
1976
2215
|
if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
|
|
1977
2216
|
}
|
|
1978
2217
|
}
|
|
@@ -2008,45 +2247,56 @@ async function navigableGroup(prompts, opts) {
|
|
|
2008
2247
|
delete results[prevName];
|
|
2009
2248
|
currentIndex--;
|
|
2010
2249
|
};
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
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;
|
|
2026
2277
|
continue;
|
|
2027
2278
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
results
|
|
2034
|
-
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;
|
|
2035
2285
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
stepBack();
|
|
2042
|
-
continue;
|
|
2286
|
+
if (goingBack && !didLastPromptShowUI()) {
|
|
2287
|
+
if (currentIndex > 0) {
|
|
2288
|
+
stepBack();
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2043
2291
|
}
|
|
2292
|
+
goingBack = false;
|
|
2293
|
+
results[name] = result;
|
|
2294
|
+
currentIndex++;
|
|
2044
2295
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2296
|
+
} finally {
|
|
2297
|
+
setIsFirstPrompt$1(false);
|
|
2298
|
+
setPromptProgress(void 0);
|
|
2048
2299
|
}
|
|
2049
|
-
setIsFirstPrompt$1(false);
|
|
2050
2300
|
return results;
|
|
2051
2301
|
}
|
|
2052
2302
|
//#endregion
|
|
@@ -3817,7 +4067,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
3817
4067
|
cause: e
|
|
3818
4068
|
})
|
|
3819
4069
|
});
|
|
3820
|
-
if (result.isOk()) s.stop("Dependencies installed
|
|
4070
|
+
if (result.isOk()) s.stop("Dependencies installed");
|
|
3821
4071
|
else s.stop(pc.red("Failed to install dependencies"));
|
|
3822
4072
|
return result;
|
|
3823
4073
|
}
|
|
@@ -3922,7 +4172,7 @@ async function addHandlerInternal(input) {
|
|
|
3922
4172
|
}));
|
|
3923
4173
|
if (!isSilent()) {
|
|
3924
4174
|
renderTitle();
|
|
3925
|
-
intro(pc.magenta("Add
|
|
4175
|
+
intro(pc.magenta("Add to your project"));
|
|
3926
4176
|
}
|
|
3927
4177
|
const existingConfig = await detectProjectConfig(projectDir);
|
|
3928
4178
|
if (!existingConfig) return Result.err(new CLIError({ message: `No Better-T-Stack project found in ${projectDir}. Make sure bts.jsonc exists.` }));
|
|
@@ -3931,7 +4181,10 @@ async function addHandlerInternal(input) {
|
|
|
3931
4181
|
if (input.addons && input.addons.length > 0) {
|
|
3932
4182
|
addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingConfig.addons.includes(addon));
|
|
3933
4183
|
if (addonsToAdd.length === 0) {
|
|
3934
|
-
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
|
+
}
|
|
3935
4188
|
return Result.ok({
|
|
3936
4189
|
success: true,
|
|
3937
4190
|
addedAddons: [],
|
|
@@ -3953,10 +4206,7 @@ async function addHandlerInternal(input) {
|
|
|
3953
4206
|
if (promptResult.isErr()) return Result.err(promptResult.error);
|
|
3954
4207
|
const selectedAddons = promptResult.value;
|
|
3955
4208
|
if (selectedAddons.length === 0) {
|
|
3956
|
-
if (!isSilent())
|
|
3957
|
-
log.info(pc.dim("No addons selected."));
|
|
3958
|
-
outro(pc.magenta("Nothing to add."));
|
|
3959
|
-
}
|
|
4209
|
+
if (!isSilent()) outro(pc.dim("Nothing selected · project unchanged"));
|
|
3960
4210
|
return Result.ok({
|
|
3961
4211
|
success: true,
|
|
3962
4212
|
addedAddons: [],
|
|
@@ -3968,7 +4218,7 @@ async function addHandlerInternal(input) {
|
|
|
3968
4218
|
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3969
4219
|
const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
|
|
3970
4220
|
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
3971
|
-
if (!isSilent()) log.info(pc.
|
|
4221
|
+
if (!isSilent()) log.info(`${pc.dim("Adding")} ${pc.cyan(formatConfigValue(addonsToAdd))}`);
|
|
3972
4222
|
const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
|
|
3973
4223
|
const config = {
|
|
3974
4224
|
projectName: existingConfig.projectName,
|
|
@@ -3996,7 +4246,7 @@ async function addHandlerInternal(input) {
|
|
|
3996
4246
|
...config,
|
|
3997
4247
|
addons: updatedAddons
|
|
3998
4248
|
};
|
|
3999
|
-
if (!isSilent()) log.info(pc.dim("
|
|
4249
|
+
if (!isSilent()) log.info(pc.dim("Preparing addon files…"));
|
|
4000
4250
|
const vfs = new VirtualFileSystem();
|
|
4001
4251
|
for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
|
|
4002
4252
|
const fullPath = path.join(projectDir, pkgPath);
|
|
@@ -4028,9 +4278,9 @@ async function addHandlerInternal(input) {
|
|
|
4028
4278
|
};
|
|
4029
4279
|
if (input.dryRun) {
|
|
4030
4280
|
if (!isSilent()) {
|
|
4031
|
-
log.success(pc.green("Dry run
|
|
4032
|
-
log.
|
|
4033
|
-
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"));
|
|
4034
4284
|
}
|
|
4035
4285
|
return Result.ok({
|
|
4036
4286
|
success: true,
|
|
@@ -4061,17 +4311,17 @@ async function addHandlerInternal(input) {
|
|
|
4061
4311
|
addons: updatedAddons,
|
|
4062
4312
|
addonOptions: updatedConfig.addonOptions
|
|
4063
4313
|
});
|
|
4064
|
-
if (input.install) {
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
packageManager: config.packageManager
|
|
4069
|
-
});
|
|
4070
|
-
}
|
|
4314
|
+
if (input.install) await installDependencies({
|
|
4315
|
+
projectDir,
|
|
4316
|
+
packageManager: config.packageManager
|
|
4317
|
+
});
|
|
4071
4318
|
if (!isSilent()) {
|
|
4072
|
-
log.success(pc.green(`
|
|
4073
|
-
if (!input.install)
|
|
4074
|
-
|
|
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"));
|
|
4075
4325
|
}
|
|
4076
4326
|
return Result.ok({
|
|
4077
4327
|
success: true,
|
|
@@ -4104,7 +4354,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
|
|
|
4104
4354
|
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
4105
4355
|
});
|
|
4106
4356
|
const apiType = await navigableSelect({
|
|
4107
|
-
message: "
|
|
4357
|
+
message: "Choose an API layer",
|
|
4108
4358
|
options: apiOptions,
|
|
4109
4359
|
initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
|
|
4110
4360
|
});
|
|
@@ -4156,7 +4406,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
|
|
|
4156
4406
|
}
|
|
4157
4407
|
});
|
|
4158
4408
|
const response = await navigableSelect({
|
|
4159
|
-
message: "
|
|
4409
|
+
message: "Choose authentication",
|
|
4160
4410
|
options,
|
|
4161
4411
|
initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
|
|
4162
4412
|
});
|
|
@@ -4210,7 +4460,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
|
|
|
4210
4460
|
hint: "No backend server"
|
|
4211
4461
|
});
|
|
4212
4462
|
const response = await navigableSelect({
|
|
4213
|
-
message: "
|
|
4463
|
+
message: "Choose a backend",
|
|
4214
4464
|
options: backendOptions,
|
|
4215
4465
|
initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
|
|
4216
4466
|
});
|
|
@@ -4250,7 +4500,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
|
|
|
4250
4500
|
hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
|
|
4251
4501
|
});
|
|
4252
4502
|
const response = await navigableSelect({
|
|
4253
|
-
message: "
|
|
4503
|
+
message: "Choose a database",
|
|
4254
4504
|
options: databaseOptions,
|
|
4255
4505
|
initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
|
|
4256
4506
|
});
|
|
@@ -4349,7 +4599,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
|
|
|
4349
4599
|
];
|
|
4350
4600
|
else return "none";
|
|
4351
4601
|
const response = await navigableSelect({
|
|
4352
|
-
message: `
|
|
4602
|
+
message: `Choose a ${databaseType} setup`,
|
|
4353
4603
|
options,
|
|
4354
4604
|
initialValue: preferValidInitial(options, previousValue, "none")
|
|
4355
4605
|
});
|
|
@@ -4375,7 +4625,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
|
|
|
4375
4625
|
});
|
|
4376
4626
|
if (options.length === 0) return [];
|
|
4377
4627
|
response = await navigableMultiselect({
|
|
4378
|
-
message: "Include examples",
|
|
4628
|
+
message: "Include starter examples?",
|
|
4379
4629
|
options,
|
|
4380
4630
|
required: false,
|
|
4381
4631
|
initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
|
|
@@ -4402,7 +4652,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4402
4652
|
while (true) {
|
|
4403
4653
|
const wasFirstPrompt = isFirstPrompt();
|
|
4404
4654
|
const frontendTypes = await navigableMultiselect({
|
|
4405
|
-
message: "
|
|
4655
|
+
message: "What are you building?",
|
|
4406
4656
|
options: [{
|
|
4407
4657
|
value: "web",
|
|
4408
4658
|
label: "Web",
|
|
@@ -4464,7 +4714,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4464
4714
|
}
|
|
4465
4715
|
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
|
|
4466
4716
|
const webFramework = await navigableSelect({
|
|
4467
|
-
message: "Choose web",
|
|
4717
|
+
message: "Choose a web framework",
|
|
4468
4718
|
options: webOptions,
|
|
4469
4719
|
initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
|
|
4470
4720
|
});
|
|
@@ -4478,7 +4728,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4478
4728
|
}
|
|
4479
4729
|
if (frontendTypes.includes("native")) {
|
|
4480
4730
|
const nativeFramework = await navigableSelect({
|
|
4481
|
-
message: "Choose native",
|
|
4731
|
+
message: "Choose a native setup",
|
|
4482
4732
|
options: [
|
|
4483
4733
|
{
|
|
4484
4734
|
value: "native-bare",
|
|
@@ -4518,7 +4768,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
|
|
|
4518
4768
|
async function getGitChoice(git, previousValue) {
|
|
4519
4769
|
if (git !== void 0) return git;
|
|
4520
4770
|
const response = await navigableConfirm({
|
|
4521
|
-
message: "Initialize
|
|
4771
|
+
message: "Initialize a Git repository?",
|
|
4522
4772
|
initialValue: previousValue ?? DEFAULT_CONFIG.git
|
|
4523
4773
|
});
|
|
4524
4774
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
@@ -4770,7 +5020,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
|
|
|
4770
5020
|
}
|
|
4771
5021
|
const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
|
|
4772
5022
|
const response = await navigableSelect({
|
|
4773
|
-
message: "
|
|
5023
|
+
message: "Choose an ORM",
|
|
4774
5024
|
options,
|
|
4775
5025
|
initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
|
|
4776
5026
|
});
|
|
@@ -4822,7 +5072,7 @@ async function getPaymentsChoice(payments, auth, backend, _frontends, previousVa
|
|
|
4822
5072
|
hint: "No payments integration"
|
|
4823
5073
|
}];
|
|
4824
5074
|
const response = await navigableSelect({
|
|
4825
|
-
message: "
|
|
5075
|
+
message: "Add payments?",
|
|
4826
5076
|
options,
|
|
4827
5077
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
|
|
4828
5078
|
});
|
|
@@ -4849,7 +5099,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
|
|
|
4849
5099
|
hint: "Edge runtime on Cloudflare's global network"
|
|
4850
5100
|
});
|
|
4851
5101
|
const response = await navigableSelect({
|
|
4852
|
-
message: "
|
|
5102
|
+
message: "Choose a runtime",
|
|
4853
5103
|
options: runtimeOptions,
|
|
4854
5104
|
initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
|
|
4855
5105
|
});
|
|
@@ -4903,7 +5153,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
|
|
|
4903
5153
|
};
|
|
4904
5154
|
});
|
|
4905
5155
|
const response = await navigableSelect({
|
|
4906
|
-
message: "
|
|
5156
|
+
message: "Choose server deployment",
|
|
4907
5157
|
options,
|
|
4908
5158
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
|
|
4909
5159
|
});
|
|
@@ -4951,7 +5201,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
|
|
|
4951
5201
|
};
|
|
4952
5202
|
});
|
|
4953
5203
|
const response = await navigableSelect({
|
|
4954
|
-
message: "
|
|
5204
|
+
message: "Choose web deployment",
|
|
4955
5205
|
options,
|
|
4956
5206
|
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
|
|
4957
5207
|
});
|
|
@@ -4960,7 +5210,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
|
|
|
4960
5210
|
}
|
|
4961
5211
|
//#endregion
|
|
4962
5212
|
//#region src/prompts/config-prompts.ts
|
|
4963
|
-
async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
5213
|
+
async function gatherConfig(flags, projectName, projectDir, relativePath, options = {}) {
|
|
4964
5214
|
if (isSilent()) return {
|
|
4965
5215
|
projectName,
|
|
4966
5216
|
projectDir,
|
|
@@ -4988,22 +5238,63 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
4988
5238
|
frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
|
|
4989
5239
|
backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
|
|
4990
5240
|
runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
|
|
5241
|
+
api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
|
|
4991
5242
|
database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
|
|
4992
5243
|
orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
|
|
4993
|
-
|
|
5244
|
+
dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
|
|
4994
5245
|
auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
|
|
4995
5246
|
payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
|
|
4996
5247
|
addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
|
|
4997
5248
|
examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
|
|
4998
|
-
dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
|
|
4999
5249
|
webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
|
|
5000
5250
|
serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
|
|
5001
5251
|
git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
|
|
5002
5252
|
packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
|
|
5003
5253
|
install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
|
|
5004
|
-
}, {
|
|
5005
|
-
|
|
5006
|
-
|
|
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
|
+
});
|
|
5007
5298
|
return {
|
|
5008
5299
|
projectName,
|
|
5009
5300
|
projectDir,
|
|
@@ -5052,13 +5343,22 @@ async function getProjectName(initialName) {
|
|
|
5052
5343
|
let projectPath = "";
|
|
5053
5344
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
5054
5345
|
let counter = 1;
|
|
5055
|
-
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;
|
|
5056
5356
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
5057
5357
|
counter++;
|
|
5058
5358
|
}
|
|
5059
5359
|
while (!isValid) {
|
|
5060
5360
|
const response = await text({
|
|
5061
|
-
message: "
|
|
5361
|
+
message: "Where should we create your project?",
|
|
5062
5362
|
placeholder: defaultName,
|
|
5063
5363
|
initialValue: initialName,
|
|
5064
5364
|
defaultValue: defaultName,
|
|
@@ -5119,90 +5419,83 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
5119
5419
|
});
|
|
5120
5420
|
}
|
|
5121
5421
|
//#endregion
|
|
5122
|
-
//#region src/utils/
|
|
5123
|
-
function
|
|
5124
|
-
const
|
|
5125
|
-
|
|
5126
|
-
if (config.frontend !== void 0) {
|
|
5127
|
-
const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
|
|
5128
|
-
const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
|
|
5129
|
-
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
|
5130
|
-
}
|
|
5131
|
-
if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
|
|
5132
|
-
if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
|
|
5133
|
-
if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
|
|
5134
|
-
if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
|
|
5135
|
-
if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
|
|
5136
|
-
if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
|
|
5137
|
-
if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
|
|
5138
|
-
if (config.addons !== void 0) {
|
|
5139
|
-
const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
|
|
5140
|
-
const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
|
|
5141
|
-
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
|
|
5142
|
-
}
|
|
5143
|
-
if (config.examples !== void 0) {
|
|
5144
|
-
const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
|
|
5145
|
-
const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
|
|
5146
|
-
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
|
|
5147
|
-
}
|
|
5148
|
-
if (config.git !== void 0) {
|
|
5149
|
-
const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
|
|
5150
|
-
configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
|
|
5151
|
-
}
|
|
5152
|
-
if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
|
|
5153
|
-
if (config.install !== void 0) {
|
|
5154
|
-
const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
|
|
5155
|
-
configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
|
|
5156
|
-
}
|
|
5157
|
-
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
5158
|
-
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
5159
|
-
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
5160
|
-
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
5161
|
-
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}`);
|
|
5162
5426
|
}
|
|
5163
5427
|
//#endregion
|
|
5164
5428
|
//#region src/utils/project-directory.ts
|
|
5165
5429
|
async function handleDirectoryConflict(currentPathInput) {
|
|
5166
5430
|
while (true) {
|
|
5167
|
-
const
|
|
5168
|
-
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 {
|
|
5169
5435
|
finalPathInput: currentPathInput,
|
|
5170
5436
|
shouldClearDirectory: false
|
|
5171
5437
|
};
|
|
5172
|
-
if (isSilent()) throw new CLIError({ message: `
|
|
5173
|
-
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
|
+
});
|
|
5174
5473
|
const action = await select({
|
|
5175
|
-
message: "
|
|
5176
|
-
options
|
|
5177
|
-
|
|
5178
|
-
value: "overwrite",
|
|
5179
|
-
label: "Overwrite",
|
|
5180
|
-
hint: "Empty the directory and create the project"
|
|
5181
|
-
},
|
|
5182
|
-
{
|
|
5183
|
-
value: "merge",
|
|
5184
|
-
label: "Merge",
|
|
5185
|
-
hint: "Create project files inside, potentially overwriting conflicts"
|
|
5186
|
-
},
|
|
5187
|
-
{
|
|
5188
|
-
value: "rename",
|
|
5189
|
-
label: "Choose a different name/path",
|
|
5190
|
-
hint: "Keep the existing directory and create a new one"
|
|
5191
|
-
},
|
|
5192
|
-
{
|
|
5193
|
-
value: "cancel",
|
|
5194
|
-
label: "Cancel",
|
|
5195
|
-
hint: "Abort the process"
|
|
5196
|
-
}
|
|
5197
|
-
],
|
|
5198
|
-
initialValue: "rename"
|
|
5474
|
+
message: "How should we continue?",
|
|
5475
|
+
options,
|
|
5476
|
+
initialValue: incrementedPath ? "increment" : "rename"
|
|
5199
5477
|
});
|
|
5200
5478
|
if (isCancel(action)) throw new UserCancelledError({ message: "Operation cancelled." });
|
|
5201
5479
|
switch (action) {
|
|
5202
|
-
case "
|
|
5203
|
-
finalPathInput:
|
|
5204
|
-
shouldClearDirectory:
|
|
5480
|
+
case "increment": return {
|
|
5481
|
+
finalPathInput: incrementedPath,
|
|
5482
|
+
shouldClearDirectory: false
|
|
5205
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
|
+
}
|
|
5206
5499
|
case "merge":
|
|
5207
5500
|
log.info(`Proceeding into existing directory "${pc.yellow(currentPathInput)}". Files may be overwritten.`);
|
|
5208
5501
|
return {
|
|
@@ -5210,8 +5503,8 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
5210
5503
|
shouldClearDirectory: false
|
|
5211
5504
|
};
|
|
5212
5505
|
case "rename":
|
|
5213
|
-
|
|
5214
|
-
|
|
5506
|
+
currentPathInput = await getProjectName(void 0);
|
|
5507
|
+
continue;
|
|
5215
5508
|
case "cancel": throw new UserCancelledError({ message: "Operation cancelled." });
|
|
5216
5509
|
}
|
|
5217
5510
|
}
|
|
@@ -5226,9 +5519,11 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
5226
5519
|
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
|
5227
5520
|
finalBaseName = path.basename(finalResolvedPath);
|
|
5228
5521
|
}
|
|
5522
|
+
const pathSafetyResult = await validateSafeProjectDirectoryPath(finalPathInput);
|
|
5523
|
+
if (pathSafetyResult.isErr()) throw pathSafetyResult.error;
|
|
5229
5524
|
if (shouldClearDirectory) {
|
|
5230
|
-
const s = spinner();
|
|
5231
|
-
s
|
|
5525
|
+
const s = isSilent() ? void 0 : spinner();
|
|
5526
|
+
s?.start(`Clearing directory "${finalResolvedPath}"...`);
|
|
5232
5527
|
const clearResult = await Result.tryPromise({
|
|
5233
5528
|
try: () => fs.emptyDir(finalResolvedPath),
|
|
5234
5529
|
catch: (error) => new CLIError({
|
|
@@ -5237,16 +5532,78 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
5237
5532
|
})
|
|
5238
5533
|
});
|
|
5239
5534
|
if (clearResult.isErr()) {
|
|
5240
|
-
s
|
|
5535
|
+
s?.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
|
5241
5536
|
throw clearResult.error;
|
|
5242
5537
|
}
|
|
5243
|
-
s
|
|
5538
|
+
s?.stop(`Directory "${finalResolvedPath}" cleared.`);
|
|
5244
5539
|
} else await fs.ensureDir(finalResolvedPath);
|
|
5245
5540
|
return {
|
|
5246
5541
|
finalResolvedPath,
|
|
5247
5542
|
finalBaseName
|
|
5248
5543
|
};
|
|
5249
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
|
+
}
|
|
5250
5607
|
//#endregion
|
|
5251
5608
|
//#region src/utils/project-name-validation.ts
|
|
5252
5609
|
function validateProjectName(name) {
|
|
@@ -6956,7 +7313,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
6956
7313
|
const polarInstructions = config.payments === "polar" && config.auth === "better-auth" ? getPolarInstructions(backend, packageManager) : "";
|
|
6957
7314
|
const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
|
|
6958
7315
|
const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
|
|
6959
|
-
let output = `${pc.
|
|
7316
|
+
let output = `${pc.cyan("1.")} ${cdCmd}\n`;
|
|
6960
7317
|
let stepCounter = 2;
|
|
6961
7318
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
|
|
6962
7319
|
if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
|
|
@@ -6974,19 +7331,44 @@ async function displayPostInstallInstructions(config) {
|
|
|
6974
7331
|
}
|
|
6975
7332
|
const hasStandaloneBackend = backend !== "none";
|
|
6976
7333
|
if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
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";
|
|
6980
7341
|
if (!isConvex && !isBackendSelf && hasStandaloneBackend) {
|
|
6981
|
-
|
|
6982
|
-
|
|
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
|
+
});
|
|
6983
7350
|
}
|
|
6984
7351
|
if (isBackendSelf && api === "orpc") {
|
|
6985
7352
|
const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
|
|
6986
|
-
|
|
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`;
|
|
6987
7371
|
}
|
|
6988
|
-
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
6989
|
-
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
6990
7372
|
}
|
|
6991
7373
|
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
|
6992
7374
|
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
|
@@ -7006,10 +7388,15 @@ async function displayPostInstallInstructions(config) {
|
|
|
7006
7388
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
7007
7389
|
const sponsorsResult = await fetchSponsorsQuietly();
|
|
7008
7390
|
const specialSponsorsSection = sponsorsResult.isOk() ? formatPostInstallSpecialSponsorsSection(sponsorsResult.value) : "";
|
|
7009
|
-
|
|
7010
|
-
output
|
|
7011
|
-
|
|
7012
|
-
|
|
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")}`);
|
|
7013
7400
|
}
|
|
7014
7401
|
function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
|
|
7015
7402
|
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
|
|
@@ -7036,43 +7423,82 @@ function getVitePlusNativeHooksInstructions(runCmd) {
|
|
|
7036
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`;
|
|
7037
7424
|
}
|
|
7038
7425
|
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
|
|
7039
|
-
const
|
|
7426
|
+
const notes = [];
|
|
7427
|
+
const commands = [];
|
|
7040
7428
|
const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
|
|
7041
7429
|
if (dbSetup === "docker") {
|
|
7042
7430
|
const dockerStatus = await getDockerStatus(database);
|
|
7043
|
-
if (dockerStatus.message)
|
|
7044
|
-
instructions.push(dockerStatus.message);
|
|
7045
|
-
instructions.push("");
|
|
7046
|
-
}
|
|
7431
|
+
if (dockerStatus.message) notes.push(dockerStatus.message);
|
|
7047
7432
|
}
|
|
7048
7433
|
if (isD1Alchemy) {
|
|
7049
|
-
if (orm === "drizzle")
|
|
7434
|
+
if (orm === "drizzle") commands.push({
|
|
7435
|
+
label: "Generate migrations",
|
|
7436
|
+
command: `${runCmd} db:generate`
|
|
7437
|
+
});
|
|
7050
7438
|
else if (orm === "prisma") {
|
|
7051
|
-
|
|
7052
|
-
|
|
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
|
+
});
|
|
7053
7447
|
}
|
|
7054
7448
|
}
|
|
7055
7449
|
if (dbSetup === "planetscale") {
|
|
7056
|
-
if (database === "mysql" && orm === "drizzle")
|
|
7057
|
-
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`);
|
|
7058
7452
|
}
|
|
7059
|
-
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`);
|
|
7060
7454
|
if (orm === "prisma") {
|
|
7061
|
-
if (database === "mongodb" && dbSetup === "docker")
|
|
7062
|
-
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
|
+
});
|
|
7063
7460
|
if (!isD1Alchemy) {
|
|
7064
|
-
|
|
7065
|
-
|
|
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
|
+
});
|
|
7066
7469
|
}
|
|
7067
|
-
if (!isD1Alchemy)
|
|
7470
|
+
if (!isD1Alchemy) commands.push({
|
|
7471
|
+
label: "Open studio",
|
|
7472
|
+
command: `${runCmd} db:studio`
|
|
7473
|
+
});
|
|
7068
7474
|
} else if (orm === "drizzle") {
|
|
7069
|
-
if (dbSetup === "docker")
|
|
7070
|
-
|
|
7071
|
-
|
|
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
|
+
});
|
|
7072
7487
|
} else if (orm === "mongoose") {
|
|
7073
|
-
if (dbSetup === "docker")
|
|
7074
|
-
|
|
7075
|
-
|
|
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");
|
|
7076
7502
|
}
|
|
7077
7503
|
function getTauriInstructions(runCmd, frontend) {
|
|
7078
7504
|
const staticBuildNote = getDesktopStaticBuildNote(frontend);
|
|
@@ -7239,7 +7665,7 @@ async function createProject(options, cliInput = {}) {
|
|
|
7239
7665
|
})
|
|
7240
7666
|
}));
|
|
7241
7667
|
yield* Result.await(formatProject(projectDir));
|
|
7242
|
-
if (!isSilent()) log.success("Project
|
|
7668
|
+
if (!isSilent()) log.success("Project scaffolded");
|
|
7243
7669
|
if (options.install) yield* Result.await(installDependencies({
|
|
7244
7670
|
projectDir,
|
|
7245
7671
|
packageManager: options.packageManager
|
|
@@ -7334,19 +7760,16 @@ async function createProjectHandler(input, options = {}) {
|
|
|
7334
7760
|
async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
7335
7761
|
return Result.gen(async function* () {
|
|
7336
7762
|
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
7337
|
-
if (!isSilent()) intro(pc.magenta("
|
|
7338
|
-
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."));
|
|
7339
7765
|
let currentPathInput;
|
|
7340
7766
|
if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
|
|
7341
7767
|
else if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
7342
7768
|
else if (input.yes) {
|
|
7343
7769
|
const defaultConfig = getDefaultConfig();
|
|
7344
7770
|
let defaultName = defaultConfig.relativePath;
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
7348
|
-
counter++;
|
|
7349
|
-
}
|
|
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));
|
|
7350
7773
|
currentPathInput = defaultName;
|
|
7351
7774
|
} else currentPathInput = yield* Result.await(Result.tryPromise({
|
|
7352
7775
|
try: async () => getProjectName(input.projectName),
|
|
@@ -7365,6 +7788,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7365
7788
|
finalPathInput = conflictResult.finalPathInput;
|
|
7366
7789
|
shouldClearDirectory = conflictResult.shouldClearDirectory;
|
|
7367
7790
|
yield* validateResolvedProjectPathInput(finalPathInput);
|
|
7791
|
+
yield* Result.await(validateSafeProjectDirectoryPath(finalPathInput));
|
|
7368
7792
|
let finalResolvedPath;
|
|
7369
7793
|
let finalBaseName;
|
|
7370
7794
|
if (input.dryRun) {
|
|
@@ -7395,10 +7819,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7395
7819
|
if (templateConfig) {
|
|
7396
7820
|
const templateName = input.template.toUpperCase();
|
|
7397
7821
|
const templateDescription = getTemplateDescription(input.template);
|
|
7398
|
-
if (!isSilent()) {
|
|
7399
|
-
log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
|
|
7400
|
-
log.message(pc.dim(` ${templateDescription}`));
|
|
7401
|
-
}
|
|
7822
|
+
if (!isSilent()) log.info(`${pc.dim("Template")} ${pc.bold(pc.cyan(templateName))}\n${pc.dim(templateDescription)}`);
|
|
7402
7823
|
const userOverrides = {};
|
|
7403
7824
|
for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
|
|
7404
7825
|
cliInput = {
|
|
@@ -7429,10 +7850,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7429
7850
|
message: validationResult.error.message,
|
|
7430
7851
|
cause: validationResult.error
|
|
7431
7852
|
}));
|
|
7432
|
-
if (!isSilent())
|
|
7433
|
-
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
7434
|
-
log.message(displayConfig(config));
|
|
7435
|
-
}
|
|
7853
|
+
if (!isSilent()) log.info(pc.dim("Quick setup selected — using defaults and provided flags."));
|
|
7436
7854
|
} else {
|
|
7437
7855
|
const flagConfigResult = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
7438
7856
|
if (flagConfigResult.isErr()) return Result.err(new CLIError({
|
|
@@ -7441,13 +7859,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7441
7859
|
}));
|
|
7442
7860
|
const flagConfig = flagConfigResult.value;
|
|
7443
7861
|
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
log.message(displayConfig(otherFlags));
|
|
7447
|
-
log.message("");
|
|
7448
|
-
}
|
|
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."));
|
|
7449
7864
|
config = yield* Result.await(Result.tryPromise({
|
|
7450
|
-
try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput),
|
|
7865
|
+
try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput, { skipCompatibilityChecks: cliInput.yolo }),
|
|
7451
7866
|
catch: (e) => {
|
|
7452
7867
|
if (e instanceof UserCancelledError) return e;
|
|
7453
7868
|
return new CLIError({
|
|
@@ -7469,12 +7884,16 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7469
7884
|
const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
7470
7885
|
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
7471
7886
|
}
|
|
7887
|
+
if (!isSilent()) {
|
|
7888
|
+
log.info(pc.magenta(pc.bold("Stack ready")));
|
|
7889
|
+
log.message(displayConfig(config));
|
|
7890
|
+
}
|
|
7472
7891
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
7473
7892
|
if (input.dryRun) {
|
|
7474
7893
|
const elapsedTimeMs = Date.now() - startTime;
|
|
7475
7894
|
if (!isSilent()) {
|
|
7476
7895
|
if (shouldClearDirectory) log.warn(pc.yellow(`Dry run: directory "${finalPathInput}" would be cleared due to overwrite strategy.`));
|
|
7477
|
-
log.success(pc.green("
|
|
7896
|
+
log.success(pc.green("Configuration ready. No files were written."));
|
|
7478
7897
|
log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
|
|
7479
7898
|
log.message(pc.dim(`Run without --dry-run to create the project.`));
|
|
7480
7899
|
outro(pc.magenta("Dry run complete."));
|
|
@@ -7493,14 +7912,19 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
7493
7912
|
manualDb: cliInput.manualDb ?? input.manualDb,
|
|
7494
7913
|
dbSetupOptions: effectiveDbSetupOptions
|
|
7495
7914
|
}));
|
|
7496
|
-
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
7497
7915
|
await trackProjectCreation(config, input.disableAnalytics);
|
|
7498
7916
|
const historyResult = await addToHistory(config, reproducibleCommand);
|
|
7499
|
-
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
|
+
}
|
|
7500
7924
|
const elapsedTimeMs = Date.now() - startTime;
|
|
7501
7925
|
if (!isSilent()) {
|
|
7502
|
-
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(
|
|
7503
|
-
outro(pc.magenta(`Project
|
|
7926
|
+
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(1);
|
|
7927
|
+
outro(pc.magenta(`Project ready in ${pc.bold(`${elapsedTimeInSeconds}s`)}`));
|
|
7504
7928
|
}
|
|
7505
7929
|
return Result.ok({
|
|
7506
7930
|
success: true,
|
|
@@ -7553,15 +7977,23 @@ async function handleDirectoryConflictResult(currentPathInput, strategy) {
|
|
|
7553
7977
|
});
|
|
7554
7978
|
}
|
|
7555
7979
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
7556
|
-
const
|
|
7557
|
-
if (
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
});
|
|
7561
|
-
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({
|
|
7562
7984
|
finalPathInput: currentPathInput,
|
|
7563
7985
|
shouldClearDirectory: false
|
|
7564
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".` }));
|
|
7565
7997
|
switch (strategy) {
|
|
7566
7998
|
case "overwrite": return Result.ok({
|
|
7567
7999
|
finalPathInput: currentPathInput,
|
|
@@ -7571,19 +8003,6 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
7571
8003
|
finalPathInput: currentPathInput,
|
|
7572
8004
|
shouldClearDirectory: false
|
|
7573
8005
|
});
|
|
7574
|
-
case "increment": {
|
|
7575
|
-
let counter = 1;
|
|
7576
|
-
const baseName = currentPathInput;
|
|
7577
|
-
let finalPathInput = `${baseName}-${counter}`;
|
|
7578
|
-
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
7579
|
-
counter++;
|
|
7580
|
-
finalPathInput = `${baseName}-${counter}`;
|
|
7581
|
-
}
|
|
7582
|
-
return Result.ok({
|
|
7583
|
-
finalPathInput,
|
|
7584
|
-
shouldClearDirectory: false
|
|
7585
|
-
});
|
|
7586
|
-
}
|
|
7587
8006
|
case "error": return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
|
|
7588
8007
|
default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
|
|
7589
8008
|
}
|
|
@@ -7618,6 +8037,10 @@ const SchemaNameSchema = z.enum([
|
|
|
7618
8037
|
"betterTStackConfigFile",
|
|
7619
8038
|
"initResult"
|
|
7620
8039
|
]).default("all");
|
|
8040
|
+
const CreateVirtualInputSchema = types_exports.ProjectConfigSchema.omit({
|
|
8041
|
+
projectDir: true,
|
|
8042
|
+
relativePath: true
|
|
8043
|
+
}).partial().strict();
|
|
7621
8044
|
const t = initTRPC.meta().create();
|
|
7622
8045
|
function getCliSchemaJson() {
|
|
7623
8046
|
return createCli({
|
|
@@ -7673,7 +8096,7 @@ const router = t.router({
|
|
|
7673
8096
|
projectName,
|
|
7674
8097
|
...options
|
|
7675
8098
|
});
|
|
7676
|
-
if (options.verbose
|
|
8099
|
+
if (options.verbose) return result;
|
|
7677
8100
|
}),
|
|
7678
8101
|
createJson: t.procedure.meta({
|
|
7679
8102
|
description: "Create a project from a raw JSON payload (agent-friendly)",
|
|
@@ -7692,7 +8115,8 @@ const router = t.router({
|
|
|
7692
8115
|
addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
|
|
7693
8116
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
|
|
7694
8117
|
packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
|
|
7695
|
-
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")
|
|
7696
8120
|
})).mutation(async ({ input }) => {
|
|
7697
8121
|
await addHandler(input);
|
|
7698
8122
|
}),
|
|
@@ -7720,6 +8144,12 @@ function createBtsCli() {
|
|
|
7720
8144
|
version: getLatestCLIVersion()
|
|
7721
8145
|
});
|
|
7722
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
|
+
}
|
|
7723
8153
|
/**
|
|
7724
8154
|
* Programmatic API to create a new Better-T-Stack project.
|
|
7725
8155
|
* Returns a Result type - no console output, no interactive prompts.
|
|
@@ -7746,13 +8176,21 @@ function createBtsCli() {
|
|
|
7746
8176
|
* ```
|
|
7747
8177
|
*/
|
|
7748
8178
|
async function create(projectName, options) {
|
|
7749
|
-
const
|
|
8179
|
+
const rawInput = options === void 0 || typeof options === "object" && options !== null ? {
|
|
7750
8180
|
...options,
|
|
7751
|
-
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,
|
|
7752
8190
|
renderTitle: false,
|
|
7753
8191
|
verbose: true,
|
|
7754
|
-
disableAnalytics:
|
|
7755
|
-
directoryConflict:
|
|
8192
|
+
disableAnalytics: parsedInput.data.disableAnalytics ?? true,
|
|
8193
|
+
directoryConflict: parsedInput.data.directoryConflict ?? "error"
|
|
7756
8194
|
};
|
|
7757
8195
|
return Result.tryPromise({
|
|
7758
8196
|
try: async () => {
|
|
@@ -7805,28 +8243,35 @@ async function builder() {
|
|
|
7805
8243
|
* ```
|
|
7806
8244
|
*/
|
|
7807
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;
|
|
7808
8253
|
const config = {
|
|
7809
|
-
projectName:
|
|
8254
|
+
projectName: virtualOptions.projectName || "my-project",
|
|
7810
8255
|
projectDir: "/virtual",
|
|
7811
8256
|
relativePath: "./virtual",
|
|
7812
|
-
addonOptions:
|
|
7813
|
-
dbSetupOptions:
|
|
7814
|
-
database:
|
|
7815
|
-
orm:
|
|
7816
|
-
backend:
|
|
7817
|
-
runtime:
|
|
7818
|
-
frontend:
|
|
7819
|
-
addons:
|
|
7820
|
-
examples:
|
|
7821
|
-
auth:
|
|
7822
|
-
payments:
|
|
7823
|
-
git:
|
|
7824
|
-
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",
|
|
7825
8270
|
install: false,
|
|
7826
|
-
dbSetup:
|
|
7827
|
-
api:
|
|
7828
|
-
webDeploy:
|
|
7829
|
-
serverDeploy:
|
|
8271
|
+
dbSetup: virtualOptions.dbSetup || "none",
|
|
8272
|
+
api: virtualOptions.api || "trpc",
|
|
8273
|
+
webDeploy: virtualOptions.webDeploy || "none",
|
|
8274
|
+
serverDeploy: virtualOptions.serverDeploy || "none"
|
|
7830
8275
|
};
|
|
7831
8276
|
const validationResult = validateConfigCompatibility(config, new Set([
|
|
7832
8277
|
"database",
|
|
@@ -7865,13 +8310,25 @@ async function createVirtual(options) {
|
|
|
7865
8310
|
* install: true,
|
|
7866
8311
|
* });
|
|
7867
8312
|
*
|
|
7868
|
-
* if (result
|
|
8313
|
+
* if (result.success) {
|
|
7869
8314
|
* console.log(`Added: ${result.addedAddons.join(", ")}`);
|
|
7870
8315
|
* }
|
|
7871
8316
|
* ```
|
|
7872
8317
|
*/
|
|
7873
8318
|
async function add(options = {}) {
|
|
7874
|
-
|
|
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
|
+
};
|
|
7875
8332
|
}
|
|
7876
8333
|
//#endregion
|
|
7877
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 };
|