create-better-t-stack 3.36.5 → 3.38.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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { n as __reExport, t as __exportAll } from "./chunk-BtN16TXe.mjs";
2
+ import { n as __reExport, t as __exportAll } from "./rolldown-runtime-yhw22V8Z.mjs";
3
3
  import { getAllJsonSchemas } from "@better-t-stack/types/json-schema";
4
4
  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.join(", "));
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 History (${entries.length} entries)`));
496
- for (const [index, entry] of entries.entries()) {
497
- const num = pc.dim(`${index + 1}.`);
498
- const name = pc.cyan(pc.bold(entry.projectName));
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. You can be the first one! ✨");
579
- outro(pc.cyan(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
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.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
584
- outro(pc.magenta(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
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
- let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
589
- sponsors.specialSponsors.forEach((sponsor, idx) => {
590
- const displayName = sponsor.name ?? sponsor.githubId;
591
- const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
592
- output += `${pc.green(`• ${displayName}`)}${tier}\n`;
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
- cliConsola.box(output);
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 fetched successfully!");
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("Better-T-Stack Sponsors"));
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(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back${pc.gray("ctrl+c")} cancel`);
1013
- const KEYBOARD_HINT_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
1014
- const KEYBOARD_HINT_MULTI = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("b")} back${pc.gray("ctrl+c")} cancel`);
1015
- const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
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
- const result = await prompt.prompt();
1036
- return goBack ? GO_BACK_SYMBOL : result;
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.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
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 `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
1056
- case "cancel": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR)}`;
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 = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
1059
- const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
1060
- return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
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 `${title}${pc.gray(S_BAR)} ${submitText}`;
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 `${title}${pc.gray(S_BAR)} ${label}\n${pc.gray(S_BAR)}`;
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 = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
1108
- return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
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 = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
1112
- const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
1113
- return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
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 `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
1131
- case "cancel": return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR)}`;
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 = `\n${pc.gray(S_BAR)} ${getHint()}`;
1134
- return `${title}${pc.cyan(S_BAR)} ${this.value ? `${pc.green(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.green(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${pc.cyan(S_BAR_END)}${hint}\n`;
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 selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
1176
- const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
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 `${title}${pc.gray(S_BAR)} ${label.trim() ? `${label}\n${pc.gray(S_BAR)}` : ""}`;
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 = this.options.map((option, i, options) => {
1186
- const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1187
- const active = i === this.cursor;
1188
- if (!active && typeof option.group === "string" && this.options[this.cursor].value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", options);
1189
- if (active && selected) return opt(option, "active-selected", options);
1190
- if (selected) return opt(option, "selected", options);
1191
- return opt(option, active ? "active" : "inactive", options);
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 `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
1406
+ return `${activePromptTitle(opts.message, "error")}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
1194
1407
  }
1195
1408
  default: {
1196
- const optionsText = this.options.map((option, i, options) => {
1197
- const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1198
- const active = i === this.cursor;
1199
- const groupActive = !active && typeof option.group === "string" && this.options[this.cursor].value === option.group;
1200
- let optionText = "";
1201
- if (groupActive) optionText = opt(option, selected ? "group-active-selected" : "group-active", options);
1202
- else if (active && selected) optionText = opt(option, "active-selected", options);
1203
- else if (selected) optionText = opt(option, "selected", options);
1204
- else optionText = opt(option, active ? "active" : "inactive", options);
1205
- return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
1206
- }).join(`\n${pc.cyan(S_BAR)}`);
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: "Select addons",
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,
@@ -1491,15 +1701,37 @@ const evlogWebFrontends = [
1491
1701
  "tanstack-start",
1492
1702
  "astro"
1493
1703
  ];
1704
+ const NODE_DEV_FS_DRAIN_EXPRESSION = "process.env.NODE_ENV === \"production\" ? undefined : createFsDrain()";
1705
+ const SVELTE_DEV_FS_DRAIN_EXPRESSION = "dev ? createFsDrain() : undefined";
1706
+ const ASTRO_DEV_FS_DRAIN_EXPRESSION = "import.meta.env.DEV ? createFsDrain() : undefined";
1494
1707
  function isEvlogBackend(backend) {
1495
1708
  return evlogBackends.includes(backend);
1496
1709
  }
1497
1710
  function getEvlogWebFrontend(frontends) {
1498
1711
  return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
1499
1712
  }
1713
+ function shouldWireEvlogServerFsDrain(config) {
1714
+ return isEvlogBackend(config.backend) && config.runtime !== "workers" && config.serverDeploy !== "cloudflare";
1715
+ }
1716
+ function shouldWireEvlogWebFsDrain(config) {
1717
+ return getEvlogWebFrontend(config.frontend) !== void 0 && config.webDeploy !== "cloudflare";
1718
+ }
1719
+ function supportsEvlogLocalLogs(config) {
1720
+ return shouldWireEvlogServerFsDrain(config) || shouldWireEvlogWebFsDrain(config);
1721
+ }
1500
1722
  function shouldIdentifyWebAuth(config) {
1501
1723
  return config.auth === "better-auth" && config.backend === "self";
1502
1724
  }
1725
+ function getEvlogServerMiddlewareMarker(backend, fsDrain) {
1726
+ const options = fsDrain ? `{ drain: ${NODE_DEV_FS_DRAIN_EXPRESSION} }` : "";
1727
+ if (backend === "hono" || backend === "express") return `app.use(evlog(${options}));`;
1728
+ if (backend === "fastify") return `fastify.register(evlog${options ? `, ${options}` : ""});`;
1729
+ return `.use(evlog(${options}))`;
1730
+ }
1731
+ function findEvlogServerMiddlewareMarker(content, backend) {
1732
+ const fsDrainMarker = getEvlogServerMiddlewareMarker(backend, true);
1733
+ return content.includes(fsDrainMarker) ? fsDrainMarker : getEvlogServerMiddlewareMarker(backend, false);
1734
+ }
1503
1735
  function prependMissingImports(content, imports) {
1504
1736
  const missingImports = imports.filter((line) => !content.includes(line));
1505
1737
  if (missingImports.length === 0) return content;
@@ -1561,46 +1793,72 @@ function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
1561
1793
  const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
1562
1794
  const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
1563
1795
  if (backend === "hono") {
1796
+ const evlogMarker = findEvlogServerMiddlewareMarker(nextContent, backend);
1564
1797
  nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
1565
- 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\")");
1798
+ return insertAfterOnce(nextContent, evlogMarker, `\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\")");
1566
1799
  }
1567
1800
  if (backend === "express") {
1801
+ const evlogMarker = findEvlogServerMiddlewareMarker(nextContent, backend);
1568
1802
  nextContent = addNamedImport(nextContent, "evlog/express", ["useLogger"]);
1569
1803
  nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
1570
- 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()");
1804
+ return insertAfterOnce(nextContent, evlogMarker, `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), req.headers, req.path);\n\tnext();\n});`, "identifyUser(useLogger()");
1571
1805
  }
1572
1806
  if (backend === "fastify") {
1807
+ const evlogMarker = findEvlogServerMiddlewareMarker(nextContent, backend);
1573
1808
  nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
1574
1809
  nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
1575
- return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
1810
+ return insertAfterOnce(nextContent, evlogMarker, `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
1576
1811
  }
1577
1812
  const elysiaMarker = nextContent.includes("const app = new Elysia") ? "const app = new Elysia" : "new Elysia";
1578
1813
  nextContent = insertBeforeOnce(nextContent, elysiaMarker, identifySnippet, "createAuthMiddleware(");
1579
- 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");
1814
+ const evlogMarker = findEvlogServerMiddlewareMarker(nextContent, backend);
1815
+ return insertAfterOnce(nextContent, evlogMarker, `\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");
1580
1816
  }
1581
- function addEvlogServerSetup(content, backend, serviceName) {
1817
+ function addEvlogServerSetup(content, backend, serviceName, fsDrain) {
1582
1818
  const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
1819
+ const evlogMarker = getEvlogServerMiddlewareMarker(backend, fsDrain);
1820
+ const legacyEvlogMarker = getEvlogServerMiddlewareMarker(backend, false);
1583
1821
  if (backend === "hono") {
1584
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
1822
+ let nextContent = prependMissingImports(content, [
1823
+ "import { initLogger } from \"evlog\";",
1824
+ "import { evlog, type EvlogVariables } from \"evlog/hono\";",
1825
+ ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []
1826
+ ]);
1585
1827
  nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
1586
1828
  nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
1587
1829
  nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
1588
- return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
1830
+ if (fsDrain) nextContent = nextContent.replace(legacyEvlogMarker, evlogMarker);
1831
+ return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", `\n\n${evlogMarker}`, evlogMarker);
1589
1832
  }
1590
1833
  if (backend === "express") {
1591
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
1834
+ let nextContent = prependMissingImports(content, [
1835
+ "import { initLogger } from \"evlog\";",
1836
+ "import { evlog } from \"evlog/express\";",
1837
+ ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []
1838
+ ]);
1592
1839
  nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
1593
- return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
1840
+ if (fsDrain) nextContent = nextContent.replace(legacyEvlogMarker, evlogMarker);
1841
+ return insertAfterOnce(nextContent, "const app = express();", `\n\n${evlogMarker}`, evlogMarker);
1594
1842
  }
1595
1843
  if (backend === "fastify") {
1596
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
1844
+ let nextContent = prependMissingImports(content, [
1845
+ "import { initLogger } from \"evlog\";",
1846
+ "import { evlog } from \"evlog/fastify\";",
1847
+ ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []
1848
+ ]);
1597
1849
  nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
1598
- return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
1850
+ if (fsDrain) nextContent = nextContent.replace(legacyEvlogMarker, evlogMarker);
1851
+ return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", `${evlogMarker}\n`, evlogMarker);
1599
1852
  }
1600
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
1853
+ let nextContent = prependMissingImports(content, [
1854
+ "import { initLogger } from \"evlog\";",
1855
+ "import { evlog } from \"evlog/elysia\";",
1856
+ ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []
1857
+ ]);
1601
1858
  const elysiaMarker = nextContent.includes("const app = new Elysia") ? "const app = new Elysia" : "new Elysia";
1602
1859
  nextContent = insertBeforeOnce(nextContent, elysiaMarker, initSnippet, "initLogger({");
1603
- for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
1860
+ if (fsDrain) nextContent = nextContent.replace(legacyEvlogMarker, evlogMarker);
1861
+ for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, `\n\t${evlogMarker}`, evlogMarker);
1604
1862
  return nextContent;
1605
1863
  }
1606
1864
  function addNuxtEvlogSetup(content, serviceName) {
@@ -1617,14 +1875,20 @@ function addSvelteViteEvlogSetup(content, serviceName) {
1617
1875
  if (nextContent.includes("evlog({")) return nextContent;
1618
1876
  return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
1619
1877
  }
1620
- function addSvelteHooksEvlogSetup(content) {
1621
- let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
1878
+ function getSvelteEvlogHooksCall(fsDrain) {
1879
+ return fsDrain ? `createEvlogHooks({ drain: ${SVELTE_DEV_FS_DRAIN_EXPRESSION} })` : "createEvlogHooks()";
1880
+ }
1881
+ function addSvelteHooksEvlogSetup(content, fsDrain) {
1882
+ let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";", ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []]);
1883
+ if (fsDrain) nextContent = addNamedImport(nextContent, "$app/environment", ["dev"]);
1884
+ const hooksCall = getSvelteEvlogHooksCall(fsDrain);
1885
+ if (fsDrain) nextContent = nextContent.replaceAll("createEvlogHooks()", hooksCall);
1622
1886
  if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
1623
- if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
1887
+ if (!nextContent.includes("createEvlogHooks(")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = ${hooksCall};\n`;
1624
1888
  return nextContent;
1625
1889
  }
1626
1890
  nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
1627
- if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
1891
+ if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = ${hooksCall};\n\n`);
1628
1892
  nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
1629
1893
  if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
1630
1894
  return nextContent;
@@ -1643,10 +1907,14 @@ function addTanstackStartRootEvlogSetup(content) {
1643
1907
  if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
1644
1908
  return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
1645
1909
  }
1646
- function addAstroMiddlewareEvlogSetup(content, serviceName) {
1647
- let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
1648
- const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
1910
+ function getInitLoggerSnippet(serviceName, fsDrain, indent) {
1911
+ return `initLogger({\n${indent}env: { service: "${serviceName}" },${fsDrain ? `\n${indent}drain: ${ASTRO_DEV_FS_DRAIN_EXPRESSION},` : ""}\n});\n\n`;
1912
+ }
1913
+ function addAstroMiddlewareEvlogSetup(content, serviceName, fsDrain) {
1914
+ let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";", ...fsDrain ? ["import { createFsDrain } from \"evlog/fs\";"] : []]);
1915
+ const initSnippet = getInitLoggerSnippet(serviceName, fsDrain, " ");
1649
1916
  nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
1917
+ if (fsDrain && !nextContent.includes("drain:")) nextContent = nextContent.replace(/initLogger\(\{\n(\s+env: \{ service: "[^"]+" \},)/, `initLogger({\n$1\n drain: ${ASTRO_DEV_FS_DRAIN_EXPRESSION},`);
1650
1918
  if (nextContent.includes("createRequestLogger({")) return nextContent;
1651
1919
  const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
1652
1920
  if (nextContent.includes(contextMarker)) {
@@ -1721,7 +1989,8 @@ function addSvelteBetterAuthEvlogSetup(content, config) {
1721
1989
  const authExpression = getAuthExpression(config);
1722
1990
  const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1723
1991
  const authHandleSnippet = usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" ? `const evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tif (building) {\n\t\treturn resolve(event);\n\t}\n\n\tconst authEnv = event.platform?.env ?? localEnv;\n\tconst identifyUser = createAuthMiddleware(createAuth(authEnv) as BetterAuthInstance, ${authOptions});\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n` : `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\nconst evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n`;
1724
- nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
1992
+ const evlogHandleDeclaration = nextContent.match(/const \{ handle: evlogHandle, handleError \} = createEvlogHooks\([\s\S]*?\);\n\n/)?.[0];
1993
+ if (evlogHandleDeclaration) nextContent = insertAfterOnce(nextContent, evlogHandleDeclaration, authHandleSnippet, "evlogAuthHandle");
1725
1994
  return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
1726
1995
  }
1727
1996
  function addAstroBetterAuthEvlogSetup(content, config) {
@@ -1740,19 +2009,29 @@ function addAstroBetterAuthEvlogSetup(content, config) {
1740
2009
  }
1741
2010
  return nextContent;
1742
2011
  }
1743
- function getNextEvlogFile(serviceName) {
2012
+ function getNextEvlogFile(serviceName, fsDrain) {
1744
2013
  return `import { createEvlog } from "evlog/next";
1745
2014
  import { createInstrumentation } from "evlog/next/instrumentation/create";
2015
+ ${fsDrain ? "import { createFsDrain } from \"evlog/fs\";\n" : ""}
1746
2016
 
1747
2017
  export const { withEvlog, useLogger, log, createError } = createEvlog({
1748
2018
  service: "${serviceName}",
1749
- });
2019
+ ${fsDrain ? ` drain: ${NODE_DEV_FS_DRAIN_EXPRESSION},\n` : ""}});
1750
2020
 
1751
2021
  export const { register, onRequestError } = createInstrumentation({
1752
2022
  service: "${serviceName}",
1753
2023
  });
1754
2024
  `;
1755
2025
  }
2026
+ function getNitroEvlogDrainFile() {
2027
+ return `import { createFsDrain } from "evlog/fs";
2028
+
2029
+ export default defineNitroPlugin((nitroApp) => {
2030
+ if (!import.meta.dev) return;
2031
+ nitroApp.hooks.hook("evlog:drain", createFsDrain());
2032
+ });
2033
+ `;
2034
+ }
1756
2035
  function getNextInstrumentationFile() {
1757
2036
  return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
1758
2037
 
@@ -1867,13 +2146,12 @@ export default defineConfig({
1867
2146
  });
1868
2147
  `;
1869
2148
  }
1870
- function getAstroMiddlewareFile(serviceName) {
2149
+ function getAstroMiddlewareFile(serviceName, fsDrain) {
1871
2150
  return `import { defineMiddleware } from "astro:middleware";
1872
2151
  import { createRequestLogger, initLogger } from "evlog";
2152
+ ${fsDrain ? "import { createFsDrain } from \"evlog/fs\";\n" : ""}
1873
2153
 
1874
- initLogger({
1875
- env: { service: "${serviceName}" },
1876
- });
2154
+ ${getInitLoggerSnippet(serviceName, fsDrain, " ").trimEnd()}
1877
2155
 
1878
2156
  export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
1879
2157
  const url = new URL(request.url);
@@ -1910,8 +2188,9 @@ declare namespace App {
1910
2188
  }
1911
2189
  async function setupNextEvlog(config, serviceName) {
1912
2190
  const webDir = path.join(config.projectDir, "apps/web");
2191
+ const fsDrain = shouldWireEvlogWebFsDrain(config);
1913
2192
  const evlogPath = path.join(webDir, "src/lib/evlog.ts");
1914
- if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
2193
+ if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName, fsDrain));
1915
2194
  const identifyWebAuth = shouldIdentifyWebAuth(config);
1916
2195
  if (identifyWebAuth) {
1917
2196
  const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
@@ -1937,7 +2216,12 @@ async function setupNextEvlog(config, serviceName) {
1937
2216
  }
1938
2217
  async function setupNuxtEvlog(config, serviceName) {
1939
2218
  const webDir = path.join(config.projectDir, "apps/web");
2219
+ const fsDrain = shouldWireEvlogWebFsDrain(config);
1940
2220
  await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
2221
+ if (fsDrain) {
2222
+ const drainPath = path.join(webDir, "server/plugins/evlog-drain.ts");
2223
+ if (!await fs.pathExists(drainPath)) await writeFileIfChanged(drainPath, getNitroEvlogDrainFile());
2224
+ }
1941
2225
  if (shouldIdentifyWebAuth(config)) {
1942
2226
  const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1943
2227
  if (await fs.pathExists(oldAuthPluginPath)) {
@@ -1950,12 +2234,15 @@ async function setupNuxtEvlog(config, serviceName) {
1950
2234
  }
1951
2235
  async function setupSvelteEvlog(config, serviceName) {
1952
2236
  const webDir = path.join(config.projectDir, "apps/web");
2237
+ const fsDrain = shouldWireEvlogWebFsDrain(config);
1953
2238
  await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
1954
2239
  const hooksPath = path.join(webDir, "src/hooks.server.ts");
1955
- if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
2240
+ if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, (content) => addSvelteHooksEvlogSetup(content, fsDrain));
1956
2241
  else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
2242
+ ${fsDrain ? "import { createFsDrain } from \"evlog/fs\";\n" : ""}
2243
+ ${fsDrain ? "import { dev } from \"$app/environment\";\n" : ""}
1957
2244
 
1958
- export const { handle, handleError } = createEvlogHooks();
2245
+ export const { handle, handleError } = ${getSvelteEvlogHooksCall(fsDrain)};
1959
2246
  `);
1960
2247
  await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
1961
2248
  if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
@@ -1963,9 +2250,14 @@ export const { handle, handleError } = createEvlogHooks();
1963
2250
  }
1964
2251
  async function setupTanstackStartEvlog(config, serviceName) {
1965
2252
  const webDir = path.join(config.projectDir, "apps/web");
2253
+ const fsDrain = shouldWireEvlogWebFsDrain(config);
1966
2254
  const nitroConfigPath = path.join(webDir, "nitro.config.ts");
1967
2255
  if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
1968
2256
  await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
2257
+ if (fsDrain) {
2258
+ const drainPath = path.join(webDir, "server/plugins/evlog-drain.ts");
2259
+ if (!await fs.pathExists(drainPath)) await writeFileIfChanged(drainPath, getNitroEvlogDrainFile());
2260
+ }
1969
2261
  if (shouldIdentifyWebAuth(config)) {
1970
2262
  const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1971
2263
  if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
@@ -1974,9 +2266,10 @@ async function setupTanstackStartEvlog(config, serviceName) {
1974
2266
  }
1975
2267
  async function setupAstroEvlog(config, serviceName) {
1976
2268
  const webDir = path.join(config.projectDir, "apps/web");
2269
+ const fsDrain = shouldWireEvlogWebFsDrain(config);
1977
2270
  const middlewarePath = path.join(webDir, "src/middleware.ts");
1978
- if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
1979
- else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
2271
+ if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName, fsDrain));
2272
+ else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName, fsDrain));
1980
2273
  const envPath = path.join(webDir, "src/env.d.ts");
1981
2274
  if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
1982
2275
  else await updateFileIfExists(envPath, addAstroLocalsType);
@@ -1999,7 +2292,7 @@ async function setupEvlog(config) {
1999
2292
  const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
2000
2293
  if (await fs.pathExists(serverIndexPath)) {
2001
2294
  const content = await fs.readFile(serverIndexPath, "utf-8");
2002
- let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
2295
+ let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`, shouldWireEvlogServerFsDrain(config));
2003
2296
  if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
2004
2297
  if (config.examples.includes("ai")) nextContent = addBackendAiEvlogSetup(nextContent, config.backend);
2005
2298
  if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
@@ -2037,45 +2330,56 @@ async function navigableGroup(prompts, opts) {
2037
2330
  delete results[prevName];
2038
2331
  currentIndex--;
2039
2332
  };
2040
- while (currentIndex < promptNames.length) {
2041
- const name = promptNames[currentIndex];
2042
- const prompt = prompts[name];
2043
- setIsFirstPrompt$1(currentIndex === 0);
2044
- setLastPromptShownUI(false);
2045
- const result = await prompt({
2046
- results,
2047
- previousAnswer: previousAnswers[name]
2048
- })?.catch((e) => {
2049
- throw e;
2050
- });
2051
- if (isGoBack(result)) {
2052
- goingBack = true;
2053
- if (currentIndex > 0) {
2054
- stepBack();
2333
+ try {
2334
+ while (currentIndex < promptNames.length) {
2335
+ const name = promptNames[currentIndex];
2336
+ const prompt = prompts[name];
2337
+ const section = opts?.sections?.find(({ prompts: sectionPrompts }) => sectionPrompts.includes(name));
2338
+ const sectionPromptIndex = section?.prompts.indexOf(name) ?? -1;
2339
+ setPromptProgress({
2340
+ current: currentIndex + 1,
2341
+ total: promptNames.length,
2342
+ section: section?.label ?? "Setup",
2343
+ sectionCurrent: sectionPromptIndex + 1,
2344
+ sectionTotal: section?.prompts.length ?? promptNames.length
2345
+ });
2346
+ setIsFirstPrompt$1(currentIndex === 0);
2347
+ setLastPromptShownUI(false);
2348
+ const presetResult = opts?.preselected?.[name];
2349
+ const result = presetResult !== void 0 ? presetResult : await prompt({
2350
+ results,
2351
+ previousAnswer: previousAnswers[name]
2352
+ });
2353
+ if (isGoBack(result)) {
2354
+ goingBack = true;
2355
+ if (currentIndex > 0) {
2356
+ stepBack();
2357
+ continue;
2358
+ }
2359
+ goingBack = false;
2055
2360
  continue;
2056
2361
  }
2057
- goingBack = false;
2058
- continue;
2059
- }
2060
- if (isCancel$1(result)) {
2061
- if (typeof opts?.onCancel === "function") {
2062
- results[name] = "canceled";
2063
- opts.onCancel({ results });
2362
+ if (isCancel$1(result)) {
2363
+ if (typeof opts?.onCancel === "function") {
2364
+ results[name] = "canceled";
2365
+ opts.onCancel({ results });
2366
+ }
2367
+ return results;
2064
2368
  }
2065
- setIsFirstPrompt$1(false);
2066
- return results;
2067
- }
2068
- if (goingBack && !didLastPromptShowUI()) {
2069
- if (currentIndex > 0) {
2070
- stepBack();
2071
- continue;
2369
+ if (goingBack && !didLastPromptShowUI()) {
2370
+ if (currentIndex > 0) {
2371
+ stepBack();
2372
+ continue;
2373
+ }
2072
2374
  }
2375
+ goingBack = false;
2376
+ results[name] = result;
2377
+ currentIndex++;
2073
2378
  }
2074
- goingBack = false;
2075
- results[name] = result;
2076
- currentIndex++;
2379
+ } finally {
2380
+ setIsFirstPrompt$1(false);
2381
+ setPromptProgress(void 0);
2077
2382
  }
2078
- setIsFirstPrompt$1(false);
2079
2383
  return results;
2080
2384
  }
2081
2385
  //#endregion
@@ -2210,6 +2514,10 @@ const TEMPLATES$2 = {
2210
2514
  label: "Tanstack Start SPA: Fumadocs MDX (not RSC)",
2211
2515
  hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
2212
2516
  value: "tanstack-start-spa"
2517
+ },
2518
+ astro: {
2519
+ label: "Astro: Fumadocs MDX",
2520
+ value: "astro"
2213
2521
  }
2214
2522
  };
2215
2523
  const DEFAULT_TEMPLATE$2 = "next-mdx";
@@ -2223,7 +2531,7 @@ function getFumadocsLinter(addons) {
2223
2531
  if (addons.includes("vite-plus")) return "oxlint";
2224
2532
  }
2225
2533
  function getFumadocsAddonContext(currentAddons, persistedAddons) {
2226
- return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
2534
+ return Array.from(/* @__PURE__ */ new Set([...persistedAddons ?? [], ...currentAddons]));
2227
2535
  }
2228
2536
  async function setupFumadocs(config) {
2229
2537
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
@@ -2425,6 +2733,11 @@ const MCP_AGENTS = [
2425
2733
  label: "GitHub Copilot CLI",
2426
2734
  scope: "both"
2427
2735
  },
2736
+ {
2737
+ value: "grok-build",
2738
+ label: "Grok Build",
2739
+ scope: "both"
2740
+ },
2428
2741
  {
2429
2742
  value: "mcporter",
2430
2743
  label: "MCPorter",
@@ -2435,6 +2748,11 @@ const MCP_AGENTS = [
2435
2748
  label: "VS Code (GitHub Copilot)",
2436
2749
  scope: "both"
2437
2750
  },
2751
+ {
2752
+ value: "windsurf",
2753
+ label: "Windsurf",
2754
+ scope: "global"
2755
+ },
2438
2756
  {
2439
2757
  value: "zed",
2440
2758
  label: "Zed",
@@ -2808,10 +3126,42 @@ const SKILL_SOURCES = {
2808
3126
  "haydenbleasel/ultracite": { label: "Ultracite" },
2809
3127
  "https://www.evlog.dev": { label: "evlog" }
2810
3128
  };
2811
- const AVAILABLE_AGENTS = [
3129
+ const SKILLS_CLI_AGENT_OPTIONS = [
2812
3130
  {
2813
- value: "cursor",
2814
- label: "Cursor"
3131
+ value: "adal",
3132
+ label: "AdaL"
3133
+ },
3134
+ {
3135
+ value: "aider-desk",
3136
+ label: "AiderDesk"
3137
+ },
3138
+ {
3139
+ value: "amp",
3140
+ label: "Amp"
3141
+ },
3142
+ {
3143
+ value: "antigravity",
3144
+ label: "Antigravity"
3145
+ },
3146
+ {
3147
+ value: "antigravity-cli",
3148
+ label: "Antigravity CLI"
3149
+ },
3150
+ {
3151
+ value: "astrbot",
3152
+ label: "AstrBot"
3153
+ },
3154
+ {
3155
+ value: "augment",
3156
+ label: "Augment"
3157
+ },
3158
+ {
3159
+ value: "autohand-code",
3160
+ label: "Autohand Code CLI"
3161
+ },
3162
+ {
3163
+ value: "bob",
3164
+ label: "IBM Bob"
2815
3165
  },
2816
3166
  {
2817
3167
  value: "claude-code",
@@ -2822,100 +3172,306 @@ const AVAILABLE_AGENTS = [
2822
3172
  label: "Cline"
2823
3173
  },
2824
3174
  {
2825
- value: "github-copilot",
2826
- label: "GitHub Copilot"
3175
+ value: "codearts-agent",
3176
+ label: "CodeArts Agent"
3177
+ },
3178
+ {
3179
+ value: "codebuddy",
3180
+ label: "CodeBuddy"
3181
+ },
3182
+ {
3183
+ value: "codemaker",
3184
+ label: "Codemaker"
3185
+ },
3186
+ {
3187
+ value: "codestudio",
3188
+ label: "Code Studio"
2827
3189
  },
2828
3190
  {
2829
3191
  value: "codex",
2830
3192
  label: "Codex"
2831
3193
  },
2832
3194
  {
2833
- value: "opencode",
2834
- label: "OpenCode"
3195
+ value: "command-code",
3196
+ label: "Command Code"
2835
3197
  },
2836
3198
  {
2837
- value: "windsurf",
2838
- label: "Windsurf"
3199
+ value: "continue",
3200
+ label: "Continue"
3201
+ },
3202
+ {
3203
+ value: "cortex",
3204
+ label: "Cortex Code"
3205
+ },
3206
+ {
3207
+ value: "crush",
3208
+ label: "Crush"
3209
+ },
3210
+ {
3211
+ value: "cursor",
3212
+ label: "Cursor"
3213
+ },
3214
+ {
3215
+ value: "deepagents",
3216
+ label: "Deep Agents"
3217
+ },
3218
+ {
3219
+ value: "devin",
3220
+ label: "Devin for Terminal"
3221
+ },
3222
+ {
3223
+ value: "dexto",
3224
+ label: "Dexto"
3225
+ },
3226
+ {
3227
+ value: "droid",
3228
+ label: "Droid"
3229
+ },
3230
+ {
3231
+ value: "eve",
3232
+ label: "Eve"
3233
+ },
3234
+ {
3235
+ value: "firebender",
3236
+ label: "Firebender"
3237
+ },
3238
+ {
3239
+ value: "forgecode",
3240
+ label: "ForgeCode"
3241
+ },
3242
+ {
3243
+ value: "gemini-cli",
3244
+ label: "Gemini CLI"
3245
+ },
3246
+ {
3247
+ value: "github-copilot",
3248
+ label: "GitHub Copilot"
2839
3249
  },
2840
3250
  {
2841
3251
  value: "goose",
2842
3252
  label: "Goose"
2843
3253
  },
2844
3254
  {
2845
- value: "roo",
2846
- label: "Roo Code"
3255
+ value: "grok",
3256
+ label: "Grok Build"
3257
+ },
3258
+ {
3259
+ value: "hermes-agent",
3260
+ label: "Hermes Agent"
3261
+ },
3262
+ {
3263
+ value: "iflow-cli",
3264
+ label: "iFlow CLI"
3265
+ },
3266
+ {
3267
+ value: "inference-sh",
3268
+ label: "inference.sh"
3269
+ },
3270
+ {
3271
+ value: "jazz",
3272
+ label: "Jazz"
3273
+ },
3274
+ {
3275
+ value: "junie",
3276
+ label: "Junie"
2847
3277
  },
2848
3278
  {
2849
3279
  value: "kilo",
2850
3280
  label: "Kilo Code"
2851
3281
  },
2852
3282
  {
2853
- value: "gemini-cli",
2854
- label: "Gemini CLI"
3283
+ value: "kimchi",
3284
+ label: "Kimchi"
2855
3285
  },
2856
3286
  {
2857
- value: "antigravity",
2858
- label: "Antigravity"
3287
+ value: "kimi-code-cli",
3288
+ label: "Kimi Code CLI"
2859
3289
  },
2860
3290
  {
2861
- value: "openhands",
2862
- label: "OpenHands"
3291
+ value: "kiro-cli",
3292
+ label: "Kiro CLI"
2863
3293
  },
2864
3294
  {
2865
- value: "trae",
2866
- label: "Trae"
3295
+ value: "kode",
3296
+ label: "Kode"
2867
3297
  },
2868
3298
  {
2869
- value: "amp",
2870
- label: "Amp"
3299
+ value: "lingma",
3300
+ label: "Lingma"
3301
+ },
3302
+ {
3303
+ value: "loaf",
3304
+ label: "Loaf"
3305
+ },
3306
+ {
3307
+ value: "mcpjam",
3308
+ label: "MCPJam"
3309
+ },
3310
+ {
3311
+ value: "mistral-vibe",
3312
+ label: "Mistral Vibe"
3313
+ },
3314
+ {
3315
+ value: "moxby",
3316
+ label: "Moxby"
3317
+ },
3318
+ {
3319
+ value: "mux",
3320
+ label: "Mux"
3321
+ },
3322
+ {
3323
+ value: "neovate",
3324
+ label: "Neovate"
3325
+ },
3326
+ {
3327
+ value: "ona",
3328
+ label: "Ona"
3329
+ },
3330
+ {
3331
+ value: "openclaw",
3332
+ label: "OpenClaw"
3333
+ },
3334
+ {
3335
+ value: "opencode",
3336
+ label: "OpenCode"
3337
+ },
3338
+ {
3339
+ value: "openhands",
3340
+ label: "OpenHands"
2871
3341
  },
2872
3342
  {
2873
3343
  value: "pi",
2874
3344
  label: "Pi"
2875
3345
  },
3346
+ {
3347
+ value: "pochi",
3348
+ label: "Pochi"
3349
+ },
3350
+ {
3351
+ value: "promptscript",
3352
+ label: "PromptScript"
3353
+ },
2876
3354
  {
2877
3355
  value: "qoder",
2878
3356
  label: "Qoder"
2879
3357
  },
3358
+ {
3359
+ value: "qoder-cn",
3360
+ label: "Qoder CN"
3361
+ },
2880
3362
  {
2881
3363
  value: "qwen-code",
2882
3364
  label: "Qwen Code"
2883
3365
  },
2884
3366
  {
2885
- value: "kiro-cli",
2886
- label: "Kiro CLI"
3367
+ value: "reasonix",
3368
+ label: "Reasonix"
2887
3369
  },
2888
3370
  {
2889
- value: "droid",
2890
- label: "Droid"
3371
+ value: "replit",
3372
+ label: "Replit"
2891
3373
  },
2892
3374
  {
2893
- value: "command-code",
2894
- label: "Command Code"
3375
+ value: "roo",
3376
+ label: "Roo Code"
2895
3377
  },
2896
3378
  {
2897
- value: "clawdbot",
2898
- label: "Clawdbot"
3379
+ value: "rovodev",
3380
+ label: "Rovo Dev"
2899
3381
  },
2900
3382
  {
2901
- value: "zencoder",
2902
- label: "Zencoder"
3383
+ value: "tabnine-cli",
3384
+ label: "Tabnine CLI"
2903
3385
  },
2904
3386
  {
2905
- value: "neovate",
2906
- label: "Neovate"
3387
+ value: "terramind",
3388
+ label: "Terramind"
2907
3389
  },
2908
3390
  {
2909
- value: "mcpjam",
2910
- label: "MCPJam"
3391
+ value: "tinycloud",
3392
+ label: "Tinycloud"
3393
+ },
3394
+ {
3395
+ value: "trae",
3396
+ label: "Trae"
3397
+ },
3398
+ {
3399
+ value: "trae-cn",
3400
+ label: "Trae CN"
3401
+ },
3402
+ {
3403
+ value: "universal",
3404
+ label: "Universal"
3405
+ },
3406
+ {
3407
+ value: "warp",
3408
+ label: "Warp"
3409
+ },
3410
+ {
3411
+ value: "windsurf",
3412
+ label: "Windsurf"
3413
+ },
3414
+ {
3415
+ value: "zcode",
3416
+ label: "ZCode"
3417
+ },
3418
+ {
3419
+ value: "zed",
3420
+ label: "Zed"
3421
+ },
3422
+ {
3423
+ value: "zencoder",
3424
+ label: "Zencoder"
3425
+ },
3426
+ {
3427
+ value: "zenflow",
3428
+ label: "Zenflow"
2911
3429
  }
2912
3430
  ];
2913
- const DEFAULT_SCOPE = "project";
2914
- const DEFAULT_AGENTS$1 = [
3431
+ const UNIVERSAL_SKILLS_AGENTS = [
3432
+ "amp",
3433
+ "antigravity",
3434
+ "antigravity-cli",
3435
+ "cline",
3436
+ "codex",
2915
3437
  "cursor",
2916
- "claude-code",
2917
- "github-copilot"
3438
+ "deepagents",
3439
+ "dexto",
3440
+ "firebender",
3441
+ "gemini-cli",
3442
+ "github-copilot",
3443
+ "kimi-code-cli",
3444
+ "loaf",
3445
+ "opencode",
3446
+ "promptscript",
3447
+ "warp",
3448
+ "zed"
2918
3449
  ];
3450
+ const PROMPT_HIDDEN_AGENTS = /* @__PURE__ */ new Set([
3451
+ ...UNIVERSAL_SKILLS_AGENTS,
3452
+ "universal",
3453
+ "eve",
3454
+ "replit"
3455
+ ]);
3456
+ const SKILLS_AGENT_PROMPT_OPTIONS = [
3457
+ {
3458
+ value: "universal",
3459
+ label: "Universal (.agents/skills)",
3460
+ hint: "17 agents including Amp, Antigravity, Cline, Codex, Cursor, Gemini CLI, Copilot, OpenCode, Warp, and Zed"
3461
+ },
3462
+ ...SKILLS_CLI_AGENT_OPTIONS.filter(({ value }) => value === "claude-code"),
3463
+ ...SKILLS_CLI_AGENT_OPTIONS.filter(({ value }) => value !== "claude-code" && !PROMPT_HIDDEN_AGENTS.has(value))
3464
+ ];
3465
+ function expandSkillsAgentTargets(agents) {
3466
+ const expanded = agents.flatMap((agent) => {
3467
+ if (agent === "universal") return [...UNIVERSAL_SKILLS_AGENTS];
3468
+ if (agent === "clawdbot") return ["openclaw"];
3469
+ return [agent];
3470
+ });
3471
+ return Array.from(new Set(expanded));
3472
+ }
3473
+ const DEFAULT_SCOPE = "project";
3474
+ const DEFAULT_AGENTS$1 = ["universal", "claude-code"];
2919
3475
  function hasReactBasedFrontend(frontend) {
2920
3476
  return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
2921
3477
  }
@@ -3017,7 +3573,11 @@ const CURATED_SKILLS_BY_SOURCE = {
3017
3573
  ],
3018
3574
  "msmps/opentui-skill": () => ["opentui"],
3019
3575
  "haydenbleasel/ultracite": () => ["ultracite"],
3020
- "https://www.evlog.dev": () => ["review-logging-patterns", "analyze-logs"]
3576
+ "https://www.evlog.dev": (config) => [
3577
+ "review-logging-patterns",
3578
+ "build-audit-logs",
3579
+ ...supportsEvlogLocalLogs(config) ? ["analyze-logs"] : []
3580
+ ]
3021
3581
  };
3022
3582
  function getCuratedSkillNamesForSourceKey(sourceKey, config) {
3023
3583
  return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
@@ -3094,9 +3654,10 @@ async function setupSkills(config) {
3094
3654
  if (configuredAgents !== void 0) return [...configuredAgents];
3095
3655
  return navigableMultiselect({
3096
3656
  message: "Select agents to install skills to",
3097
- options: AVAILABLE_AGENTS,
3657
+ options: SKILLS_AGENT_PROMPT_OPTIONS,
3098
3658
  required: false,
3099
- initialValues: [...DEFAULT_AGENTS$1]
3659
+ initialValues: [...DEFAULT_AGENTS$1],
3660
+ maxItems: 10
3100
3661
  });
3101
3662
  }
3102
3663
  });
@@ -3116,6 +3677,7 @@ async function setupSkills(config) {
3116
3677
  installSpinner.start("Installing skills...");
3117
3678
  const runner = getPackageRunnerPrefix(packageManager);
3118
3679
  const globalFlags = scope === "global" ? ["-g"] : [];
3680
+ const agentTargets = expandSkillsAgentTargets(selectedAgents);
3119
3681
  for (const [source, skills] of Object.entries(skillsBySource)) if ((await Result.tryPromise({
3120
3682
  try: async () => {
3121
3683
  const args = [
@@ -3127,7 +3689,7 @@ async function setupSkills(config) {
3127
3689
  "--skill",
3128
3690
  ...skills,
3129
3691
  "--agent",
3130
- ...selectedAgents,
3692
+ ...agentTargets,
3131
3693
  "-y"
3132
3694
  ];
3133
3695
  await $({
@@ -3439,7 +4001,7 @@ const HOOKS = {
3439
4001
  claude: { label: "Claude Code" },
3440
4002
  copilot: { label: "GitHub Copilot" }
3441
4003
  };
3442
- const ULTRACITE_VERSION = "7.9.3";
4004
+ const ULTRACITE_VERSION = "7.9.4";
3443
4005
  const DEFAULT_LINTER = "biome";
3444
4006
  const DEFAULT_EDITORS = ["vscode"];
3445
4007
  const DEFAULT_AGENTS = ["universal"];
@@ -3846,7 +4408,7 @@ async function installDependencies({ projectDir, packageManager }) {
3846
4408
  cause: e
3847
4409
  })
3848
4410
  });
3849
- if (result.isOk()) s.stop("Dependencies installed successfully");
4411
+ if (result.isOk()) s.stop("Dependencies installed");
3850
4412
  else s.stop(pc.red("Failed to install dependencies"));
3851
4413
  return result;
3852
4414
  }
@@ -3951,7 +4513,7 @@ async function addHandlerInternal(input) {
3951
4513
  }));
3952
4514
  if (!isSilent()) {
3953
4515
  renderTitle();
3954
- intro(pc.magenta("Add addons to your Better-T-Stack project"));
4516
+ intro(pc.magenta("Add to your project"));
3955
4517
  }
3956
4518
  const existingConfig = await detectProjectConfig(projectDir);
3957
4519
  if (!existingConfig) return Result.err(new CLIError({ message: `No Better-T-Stack project found in ${projectDir}. Make sure bts.jsonc exists.` }));
@@ -3960,7 +4522,10 @@ async function addHandlerInternal(input) {
3960
4522
  if (input.addons && input.addons.length > 0) {
3961
4523
  addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingConfig.addons.includes(addon));
3962
4524
  if (addonsToAdd.length === 0) {
3963
- if (!isSilent()) log.warn(pc.yellow("All specified addons are already installed or invalid."));
4525
+ if (!isSilent()) {
4526
+ log.warn(pc.yellow("Nothing to add — those addons are already installed"));
4527
+ outro(pc.dim("Project unchanged"));
4528
+ }
3964
4529
  return Result.ok({
3965
4530
  success: true,
3966
4531
  addedAddons: [],
@@ -3982,10 +4547,7 @@ async function addHandlerInternal(input) {
3982
4547
  if (promptResult.isErr()) return Result.err(promptResult.error);
3983
4548
  const selectedAddons = promptResult.value;
3984
4549
  if (selectedAddons.length === 0) {
3985
- if (!isSilent()) {
3986
- log.info(pc.dim("No addons selected."));
3987
- outro(pc.magenta("Nothing to add."));
3988
- }
4550
+ if (!isSilent()) outro(pc.dim("Nothing selected · project unchanged"));
3989
4551
  return Result.ok({
3990
4552
  success: true,
3991
4553
  addedAddons: [],
@@ -3997,7 +4559,7 @@ async function addHandlerInternal(input) {
3997
4559
  const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3998
4560
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3999
4561
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
4000
- if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
4562
+ if (!isSilent()) log.info(`${pc.dim("Adding")} ${pc.cyan(formatConfigValue(addonsToAdd))}`);
4001
4563
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
4002
4564
  const config = {
4003
4565
  projectName: existingConfig.projectName,
@@ -4025,7 +4587,7 @@ async function addHandlerInternal(input) {
4025
4587
  ...config,
4026
4588
  addons: updatedAddons
4027
4589
  };
4028
- if (!isSilent()) log.info(pc.dim("Installing addon files..."));
4590
+ if (!isSilent()) log.info(pc.dim("Preparing addon files"));
4029
4591
  const vfs = new VirtualFileSystem();
4030
4592
  for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
4031
4593
  const fullPath = path.join(projectDir, pkgPath);
@@ -4057,9 +4619,9 @@ async function addHandlerInternal(input) {
4057
4619
  };
4058
4620
  if (input.dryRun) {
4059
4621
  if (!isSilent()) {
4060
- log.success(pc.green("Dry run validation passed. No addon files were written."));
4061
- log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
4062
- outro(pc.magenta("Dry run complete."));
4622
+ log.success(pc.green("Dry run passed · no files written"));
4623
+ log.message(pc.dim(`${vfs.getFileCount()} addon files planned`));
4624
+ outro(pc.dim("Project unchanged"));
4063
4625
  }
4064
4626
  return Result.ok({
4065
4627
  success: true,
@@ -4090,17 +4652,17 @@ async function addHandlerInternal(input) {
4090
4652
  addons: updatedAddons,
4091
4653
  addonOptions: updatedConfig.addonOptions
4092
4654
  });
4093
- if (input.install) {
4094
- if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
4095
- await installDependencies({
4096
- projectDir,
4097
- packageManager: config.packageManager
4098
- });
4099
- }
4655
+ if (input.install) await installDependencies({
4656
+ projectDir,
4657
+ packageManager: config.packageManager
4658
+ });
4100
4659
  if (!isSilent()) {
4101
- log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
4102
- if (!input.install) log.info(pc.yellow(`Run '${config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`}' to install new dependencies.`));
4103
- outro(pc.magenta("Addons added successfully!"));
4660
+ log.success(pc.green(`Added ${formatConfigValue(addonsToAdd)}`));
4661
+ if (!input.install) {
4662
+ const installCommand = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
4663
+ log.message(`${pc.dim("Next step")}\n${pc.cyan(installCommand)}`);
4664
+ }
4665
+ outro(pc.magenta("Project updated"));
4104
4666
  }
4105
4667
  return Result.ok({
4106
4668
  success: true,
@@ -4133,7 +4695,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4133
4695
  hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
4134
4696
  });
4135
4697
  const apiType = await navigableSelect({
4136
- message: "Select API type",
4698
+ message: "Choose an API layer",
4137
4699
  options: apiOptions,
4138
4700
  initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
4139
4701
  });
@@ -4185,7 +4747,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
4185
4747
  }
4186
4748
  });
4187
4749
  const response = await navigableSelect({
4188
- message: "Select authentication provider",
4750
+ message: "Choose authentication",
4189
4751
  options,
4190
4752
  initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4191
4753
  });
@@ -4239,7 +4801,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
4239
4801
  hint: "No backend server"
4240
4802
  });
4241
4803
  const response = await navigableSelect({
4242
- message: "Select backend",
4804
+ message: "Choose a backend",
4243
4805
  options: backendOptions,
4244
4806
  initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4245
4807
  });
@@ -4279,7 +4841,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4279
4841
  hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
4280
4842
  });
4281
4843
  const response = await navigableSelect({
4282
- message: "Select database",
4844
+ message: "Choose a database",
4283
4845
  options: databaseOptions,
4284
4846
  initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4285
4847
  });
@@ -4378,7 +4940,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
4378
4940
  ];
4379
4941
  else return "none";
4380
4942
  const response = await navigableSelect({
4381
- message: `Select ${databaseType} setup option`,
4943
+ message: `Choose a ${databaseType} setup`,
4382
4944
  options,
4383
4945
  initialValue: preferValidInitial(options, previousValue, "none")
4384
4946
  });
@@ -4404,7 +4966,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
4404
4966
  });
4405
4967
  if (options.length === 0) return [];
4406
4968
  response = await navigableMultiselect({
4407
- message: "Include examples",
4969
+ message: "Include starter examples?",
4408
4970
  options,
4409
4971
  required: false,
4410
4972
  initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
@@ -4431,7 +4993,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4431
4993
  while (true) {
4432
4994
  const wasFirstPrompt = isFirstPrompt();
4433
4995
  const frontendTypes = await navigableMultiselect({
4434
- message: "Select project type",
4996
+ message: "What are you building?",
4435
4997
  options: [{
4436
4998
  value: "web",
4437
4999
  label: "Web",
@@ -4493,7 +5055,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4493
5055
  }
4494
5056
  ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
4495
5057
  const webFramework = await navigableSelect({
4496
- message: "Choose web",
5058
+ message: "Choose a web framework",
4497
5059
  options: webOptions,
4498
5060
  initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4499
5061
  });
@@ -4507,7 +5069,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4507
5069
  }
4508
5070
  if (frontendTypes.includes("native")) {
4509
5071
  const nativeFramework = await navigableSelect({
4510
- message: "Choose native",
5072
+ message: "Choose a native setup",
4511
5073
  options: [
4512
5074
  {
4513
5075
  value: "native-bare",
@@ -4547,7 +5109,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4547
5109
  async function getGitChoice(git, previousValue) {
4548
5110
  if (git !== void 0) return git;
4549
5111
  const response = await navigableConfirm({
4550
- message: "Initialize git repository?",
5112
+ message: "Initialize a Git repository?",
4551
5113
  initialValue: previousValue ?? DEFAULT_CONFIG.git
4552
5114
  });
4553
5115
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -4799,7 +5361,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
4799
5361
  }
4800
5362
  const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
4801
5363
  const response = await navigableSelect({
4802
- message: "Select ORM",
5364
+ message: "Choose an ORM",
4803
5365
  options,
4804
5366
  initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4805
5367
  });
@@ -4851,7 +5413,7 @@ async function getPaymentsChoice(payments, auth, backend, _frontends, previousVa
4851
5413
  hint: "No payments integration"
4852
5414
  }];
4853
5415
  const response = await navigableSelect({
4854
- message: "Select payments provider",
5416
+ message: "Add payments?",
4855
5417
  options,
4856
5418
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
4857
5419
  });
@@ -4878,7 +5440,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
4878
5440
  hint: "Edge runtime on Cloudflare's global network"
4879
5441
  });
4880
5442
  const response = await navigableSelect({
4881
- message: "Select runtime",
5443
+ message: "Choose a runtime",
4882
5444
  options: runtimeOptions,
4883
5445
  initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4884
5446
  });
@@ -4932,7 +5494,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
4932
5494
  };
4933
5495
  });
4934
5496
  const response = await navigableSelect({
4935
- message: "Select server deployment",
5497
+ message: "Choose server deployment",
4936
5498
  options,
4937
5499
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
4938
5500
  });
@@ -4980,7 +5542,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
4980
5542
  };
4981
5543
  });
4982
5544
  const response = await navigableSelect({
4983
- message: "Select web deployment",
5545
+ message: "Choose web deployment",
4984
5546
  options,
4985
5547
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
4986
5548
  });
@@ -4989,7 +5551,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
4989
5551
  }
4990
5552
  //#endregion
4991
5553
  //#region src/prompts/config-prompts.ts
4992
- async function gatherConfig(flags, projectName, projectDir, relativePath) {
5554
+ async function gatherConfig(flags, projectName, projectDir, relativePath, options = {}) {
4993
5555
  if (isSilent()) return {
4994
5556
  projectName,
4995
5557
  projectDir,
@@ -5017,22 +5579,63 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5017
5579
  frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
5018
5580
  backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
5019
5581
  runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
5582
+ api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
5020
5583
  database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
5021
5584
  orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
5022
- api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
5585
+ dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
5023
5586
  auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
5024
5587
  payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
5025
5588
  addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
5026
5589
  examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
5027
- dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
5028
5590
  webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
5029
5591
  serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
5030
5592
  git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
5031
5593
  packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
5032
5594
  install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
5033
- }, { onCancel: () => {
5034
- throw new UserCancelledError({ message: "Operation cancelled" });
5035
- } });
5595
+ }, {
5596
+ preselected: options.skipCompatibilityChecks ? flags : void 0,
5597
+ sections: [
5598
+ {
5599
+ label: "App",
5600
+ prompts: [
5601
+ "frontend",
5602
+ "backend",
5603
+ "runtime",
5604
+ "api"
5605
+ ]
5606
+ },
5607
+ {
5608
+ label: "Data",
5609
+ prompts: [
5610
+ "database",
5611
+ "orm",
5612
+ "dbSetup"
5613
+ ]
5614
+ },
5615
+ {
5616
+ label: "Product",
5617
+ prompts: [
5618
+ "auth",
5619
+ "payments",
5620
+ "addons",
5621
+ "examples"
5622
+ ]
5623
+ },
5624
+ {
5625
+ label: "Ship",
5626
+ prompts: [
5627
+ "webDeploy",
5628
+ "serverDeploy",
5629
+ "git",
5630
+ "packageManager",
5631
+ "install"
5632
+ ]
5633
+ }
5634
+ ],
5635
+ onCancel: () => {
5636
+ throw new UserCancelledError({ message: "Operation cancelled" });
5637
+ }
5638
+ });
5036
5639
  return {
5037
5640
  projectName,
5038
5641
  projectDir,
@@ -5081,13 +5684,22 @@ async function getProjectName(initialName) {
5081
5684
  let projectPath = "";
5082
5685
  let defaultName = DEFAULT_CONFIG.projectName;
5083
5686
  let counter = 1;
5084
- while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
5687
+ while (true) {
5688
+ const defaultPath = path.resolve(process.cwd(), defaultName);
5689
+ let stats;
5690
+ try {
5691
+ stats = await fs.lstat(defaultPath);
5692
+ } catch (error) {
5693
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
5694
+ throw error;
5695
+ }
5696
+ if (stats.isDirectory() && (await fs.readdir(defaultPath)).length === 0) break;
5085
5697
  defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
5086
5698
  counter++;
5087
5699
  }
5088
5700
  while (!isValid) {
5089
5701
  const response = await text({
5090
- message: "Enter your project name or path (relative to current directory)",
5702
+ message: "Where should we create your project?",
5091
5703
  placeholder: defaultName,
5092
5704
  initialValue: initialName,
5093
5705
  defaultValue: defaultName,
@@ -5116,9 +5728,7 @@ async function getProjectName(initialName) {
5116
5728
  */
5117
5729
  function isTelemetryEnabled() {
5118
5730
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
5119
- const BTS_TELEMETRY = "1";
5120
5731
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
5121
- if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
5122
5732
  return true;
5123
5733
  }
5124
5734
  //#endregion
@@ -5148,90 +5758,83 @@ async function trackProjectCreation(config, disableAnalytics = false) {
5148
5758
  });
5149
5759
  }
5150
5760
  //#endregion
5151
- //#region src/utils/display-config.ts
5152
- function displayConfig(config) {
5153
- const configDisplay = [];
5154
- if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
5155
- if (config.frontend !== void 0) {
5156
- const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
5157
- const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
5158
- configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
5159
- }
5160
- if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
5161
- if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
5162
- if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
5163
- if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
5164
- if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
5165
- if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
5166
- if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
5167
- if (config.addons !== void 0) {
5168
- const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
5169
- const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
5170
- configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
5171
- }
5172
- if (config.examples !== void 0) {
5173
- const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
5174
- const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
5175
- configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
5176
- }
5177
- if (config.git !== void 0) {
5178
- const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
5179
- configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
5180
- }
5181
- if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
5182
- if (config.install !== void 0) {
5183
- const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
5184
- configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
5185
- }
5186
- if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
5187
- if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
5188
- if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
5189
- if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
5190
- return configDisplay.join("\n");
5761
+ //#region src/utils/cli-invocation.ts
5762
+ function getCliSubcommandCommand(subcommand, fallbackPackageManager, userAgent = process.env.npm_config_user_agent) {
5763
+ const normalizedUserAgent = userAgent?.toLowerCase();
5764
+ return getPackageExecutionCommand(normalizedUserAgent?.startsWith("bun") ? "bun" : normalizedUserAgent?.startsWith("pnpm") ? "pnpm" : normalizedUserAgent?.startsWith("npm") ? "npm" : fallbackPackageManager, `create-better-t-stack@latest ${subcommand}`);
5191
5765
  }
5192
5766
  //#endregion
5193
5767
  //#region src/utils/project-directory.ts
5194
5768
  async function handleDirectoryConflict(currentPathInput) {
5195
5769
  while (true) {
5196
- const resolvedPath = path.resolve(process.cwd(), currentPathInput);
5197
- if (!(await fs.pathExists(resolvedPath) && (await fs.readdir(resolvedPath)).length > 0)) return {
5770
+ const pathStateResult = await inspectProjectPath(path.resolve(process.cwd(), currentPathInput));
5771
+ if (pathStateResult.isErr()) throw pathStateResult.error;
5772
+ const pathState = pathStateResult.value;
5773
+ if (pathState === "missing" || pathState === "empty-directory") return {
5198
5774
  finalPathInput: currentPathInput,
5199
5775
  shouldClearDirectory: false
5200
5776
  };
5201
- if (isSilent()) throw new CLIError({ message: `Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.` });
5202
- log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
5777
+ if (isSilent()) throw new CLIError({ message: `Project path "${currentPathInput}" is unavailable. In silent mode, provide a different project path or an explicit directoryConflict strategy.` });
5778
+ if (pathState === "symbolic-link") log.warn(`Project path "${pc.yellow(currentPathInput)}" is a symbolic link.`);
5779
+ else if (pathState === "non-directory") log.warn(`Project path "${pc.yellow(currentPathInput)}" exists and is not a directory.`);
5780
+ else log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
5781
+ let incrementedPath;
5782
+ if (currentPathInput !== ".") {
5783
+ const incrementResult = await findAvailableIncrementedPath(currentPathInput);
5784
+ if (incrementResult.isErr()) throw incrementResult.error;
5785
+ incrementedPath = incrementResult.value;
5786
+ }
5787
+ const options = [];
5788
+ if (incrementedPath) options.push({
5789
+ value: "increment",
5790
+ label: `Create as "${incrementedPath}"`,
5791
+ hint: "Keep the existing path untouched"
5792
+ });
5793
+ options.push({
5794
+ value: "rename",
5795
+ label: "Choose another path",
5796
+ hint: "Enter a different project directory"
5797
+ });
5798
+ if (pathState === "non-empty-directory") options.push({
5799
+ value: "merge",
5800
+ label: "Merge into this directory",
5801
+ hint: "Keep unrelated files; replace conflicts"
5802
+ }, {
5803
+ value: "overwrite",
5804
+ label: "Delete and overwrite",
5805
+ hint: "Permanently remove existing contents"
5806
+ });
5807
+ options.push({
5808
+ value: "cancel",
5809
+ label: "Cancel",
5810
+ hint: "Leave everything unchanged"
5811
+ });
5203
5812
  const action = await select({
5204
- message: "What would you like to do?",
5205
- options: [
5206
- {
5207
- value: "overwrite",
5208
- label: "Overwrite",
5209
- hint: "Empty the directory and create the project"
5210
- },
5211
- {
5212
- value: "merge",
5213
- label: "Merge",
5214
- hint: "Create project files inside, potentially overwriting conflicts"
5215
- },
5216
- {
5217
- value: "rename",
5218
- label: "Choose a different name/path",
5219
- hint: "Keep the existing directory and create a new one"
5220
- },
5221
- {
5222
- value: "cancel",
5223
- label: "Cancel",
5224
- hint: "Abort the process"
5225
- }
5226
- ],
5227
- initialValue: "rename"
5813
+ message: "How should we continue?",
5814
+ options,
5815
+ initialValue: incrementedPath ? "increment" : "rename"
5228
5816
  });
5229
5817
  if (isCancel(action)) throw new UserCancelledError({ message: "Operation cancelled." });
5230
5818
  switch (action) {
5231
- case "overwrite": return {
5232
- finalPathInput: currentPathInput,
5233
- shouldClearDirectory: true
5819
+ case "increment": return {
5820
+ finalPathInput: incrementedPath,
5821
+ shouldClearDirectory: false
5234
5822
  };
5823
+ case "overwrite": {
5824
+ const confirmed = await confirm({
5825
+ message: `Permanently delete every file in "${currentPathInput}"?`,
5826
+ initialValue: false
5827
+ });
5828
+ if (isCancel(confirmed)) throw new UserCancelledError({ message: "Operation cancelled." });
5829
+ if (!confirmed) {
5830
+ log.info("Nothing was deleted. Choose another option.");
5831
+ continue;
5832
+ }
5833
+ return {
5834
+ finalPathInput: currentPathInput,
5835
+ shouldClearDirectory: true
5836
+ };
5837
+ }
5235
5838
  case "merge":
5236
5839
  log.info(`Proceeding into existing directory "${pc.yellow(currentPathInput)}". Files may be overwritten.`);
5237
5840
  return {
@@ -5239,8 +5842,8 @@ async function handleDirectoryConflict(currentPathInput) {
5239
5842
  shouldClearDirectory: false
5240
5843
  };
5241
5844
  case "rename":
5242
- log.info("Please choose a different project name or path.");
5243
- return await handleDirectoryConflict(await getProjectName(void 0));
5845
+ currentPathInput = await getProjectName(void 0);
5846
+ continue;
5244
5847
  case "cancel": throw new UserCancelledError({ message: "Operation cancelled." });
5245
5848
  }
5246
5849
  }
@@ -5255,9 +5858,11 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
5255
5858
  finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
5256
5859
  finalBaseName = path.basename(finalResolvedPath);
5257
5860
  }
5861
+ const pathSafetyResult = await validateSafeProjectDirectoryPath(finalPathInput);
5862
+ if (pathSafetyResult.isErr()) throw pathSafetyResult.error;
5258
5863
  if (shouldClearDirectory) {
5259
- const s = spinner();
5260
- s.start(`Clearing directory "${finalResolvedPath}"...`);
5864
+ const s = isSilent() ? void 0 : spinner();
5865
+ s?.start(`Clearing directory "${finalResolvedPath}"...`);
5261
5866
  const clearResult = await Result.tryPromise({
5262
5867
  try: () => fs.emptyDir(finalResolvedPath),
5263
5868
  catch: (error) => new CLIError({
@@ -5266,16 +5871,78 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
5266
5871
  })
5267
5872
  });
5268
5873
  if (clearResult.isErr()) {
5269
- s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
5874
+ s?.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
5270
5875
  throw clearResult.error;
5271
5876
  }
5272
- s.stop(`Directory "${finalResolvedPath}" cleared.`);
5877
+ s?.stop(`Directory "${finalResolvedPath}" cleared.`);
5273
5878
  } else await fs.ensureDir(finalResolvedPath);
5274
5879
  return {
5275
5880
  finalResolvedPath,
5276
5881
  finalBaseName
5277
5882
  };
5278
5883
  }
5884
+ async function validateSafeProjectDirectoryPath(finalPathInput) {
5885
+ return Result.tryPromise({
5886
+ try: async () => {
5887
+ const cwd = path.resolve(process.cwd());
5888
+ const targetPath = finalPathInput === "." ? cwd : path.resolve(cwd, finalPathInput);
5889
+ const relativeTarget = path.relative(cwd, targetPath);
5890
+ if (relativeTarget === ".." || relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) throw new CLIError({ message: `Project path "${finalPathInput}" resolves outside the current working directory.` });
5891
+ const pathSegments = relativeTarget.split(path.sep).filter(Boolean);
5892
+ let nearestExistingPath = cwd;
5893
+ for (const segment of pathSegments) {
5894
+ const candidatePath = path.join(nearestExistingPath, segment);
5895
+ let stats;
5896
+ try {
5897
+ stats = await fs.lstat(candidatePath);
5898
+ } catch (error) {
5899
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") break;
5900
+ throw error;
5901
+ }
5902
+ 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.` });
5903
+ if (!stats.isDirectory()) throw new CLIError({ message: `Project path "${finalPathInput}" passes through "${path.relative(cwd, candidatePath)}", which is not a directory.` });
5904
+ nearestExistingPath = candidatePath;
5905
+ }
5906
+ const [realCwd, realExistingPath] = await Promise.all([fs.realpath(cwd), fs.realpath(nearestExistingPath)]);
5907
+ const relativeRealPath = path.relative(realCwd, realExistingPath);
5908
+ if (relativeRealPath === ".." || relativeRealPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeRealPath)) throw new CLIError({ message: `Project path "${finalPathInput}" resolves outside the current working directory.` });
5909
+ },
5910
+ catch: (error) => CLIError.is(error) ? error : new CLIError({
5911
+ message: `Unable to validate project path "${finalPathInput}".`,
5912
+ cause: error
5913
+ })
5914
+ });
5915
+ }
5916
+ async function inspectProjectPath(targetPath) {
5917
+ return Result.tryPromise({
5918
+ try: async () => {
5919
+ let stats;
5920
+ try {
5921
+ stats = await fs.lstat(targetPath);
5922
+ } catch (error) {
5923
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "missing";
5924
+ throw error;
5925
+ }
5926
+ if (stats.isSymbolicLink()) return "symbolic-link";
5927
+ if (!stats.isDirectory()) return "non-directory";
5928
+ return (await fs.readdir(targetPath)).length === 0 ? "empty-directory" : "non-empty-directory";
5929
+ },
5930
+ catch: (error) => new CLIError({
5931
+ message: `Unable to inspect project path "${targetPath}".`,
5932
+ cause: error
5933
+ })
5934
+ });
5935
+ }
5936
+ async function findAvailableIncrementedPath(currentPathInput) {
5937
+ let counter = 1;
5938
+ while (true) {
5939
+ const candidate = `${currentPathInput}-${counter}`;
5940
+ const candidateStateResult = await inspectProjectPath(path.resolve(process.cwd(), candidate));
5941
+ if (candidateStateResult.isErr()) return Result.err(candidateStateResult.error);
5942
+ if (candidateStateResult.value === "missing" || candidateStateResult.value === "empty-directory") return Result.ok(candidate);
5943
+ counter++;
5944
+ }
5945
+ }
5279
5946
  //#endregion
5280
5947
  //#region src/utils/project-name-validation.ts
5281
5948
  function validateProjectName(name) {
@@ -5439,7 +6106,7 @@ function validateArrayOptions(options) {
5439
6106
  }
5440
6107
  //#endregion
5441
6108
  //#region src/validation.ts
5442
- const CORE_STACK_FLAGS = new Set([
6109
+ const CORE_STACK_FLAGS = /* @__PURE__ */ new Set([
5443
6110
  "database",
5444
6111
  "orm",
5445
6112
  "backend",
@@ -5689,7 +6356,7 @@ async function initMongoDBAtlas(serverDir) {
5689
6356
  await $({
5690
6357
  cwd: serverDir,
5691
6358
  stdio: "inherit"
5692
- })`atlas deployments setup`;
6359
+ })`atlas setup`;
5693
6360
  cliLog.success("MongoDB Atlas deployment ready");
5694
6361
  },
5695
6362
  catch: (e) => new DatabaseSetupError({
@@ -5738,7 +6405,7 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
5738
6405
  ${pc.blue("https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/")}
5739
6406
 
5740
6407
  2. Run the following command and follow the prompts:
5741
- ${pc.blue("atlas deployments setup")}
6408
+ ${pc.blue("atlas setup")}
5742
6409
 
5743
6410
  3. Get your connection string from the Atlas dashboard:
5744
6411
  Format: ${pc.dim("mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME")}
@@ -5839,10 +6506,6 @@ const NEON_REGIONS = [
5839
6506
  label: "AWS Asia Pacific (Singapore)",
5840
6507
  value: "aws-ap-southeast-1"
5841
6508
  },
5842
- {
5843
- label: "AWS South America East 1 (São Paulo)",
5844
- value: "aws-sa-east-1"
5845
- },
5846
6509
  {
5847
6510
  label: "AWS Asia Pacific (Sydney)",
5848
6511
  value: "aws-ap-southeast-2"
@@ -5852,13 +6515,12 @@ const NEON_REGIONS = [
5852
6515
  value: "azure-eastus2"
5853
6516
  }
5854
6517
  ];
5855
- async function executeNeonCommand(packageManager, commandArgsString, spinnerText) {
6518
+ async function executeNeonCommand(commandArgs, spinnerText) {
5856
6519
  const s = createSpinner();
5857
- const args = getPackageExecutionArgs(packageManager, commandArgsString);
5858
6520
  if (spinnerText) s.start(spinnerText);
5859
6521
  return Result.tryPromise({
5860
6522
  try: async () => {
5861
- const result = await $`${args}`;
6523
+ const result = await $`${commandArgs}`;
5862
6524
  if (spinnerText) s.stop(pc.green(spinnerText.replace("...", "").replace("ing ", "ed ").trim()));
5863
6525
  return result;
5864
6526
  },
@@ -5872,8 +6534,22 @@ async function executeNeonCommand(packageManager, commandArgsString, spinnerText
5872
6534
  }
5873
6535
  });
5874
6536
  }
6537
+ function getNeonProjectCreateArgs(packageManager, projectName, regionId) {
6538
+ return [
6539
+ ...getPackageRunnerPrefix(packageManager),
6540
+ "neon@latest",
6541
+ "projects",
6542
+ "create",
6543
+ "--name",
6544
+ projectName,
6545
+ "--region-id",
6546
+ regionId,
6547
+ "--output",
6548
+ "json"
6549
+ ];
6550
+ }
5875
6551
  async function createNeonProject(projectName, regionId, packageManager) {
5876
- const execResult = await executeNeonCommand(packageManager, `neonctl@latest projects create --name ${projectName} --region-id ${regionId} --output json`, `Creating Neon project "${projectName}"...`);
6552
+ const execResult = await executeNeonCommand(getNeonProjectCreateArgs(packageManager, projectName, regionId), `Creating Neon project "${projectName}"...`);
5877
6553
  if (execResult.isErr()) return Result.err(execResult.error);
5878
6554
  const parseResult = Result.try({
5879
6555
  try: () => JSON.parse(execResult.value.stdout),
@@ -5917,7 +6593,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
5917
6593
  }
5918
6594
  async function setupWithNeonDb(projectDir, packageManager, backend) {
5919
6595
  const s = createSpinner();
5920
- s.start("Creating Neon database using get-db...");
6596
+ s.start("Creating Neon database using neon-new...");
5921
6597
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
5922
6598
  const targetDir = path.join(projectDir, targetApp);
5923
6599
  const ensureDirResult = await Result.tryPromise({
@@ -5932,17 +6608,17 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
5932
6608
  s.stop(pc.red("Failed to create directory"));
5933
6609
  return ensureDirResult;
5934
6610
  }
5935
- const packageArgs = getPackageExecutionArgs(packageManager, `get-db@latest --yes --ref "sbA3tIe"`);
6611
+ const packageArgs = getPackageExecutionArgs(packageManager, `neon-new@latest --yes --ref "sbA3tIe"`);
5936
6612
  return Result.tryPromise({
5937
6613
  try: async () => {
5938
6614
  await $({ cwd: targetDir })`${packageArgs}`;
5939
6615
  s.stop(pc.green("Neon database created successfully!"));
5940
6616
  },
5941
6617
  catch: (e) => {
5942
- s.stop(pc.red("Failed to create database with get-db"));
6618
+ s.stop(pc.red("Failed to create database with neon-new"));
5943
6619
  return new DatabaseSetupError({
5944
6620
  provider: "neon",
5945
- message: `Failed to create database with get-db: ${e instanceof Error ? e.message : String(e)}`,
6621
+ message: `Failed to create database with neon-new: ${e instanceof Error ? e.message : String(e)}`,
5946
6622
  cause: e
5947
6623
  });
5948
6624
  }
@@ -6003,25 +6679,25 @@ async function setupNeonPostgres(config, cliInput) {
6003
6679
  return Result.ok(void 0);
6004
6680
  }
6005
6681
  let setupMethod = cliInput?.dbSetupOptions?.neon?.method ?? config.dbSetupOptions?.neon?.method;
6006
- if (!setupMethod) if (isSilent()) setupMethod = "neondb";
6682
+ if (!setupMethod) if (isSilent()) setupMethod = "neon-new";
6007
6683
  else {
6008
6684
  const promptedSetupMethod = await select({
6009
6685
  message: "Choose your Neon setup method:",
6010
6686
  options: [{
6011
- label: "Quick setup with get-db",
6012
- value: "neondb",
6687
+ label: "Quick setup with neon-new",
6688
+ value: "neon-new",
6013
6689
  hint: "fastest, no auth required"
6014
6690
  }, {
6015
- label: "Custom setup with neonctl",
6016
- value: "neonctl",
6691
+ label: "Custom setup with Neon CLI",
6692
+ value: "neon",
6017
6693
  hint: "More control - choose project name and region"
6018
6694
  }],
6019
- initialValue: "neondb"
6695
+ initialValue: "neon-new"
6020
6696
  });
6021
6697
  if (isCancel(promptedSetupMethod)) return userCancelled("Operation cancelled");
6022
6698
  setupMethod = promptedSetupMethod;
6023
6699
  }
6024
- if (setupMethod === "neondb") {
6700
+ if (setupMethod === "neon-new" || setupMethod === "neondb") {
6025
6701
  const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
6026
6702
  if (neonDbResult.isErr()) {
6027
6703
  cliLog.error(pc.red(neonDbResult.error.message));
@@ -6941,7 +7617,7 @@ async function getDockerStatus(database) {
6941
7617
  //#endregion
6942
7618
  //#region src/helpers/core/post-installation.ts
6943
7619
  function getDesktopStaticBuildNote(frontend) {
6944
- const staticBuildFrontends = new Map([
7620
+ const staticBuildFrontends = /* @__PURE__ */ new Map([
6945
7621
  ["tanstack-start", "TanStack Start"],
6946
7622
  ["next", "Next.js"],
6947
7623
  ["nuxt", "Nuxt"],
@@ -6985,7 +7661,7 @@ async function displayPostInstallInstructions(config) {
6985
7661
  const polarInstructions = config.payments === "polar" && config.auth === "better-auth" ? getPolarInstructions(backend, packageManager) : "";
6986
7662
  const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
6987
7663
  const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
6988
- let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
7664
+ let output = `${pc.cyan("1.")} ${cdCmd}\n`;
6989
7665
  let stepCounter = 2;
6990
7666
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
6991
7667
  if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
@@ -7003,19 +7679,44 @@ async function displayPostInstallInstructions(config) {
7003
7679
  }
7004
7680
  const hasStandaloneBackend = backend !== "none";
7005
7681
  if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
7006
- output += `${pc.bold("Your project will be available at:")}\n`;
7007
- if (hasWeb) output += `${pc.cyan("")} Frontend: http://localhost:${webPort}\n`;
7008
- else if (!hasNative && !addons?.includes("starlight")) output += `${pc.yellow("NOTE:")} You are creating a backend-only app\n (no frontend selected)\n`;
7682
+ const localServices = [];
7683
+ let localDevelopmentNote = "";
7684
+ if (hasWeb) localServices.push({
7685
+ label: "Frontend",
7686
+ url: `http://localhost:${webPort}`
7687
+ });
7688
+ else if (!hasNative && !addons?.includes("starlight")) localDevelopmentNote = "Backend-only app — no frontend selected";
7009
7689
  if (!isConvex && !isBackendSelf && hasStandaloneBackend) {
7010
- output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
7011
- if (api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
7690
+ localServices.push({
7691
+ label: "API",
7692
+ url: "http://localhost:3000"
7693
+ });
7694
+ if (api === "orpc") localServices.push({
7695
+ label: "API reference",
7696
+ url: "http://localhost:3000/api-reference"
7697
+ });
7012
7698
  }
7013
7699
  if (isBackendSelf && api === "orpc") {
7014
7700
  const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
7015
- output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
7701
+ localServices.push({
7702
+ label: "API reference",
7703
+ url: `http://localhost:${webPort}${rpcPath}/api-reference`
7704
+ });
7705
+ }
7706
+ if (addons?.includes("starlight")) localServices.push({
7707
+ label: "Docs",
7708
+ url: "http://localhost:4321"
7709
+ });
7710
+ if (addons?.includes("fumadocs")) localServices.push({
7711
+ label: "Fumadocs",
7712
+ url: "http://localhost:4000"
7713
+ });
7714
+ output += `\n${pc.bold("Local development")}\n`;
7715
+ if (localDevelopmentNote) output += `${pc.dim(localDevelopmentNote)}\n`;
7716
+ if (localServices.length > 0) {
7717
+ const labelWidth = Math.max(...localServices.map(({ label }) => label.length));
7718
+ for (const { label, url } of localServices) output += `${pc.dim(label.padEnd(labelWidth))} ${pc.cyan(url)}\n`;
7016
7719
  }
7017
- if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
7018
- if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
7019
7720
  }
7020
7721
  if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
7021
7722
  if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
@@ -7035,10 +7736,15 @@ async function displayPostInstallInstructions(config) {
7035
7736
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
7036
7737
  const sponsorsResult = await fetchSponsorsQuietly();
7037
7738
  const specialSponsorsSection = sponsorsResult.isOk() ? formatPostInstallSpecialSponsorsSection(sponsorsResult.value) : "";
7038
- if (specialSponsorsSection) output += `\n${specialSponsorsSection.trim()}\n`;
7039
- output += `\n${pc.bold("Like Better-T-Stack?")} Please consider giving us a star\n on GitHub:\n`;
7040
- output += pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack");
7041
- cliConsola.box(output);
7739
+ log.message([], { spacing: 1 });
7740
+ box(output.trimEnd(), pc.bold("Next steps"), {
7741
+ contentPadding: 2,
7742
+ formatBorder: pc.dim,
7743
+ rounded: true,
7744
+ width: "auto"
7745
+ });
7746
+ if (specialSponsorsSection) cliLog.message(specialSponsorsSection);
7747
+ cliLog.message(`${pc.bold("Like Better T Stack?")} ${pc.dim("Star the project on GitHub")}\n${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`);
7042
7748
  }
7043
7749
  function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
7044
7750
  const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
@@ -7065,43 +7771,82 @@ function getVitePlusNativeHooksInstructions(runCmd) {
7065
7771
  return `${pc.bold("Vite+ native Git hooks:")}\n${pc.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
7066
7772
  }
7067
7773
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
7068
- const instructions = [];
7774
+ const notes = [];
7775
+ const commands = [];
7069
7776
  const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
7070
7777
  if (dbSetup === "docker") {
7071
7778
  const dockerStatus = await getDockerStatus(database);
7072
- if (dockerStatus.message) {
7073
- instructions.push(dockerStatus.message);
7074
- instructions.push("");
7075
- }
7779
+ if (dockerStatus.message) notes.push(dockerStatus.message);
7076
7780
  }
7077
7781
  if (isD1Alchemy) {
7078
- if (orm === "drizzle") instructions.push(`${pc.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`);
7782
+ if (orm === "drizzle") commands.push({
7783
+ label: "Generate migrations",
7784
+ command: `${runCmd} db:generate`
7785
+ });
7079
7786
  else if (orm === "prisma") {
7080
- instructions.push(`${pc.cyan("•")} Generate Prisma client: ${`${runCmd} db:generate`}`);
7081
- instructions.push(`${pc.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`);
7787
+ commands.push({
7788
+ label: "Generate client",
7789
+ command: `${runCmd} db:generate`
7790
+ });
7791
+ commands.push({
7792
+ label: "Apply migrations",
7793
+ command: `${runCmd} db:migrate`
7794
+ });
7082
7795
  }
7083
7796
  }
7084
7797
  if (dbSetup === "planetscale") {
7085
- if (database === "mysql" && orm === "drizzle") instructions.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
7086
- if (database === "mysql" && orm === "prisma") instructions.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
7798
+ if (database === "mysql" && orm === "drizzle") notes.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
7799
+ if (database === "mysql" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
7087
7800
  }
7088
- if (dbSetup === "turso" && orm === "prisma") instructions.push(`${pc.yellow("NOTE:")} Follow Turso's Prisma guide for migrations via the Turso CLI:\n https://docs.turso.tech/sdk/ts/orm/prisma`);
7801
+ if (dbSetup === "turso" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} Follow Turso's Prisma guide for migrations via the Turso CLI:\n https://docs.turso.tech/sdk/ts/orm/prisma`);
7089
7802
  if (orm === "prisma") {
7090
- if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
7091
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7803
+ if (database === "mongodb" && dbSetup === "docker") notes.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
7804
+ if (dbSetup === "docker") commands.push({
7805
+ label: "Start database",
7806
+ command: `${runCmd} db:start`
7807
+ });
7092
7808
  if (!isD1Alchemy) {
7093
- instructions.push(`${pc.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
7094
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7809
+ commands.push({
7810
+ label: "Generate client",
7811
+ command: `${runCmd} db:generate`
7812
+ });
7813
+ commands.push({
7814
+ label: "Apply schema",
7815
+ command: `${runCmd} db:push`
7816
+ });
7095
7817
  }
7096
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7818
+ if (!isD1Alchemy) commands.push({
7819
+ label: "Open studio",
7820
+ command: `${runCmd} db:studio`
7821
+ });
7097
7822
  } else if (orm === "drizzle") {
7098
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7099
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7100
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7823
+ if (dbSetup === "docker") commands.push({
7824
+ label: "Start database",
7825
+ command: `${runCmd} db:start`
7826
+ });
7827
+ if (!isD1Alchemy) commands.push({
7828
+ label: "Apply schema",
7829
+ command: `${runCmd} db:push`
7830
+ });
7831
+ if (!isD1Alchemy) commands.push({
7832
+ label: "Open studio",
7833
+ command: `${runCmd} db:studio`
7834
+ });
7101
7835
  } else if (orm === "mongoose") {
7102
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7103
- } else if (orm === "none") instructions.push(`${pc.yellow("NOTE:")} Manual database schema setup\n required.`);
7104
- return instructions.length ? `${pc.bold("Database commands:")}\n${instructions.join("\n")}` : "";
7836
+ if (dbSetup === "docker") commands.push({
7837
+ label: "Start database",
7838
+ command: `${runCmd} db:start`
7839
+ });
7840
+ } else if (orm === "none") notes.push(`${pc.yellow("NOTE:")} Manual database schema setup required.`);
7841
+ if (notes.length === 0 && commands.length === 0) return "";
7842
+ const sections = [pc.bold("Database")];
7843
+ if (notes.length > 0) sections.push(notes.join("\n"));
7844
+ if (commands.length > 0) {
7845
+ const labelWidth = Math.max(...commands.map(({ label }) => label.length));
7846
+ const commandRows = commands.map(({ label, command }) => `${pc.dim(label.padEnd(labelWidth))} ${pc.cyan(command)}`);
7847
+ sections.push(commandRows.join("\n"));
7848
+ }
7849
+ return sections.join("\n");
7105
7850
  }
7106
7851
  function getTauriInstructions(runCmd, frontend) {
7107
7852
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
@@ -7268,7 +8013,7 @@ async function createProject(options, cliInput = {}) {
7268
8013
  })
7269
8014
  }));
7270
8015
  yield* Result.await(formatProject(projectDir));
7271
- if (!isSilent()) log.success("Project template successfully scaffolded!");
8016
+ if (!isSilent()) log.success("Project scaffolded");
7272
8017
  if (options.install) yield* Result.await(installDependencies({
7273
8018
  projectDir,
7274
8019
  packageManager: options.packageManager
@@ -7363,19 +8108,16 @@ async function createProjectHandler(input, options = {}) {
7363
8108
  async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7364
8109
  return Result.gen(async function* () {
7365
8110
  if (!isSilent() && input.renderTitle !== false) renderTitle();
7366
- if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
7367
- if (!isSilent() && input.yolo) cliConsola.fatal("YOLO mode enabled - skipping checks. Things may break!");
8111
+ if (!isSilent()) intro(pc.magenta("Configure your new project"));
8112
+ if (!isSilent() && input.yolo) log.warn(pc.yellow("YOLO mode enabled compatibility checks are disabled."));
7368
8113
  let currentPathInput;
7369
8114
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
7370
8115
  else if (input.yes && input.projectName) currentPathInput = input.projectName;
7371
8116
  else if (input.yes) {
7372
8117
  const defaultConfig = getDefaultConfig();
7373
8118
  let defaultName = defaultConfig.relativePath;
7374
- let counter = 1;
7375
- while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
7376
- defaultName = `${defaultConfig.projectName}-${counter}`;
7377
- counter++;
7378
- }
8119
+ const defaultPathState = yield* Result.await(inspectProjectPath(path.resolve(process.cwd(), defaultName)));
8120
+ if (defaultPathState !== "missing" && defaultPathState !== "empty-directory") defaultName = yield* Result.await(findAvailableIncrementedPath(defaultConfig.projectName));
7379
8121
  currentPathInput = defaultName;
7380
8122
  } else currentPathInput = yield* Result.await(Result.tryPromise({
7381
8123
  try: async () => getProjectName(input.projectName),
@@ -7394,6 +8136,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7394
8136
  finalPathInput = conflictResult.finalPathInput;
7395
8137
  shouldClearDirectory = conflictResult.shouldClearDirectory;
7396
8138
  yield* validateResolvedProjectPathInput(finalPathInput);
8139
+ yield* Result.await(validateSafeProjectDirectoryPath(finalPathInput));
7397
8140
  let finalResolvedPath;
7398
8141
  let finalBaseName;
7399
8142
  if (input.dryRun) {
@@ -7424,10 +8167,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7424
8167
  if (templateConfig) {
7425
8168
  const templateName = input.template.toUpperCase();
7426
8169
  const templateDescription = getTemplateDescription(input.template);
7427
- if (!isSilent()) {
7428
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
7429
- log.message(pc.dim(` ${templateDescription}`));
7430
- }
8170
+ if (!isSilent()) log.info(`${pc.dim("Template")} ${pc.bold(pc.cyan(templateName))}\n${pc.dim(templateDescription)}`);
7431
8171
  const userOverrides = {};
7432
8172
  for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
7433
8173
  cliInput = {
@@ -7458,10 +8198,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7458
8198
  message: validationResult.error.message,
7459
8199
  cause: validationResult.error
7460
8200
  }));
7461
- if (!isSilent()) {
7462
- log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
7463
- log.message(displayConfig(config));
7464
- }
8201
+ if (!isSilent()) log.info(pc.dim("Quick setup selected — using defaults and provided flags."));
7465
8202
  } else {
7466
8203
  const flagConfigResult = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
7467
8204
  if (flagConfigResult.isErr()) return Result.err(new CLIError({
@@ -7470,13 +8207,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7470
8207
  }));
7471
8208
  const flagConfig = flagConfigResult.value;
7472
8209
  const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
7473
- if (!isSilent() && Object.keys(otherFlags).length > 0) {
7474
- log.info(pc.yellow("Using these pre-selected options:"));
7475
- log.message(displayConfig(otherFlags));
7476
- log.message("");
7477
- }
8210
+ const isTemplateSetup = input.template && input.template !== "none";
8211
+ if (!isSilent() && !isTemplateSetup && Object.keys(otherFlags).length > 0) log.info(pc.dim("Command-line options applied."));
7478
8212
  config = yield* Result.await(Result.tryPromise({
7479
- try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput),
8213
+ try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput, { skipCompatibilityChecks: cliInput.yolo }),
7480
8214
  catch: (e) => {
7481
8215
  if (e instanceof UserCancelledError) return e;
7482
8216
  return new CLIError({
@@ -7498,12 +8232,16 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7498
8232
  const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
7499
8233
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
7500
8234
  }
8235
+ if (!isSilent()) {
8236
+ log.info(pc.magenta(pc.bold("Stack ready")));
8237
+ log.message(displayConfig(config));
8238
+ }
7501
8239
  const reproducibleCommand = generateReproducibleCommand(config);
7502
8240
  if (input.dryRun) {
7503
8241
  const elapsedTimeMs = Date.now() - startTime;
7504
8242
  if (!isSilent()) {
7505
8243
  if (shouldClearDirectory) log.warn(pc.yellow(`Dry run: directory "${finalPathInput}" would be cleared due to overwrite strategy.`));
7506
- log.success(pc.green("Dry run validation passed. No files were written."));
8244
+ log.success(pc.green("Configuration ready. No files were written."));
7507
8245
  log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
7508
8246
  log.message(pc.dim(`Run without --dry-run to create the project.`));
7509
8247
  outro(pc.magenta("Dry run complete."));
@@ -7522,14 +8260,19 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7522
8260
  manualDb: cliInput.manualDb ?? input.manualDb,
7523
8261
  dbSetupOptions: effectiveDbSetupOptions
7524
8262
  }));
7525
- if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
7526
8263
  await trackProjectCreation(config, input.disableAnalytics);
7527
8264
  const historyResult = await addToHistory(config, reproducibleCommand);
7528
- if (historyResult.isErr() && !isSilent()) log.warn(pc.yellow(historyResult.error.message));
8265
+ if (historyResult.isErr() && !isSilent()) {
8266
+ log.warn(pc.yellow(historyResult.error.message));
8267
+ log.message(`${pc.dim("Recreate this stack")}\n${pc.cyan(reproducibleCommand)}`);
8268
+ } else if (!isSilent()) {
8269
+ const historyCommand = getCliSubcommandCommand("history", config.packageManager);
8270
+ log.message(`${pc.dim("Setup saved to history")}\n${pc.cyan(historyCommand)}`);
8271
+ }
7529
8272
  const elapsedTimeMs = Date.now() - startTime;
7530
8273
  if (!isSilent()) {
7531
- const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
7532
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
8274
+ const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(1);
8275
+ outro(pc.magenta(`Project ready in ${pc.bold(`${elapsedTimeInSeconds}s`)}`));
7533
8276
  }
7534
8277
  return Result.ok({
7535
8278
  success: true,
@@ -7582,15 +8325,23 @@ async function handleDirectoryConflictResult(currentPathInput, strategy) {
7582
8325
  });
7583
8326
  }
7584
8327
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
7585
- const currentPath = path.resolve(process.cwd(), currentPathInput);
7586
- if (!await fs.pathExists(currentPath)) return Result.ok({
7587
- finalPathInput: currentPathInput,
7588
- shouldClearDirectory: false
7589
- });
7590
- if (!((await fs.readdir(currentPath)).length > 0)) return Result.ok({
8328
+ const pathStateResult = await inspectProjectPath(path.resolve(process.cwd(), currentPathInput));
8329
+ if (pathStateResult.isErr()) return Result.err(pathStateResult.error);
8330
+ const pathState = pathStateResult.value;
8331
+ if (pathState === "missing" || pathState === "empty-directory") return Result.ok({
7591
8332
  finalPathInput: currentPathInput,
7592
8333
  shouldClearDirectory: false
7593
8334
  });
8335
+ if (strategy === "increment") {
8336
+ const incrementResult = await findAvailableIncrementedPath(currentPathInput);
8337
+ if (incrementResult.isErr()) return Result.err(incrementResult.error);
8338
+ return Result.ok({
8339
+ finalPathInput: incrementResult.value,
8340
+ shouldClearDirectory: false
8341
+ });
8342
+ }
8343
+ 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".` }));
8344
+ if (pathState === "non-directory") return Result.err(new CLIError({ message: `Project path "${currentPathInput}" exists and is not a directory. Choose a different path or use directoryConflict: "increment".` }));
7594
8345
  switch (strategy) {
7595
8346
  case "overwrite": return Result.ok({
7596
8347
  finalPathInput: currentPathInput,
@@ -7600,19 +8351,6 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
7600
8351
  finalPathInput: currentPathInput,
7601
8352
  shouldClearDirectory: false
7602
8353
  });
7603
- case "increment": {
7604
- let counter = 1;
7605
- const baseName = currentPathInput;
7606
- let finalPathInput = `${baseName}-${counter}`;
7607
- while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
7608
- counter++;
7609
- finalPathInput = `${baseName}-${counter}`;
7610
- }
7611
- return Result.ok({
7612
- finalPathInput,
7613
- shouldClearDirectory: false
7614
- });
7615
- }
7616
8354
  case "error": return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
7617
8355
  default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
7618
8356
  }
@@ -7647,6 +8385,10 @@ const SchemaNameSchema = z.enum([
7647
8385
  "betterTStackConfigFile",
7648
8386
  "initResult"
7649
8387
  ]).default("all");
8388
+ const CreateVirtualInputSchema = types_exports.ProjectConfigSchema.omit({
8389
+ projectDir: true,
8390
+ relativePath: true
8391
+ }).partial().strict();
7650
8392
  const t = initTRPC.meta().create();
7651
8393
  function getCliSchemaJson() {
7652
8394
  return createCli({
@@ -7702,11 +8444,11 @@ const router = t.router({
7702
8444
  projectName,
7703
8445
  ...options
7704
8446
  });
7705
- if (options.verbose || options.dryRun) return result;
8447
+ if (options.verbose) return result;
7706
8448
  }),
7707
8449
  createJson: t.procedure.meta({
7708
8450
  description: "Create a project from a raw JSON payload (agent-friendly)",
7709
- jsonInput: true
8451
+ jsonInput: "always"
7710
8452
  }).input(types_exports.CreateInputSchema).mutation(async ({ input }) => {
7711
8453
  const result = await createProjectHandler(input, { silent: true });
7712
8454
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -7721,13 +8463,14 @@ const router = t.router({
7721
8463
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
7722
8464
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
7723
8465
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
7724
- projectDir: z.string().optional().describe("Project directory (defaults to current)")
8466
+ projectDir: z.string().optional().describe("Project directory (defaults to current)"),
8467
+ dryRun: z.boolean().optional().default(false).describe("Preview addon changes without writing files")
7725
8468
  })).mutation(async ({ input }) => {
7726
8469
  await addHandler(input);
7727
8470
  }),
7728
8471
  addJson: t.procedure.meta({
7729
8472
  description: "Add addons from a raw JSON payload (agent-friendly)",
7730
- jsonInput: true
8473
+ jsonInput: "always"
7731
8474
  }).input(types_exports.AddInputSchema).mutation(async ({ input }) => {
7732
8475
  const result = await addHandler(input, { silent: true });
7733
8476
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -7749,6 +8492,12 @@ function createBtsCli() {
7749
8492
  version: getLatestCLIVersion()
7750
8493
  });
7751
8494
  }
8495
+ function formatInputValidationError(label, error) {
8496
+ return `Invalid ${label} input: ${error.issues.map((issue) => {
8497
+ const field = issue.path.join(".");
8498
+ return field ? `${field}: ${issue.message}` : issue.message;
8499
+ }).join("; ")}`;
8500
+ }
7752
8501
  /**
7753
8502
  * Programmatic API to create a new Better-T-Stack project.
7754
8503
  * Returns a Result type - no console output, no interactive prompts.
@@ -7775,13 +8524,21 @@ function createBtsCli() {
7775
8524
  * ```
7776
8525
  */
7777
8526
  async function create(projectName, options) {
7778
- const input = {
8527
+ const rawInput = options === void 0 || typeof options === "object" && options !== null ? {
7779
8528
  ...options,
7780
- projectName,
8529
+ projectName
8530
+ } : options;
8531
+ const parsedInput = types_exports.CreateInputSchema.safeParse(rawInput);
8532
+ if (!parsedInput.success) return Result.err(new CLIError({
8533
+ message: formatInputValidationError("create", parsedInput.error),
8534
+ cause: parsedInput.error
8535
+ }));
8536
+ const input = {
8537
+ ...parsedInput.data,
7781
8538
  renderTitle: false,
7782
8539
  verbose: true,
7783
- disableAnalytics: options?.disableAnalytics ?? true,
7784
- directoryConflict: options?.directoryConflict ?? "error"
8540
+ disableAnalytics: parsedInput.data.disableAnalytics ?? true,
8541
+ directoryConflict: parsedInput.data.directoryConflict ?? "error"
7785
8542
  };
7786
8543
  return Result.tryPromise({
7787
8544
  try: async () => {
@@ -7834,30 +8591,37 @@ async function builder() {
7834
8591
  * ```
7835
8592
  */
7836
8593
  async function createVirtual(options) {
8594
+ const parsedInput = CreateVirtualInputSchema.safeParse(options);
8595
+ if (!parsedInput.success) return Result.err(new GeneratorError({
8596
+ message: formatInputValidationError("virtual create", parsedInput.error),
8597
+ phase: "validation",
8598
+ cause: parsedInput.error
8599
+ }));
8600
+ const virtualOptions = parsedInput.data;
7837
8601
  const config = {
7838
- projectName: options.projectName || "my-project",
8602
+ projectName: virtualOptions.projectName || "my-project",
7839
8603
  projectDir: "/virtual",
7840
8604
  relativePath: "./virtual",
7841
- addonOptions: options.addonOptions,
7842
- dbSetupOptions: options.dbSetupOptions,
7843
- database: options.database || "none",
7844
- orm: options.orm || "none",
7845
- backend: options.backend || "hono",
7846
- runtime: options.runtime || "bun",
7847
- frontend: options.frontend || ["tanstack-router"],
7848
- addons: options.addons || [],
7849
- examples: options.examples || [],
7850
- auth: options.auth || "none",
7851
- payments: options.payments || "none",
7852
- git: options.git ?? false,
7853
- packageManager: options.packageManager || "bun",
8605
+ addonOptions: virtualOptions.addonOptions,
8606
+ dbSetupOptions: virtualOptions.dbSetupOptions,
8607
+ database: virtualOptions.database || "none",
8608
+ orm: virtualOptions.orm || "none",
8609
+ backend: virtualOptions.backend || "hono",
8610
+ runtime: virtualOptions.runtime || "bun",
8611
+ frontend: virtualOptions.frontend || ["tanstack-router"],
8612
+ addons: virtualOptions.addons || [],
8613
+ examples: virtualOptions.examples || [],
8614
+ auth: virtualOptions.auth || "none",
8615
+ payments: virtualOptions.payments || "none",
8616
+ git: virtualOptions.git ?? false,
8617
+ packageManager: virtualOptions.packageManager || "bun",
7854
8618
  install: false,
7855
- dbSetup: options.dbSetup || "none",
7856
- api: options.api || "trpc",
7857
- webDeploy: options.webDeploy || "none",
7858
- serverDeploy: options.serverDeploy || "none"
8619
+ dbSetup: virtualOptions.dbSetup || "none",
8620
+ api: virtualOptions.api || "trpc",
8621
+ webDeploy: virtualOptions.webDeploy || "none",
8622
+ serverDeploy: virtualOptions.serverDeploy || "none"
7859
8623
  };
7860
- const validationResult = validateConfigCompatibility(config, new Set([
8624
+ const validationResult = validateConfigCompatibility(config, /* @__PURE__ */ new Set([
7861
8625
  "database",
7862
8626
  "orm",
7863
8627
  "backend",
@@ -7894,13 +8658,25 @@ async function createVirtual(options) {
7894
8658
  * install: true,
7895
8659
  * });
7896
8660
  *
7897
- * if (result?.success) {
8661
+ * if (result.success) {
7898
8662
  * console.log(`Added: ${result.addedAddons.join(", ")}`);
7899
8663
  * }
7900
8664
  * ```
7901
8665
  */
7902
8666
  async function add(options = {}) {
7903
- return addHandler(options, { silent: true });
8667
+ const parsedInput = types_exports.AddInputSchema.safeParse(options);
8668
+ if (!parsedInput.success) return {
8669
+ success: false,
8670
+ addedAddons: [],
8671
+ projectDir: "",
8672
+ error: formatInputValidationError("add", parsedInput.error)
8673
+ };
8674
+ return await addHandler(parsedInput.data, { silent: true }) ?? {
8675
+ success: false,
8676
+ addedAddons: [],
8677
+ projectDir: parsedInput.data.projectDir ?? "",
8678
+ error: "Operation cancelled"
8679
+ };
7904
8680
  }
7905
8681
  //#endregion
7906
8682
  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 };