create-better-t-stack 3.10.0 → 3.11.0-pr749.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.
Files changed (124) hide show
  1. package/bin/create-better-t-stack +98 -0
  2. package/package.json +40 -30
  3. package/src/api.ts +203 -0
  4. package/src/cli.ts +185 -0
  5. package/src/constants.ts +270 -0
  6. package/src/helpers/addons/addons-setup.ts +201 -0
  7. package/src/helpers/addons/examples-setup.ts +137 -0
  8. package/src/helpers/addons/fumadocs-setup.ts +99 -0
  9. package/src/helpers/addons/oxlint-setup.ts +36 -0
  10. package/src/helpers/addons/ruler-setup.ts +135 -0
  11. package/src/helpers/addons/starlight-setup.ts +45 -0
  12. package/src/helpers/addons/tauri-setup.ts +90 -0
  13. package/src/helpers/addons/tui-setup.ts +64 -0
  14. package/src/helpers/addons/ultracite-setup.ts +228 -0
  15. package/src/helpers/addons/vite-pwa-setup.ts +59 -0
  16. package/src/helpers/addons/wxt-setup.ts +86 -0
  17. package/src/helpers/core/add-addons.ts +85 -0
  18. package/src/helpers/core/add-deployment.ts +102 -0
  19. package/src/helpers/core/api-setup.ts +280 -0
  20. package/src/helpers/core/auth-setup.ts +203 -0
  21. package/src/helpers/core/backend-setup.ts +69 -0
  22. package/src/helpers/core/command-handlers.ts +354 -0
  23. package/src/helpers/core/convex-codegen.ts +14 -0
  24. package/src/helpers/core/create-project.ts +134 -0
  25. package/src/helpers/core/create-readme.ts +694 -0
  26. package/src/helpers/core/db-setup.ts +184 -0
  27. package/src/helpers/core/detect-project-config.ts +41 -0
  28. package/src/helpers/core/env-setup.ts +481 -0
  29. package/src/helpers/core/git.ts +23 -0
  30. package/src/helpers/core/install-dependencies.ts +29 -0
  31. package/src/helpers/core/payments-setup.ts +48 -0
  32. package/src/helpers/core/post-installation.ts +403 -0
  33. package/src/helpers/core/project-config.ts +250 -0
  34. package/src/helpers/core/runtime-setup.ts +76 -0
  35. package/src/helpers/core/template-manager.ts +917 -0
  36. package/src/helpers/core/workspace-setup.ts +184 -0
  37. package/src/helpers/database-providers/d1-setup.ts +28 -0
  38. package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
  39. package/src/helpers/database-providers/mongodb-atlas-setup.ts +182 -0
  40. package/src/helpers/database-providers/neon-setup.ts +240 -0
  41. package/src/helpers/database-providers/planetscale-setup.ts +78 -0
  42. package/src/helpers/database-providers/prisma-postgres-setup.ts +193 -0
  43. package/src/helpers/database-providers/supabase-setup.ts +196 -0
  44. package/src/helpers/database-providers/turso-setup.ts +309 -0
  45. package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
  46. package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +52 -0
  47. package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +105 -0
  48. package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +33 -0
  49. package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +33 -0
  50. package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +99 -0
  51. package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +34 -0
  52. package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +99 -0
  53. package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
  54. package/src/helpers/deployment/alchemy/index.ts +7 -0
  55. package/src/helpers/deployment/server-deploy-setup.ts +55 -0
  56. package/src/helpers/deployment/web-deploy-setup.ts +58 -0
  57. package/src/index.ts +51 -0
  58. package/src/prompts/addons.ts +200 -0
  59. package/src/prompts/api.ts +49 -0
  60. package/src/prompts/auth.ts +84 -0
  61. package/src/prompts/backend.ts +83 -0
  62. package/src/prompts/config-prompts.ts +138 -0
  63. package/src/prompts/database-setup.ts +112 -0
  64. package/src/prompts/database.ts +57 -0
  65. package/src/prompts/examples.ts +60 -0
  66. package/src/prompts/frontend.ts +118 -0
  67. package/src/prompts/git.ts +16 -0
  68. package/src/prompts/install.ts +16 -0
  69. package/src/prompts/orm.ts +53 -0
  70. package/src/prompts/package-manager.ts +32 -0
  71. package/src/prompts/payments.ts +50 -0
  72. package/src/prompts/project-name.ts +86 -0
  73. package/src/prompts/runtime.ts +47 -0
  74. package/src/prompts/server-deploy.ts +91 -0
  75. package/src/prompts/web-deploy.ts +107 -0
  76. package/src/tui/app.tsx +1062 -0
  77. package/src/types.ts +70 -0
  78. package/src/utils/add-package-deps.ts +57 -0
  79. package/src/utils/analytics.ts +39 -0
  80. package/src/utils/better-auth-plugin-setup.ts +71 -0
  81. package/src/utils/bts-config.ts +122 -0
  82. package/src/utils/command-exists.ts +16 -0
  83. package/src/utils/compatibility-rules.ts +337 -0
  84. package/src/utils/compatibility.ts +11 -0
  85. package/src/utils/config-processing.ts +130 -0
  86. package/src/utils/config-validation.ts +470 -0
  87. package/src/utils/display-config.ts +96 -0
  88. package/src/utils/docker-utils.ts +70 -0
  89. package/src/utils/errors.ts +30 -0
  90. package/src/utils/file-formatter.ts +11 -0
  91. package/src/utils/generate-reproducible-command.ts +53 -0
  92. package/src/utils/get-latest-cli-version.ts +27 -0
  93. package/src/utils/get-package-manager.ts +13 -0
  94. package/src/utils/open-url.ts +18 -0
  95. package/src/utils/package-runner.ts +23 -0
  96. package/src/utils/project-directory.ts +102 -0
  97. package/src/utils/project-name-validation.ts +43 -0
  98. package/src/utils/render-title.ts +48 -0
  99. package/src/utils/setup-catalogs.ts +192 -0
  100. package/src/utils/sponsors.ts +101 -0
  101. package/src/utils/telemetry.ts +19 -0
  102. package/src/utils/template-processor.ts +64 -0
  103. package/src/utils/templates.ts +94 -0
  104. package/src/utils/ts-morph.ts +26 -0
  105. package/src/validation.ts +117 -0
  106. package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +1 -1
  107. package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
  108. package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
  109. package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
  110. package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
  111. package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
  112. package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
  113. package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
  114. package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
  115. package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
  116. package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
  117. package/templates/frontend/react/web-base/src/index.css.hbs +1 -1
  118. package/dist/cli.d.mts +0 -1
  119. package/dist/cli.mjs +0 -8
  120. package/dist/index.d.mts +0 -347
  121. package/dist/index.mjs +0 -4
  122. package/dist/src-QkFdHtZE.mjs +0 -7072
  123. package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
  124. package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * TUI Entry Point using @opentui/react
3
+ * Stacked prompt design EXACTLY matching src/prompts/*
4
+ */
5
+ import { createCliRenderer } from "@opentui/core";
6
+ import { createRoot, useKeyboard, useTerminalDimensions } from "@opentui/react";
7
+ import { useState, useCallback, useEffect } from "react";
8
+ import type {
9
+ ProjectConfig,
10
+ Frontend,
11
+ Backend,
12
+ Runtime,
13
+ Database,
14
+ ORM,
15
+ API,
16
+ Auth,
17
+ Payments,
18
+ Addons,
19
+ Examples,
20
+ DatabaseSetup,
21
+ WebDeploy,
22
+ ServerDeploy,
23
+ PackageManager,
24
+ } from "../types";
25
+ import { getDefaultConfig, DEFAULT_CONFIG } from "../constants";
26
+
27
+ // Dark theme
28
+ const theme = {
29
+ bg: "#111111",
30
+ surface: "#1a1a1a",
31
+ text: "#e4e4e7",
32
+ subtext: "#a1a1aa",
33
+ muted: "#52525b",
34
+ primary: "#8b5cf6",
35
+ success: "#22c55e",
36
+ error: "#ef4444",
37
+ border: "#27272a",
38
+ };
39
+
40
+ export interface TuiOptions {
41
+ initialConfig?: Partial<ProjectConfig>;
42
+ onComplete: (config: ProjectConfig) => Promise<void>;
43
+ onCancel: () => void;
44
+ }
45
+
46
+ interface StepConfig {
47
+ id: string;
48
+ title: string;
49
+ type: "input" | "select" | "multiselect" | "confirm";
50
+ skip?: (config: any) => boolean;
51
+ getDefault?: (config: any) => any;
52
+ getOptions?: (config: any) => { name: string; value: string; hint?: string }[];
53
+ options?: { name: string; value: string; hint?: string }[];
54
+ }
55
+
56
+ // Steps EXACTLY matching config-prompts.ts order
57
+ const STEPS: StepConfig[] = [
58
+ // Project Name
59
+ { id: "projectName", title: "Project name", type: "input" },
60
+
61
+ // Frontend - first asks for project type (web/native)
62
+ {
63
+ id: "projectType",
64
+ title: "Select project type",
65
+ type: "multiselect",
66
+ options: [
67
+ { name: "Web", value: "web", hint: "React, Vue or Svelte Web Application" },
68
+ { name: "Native", value: "native", hint: "Create a React Native/Expo app" },
69
+ ],
70
+ },
71
+
72
+ // Web Framework - matching frontend.ts exactly
73
+ {
74
+ id: "webFramework",
75
+ title: "Choose web",
76
+ type: "select",
77
+ skip: (c) => !c.projectType?.includes("web"),
78
+ options: [
79
+ {
80
+ name: "TanStack Router",
81
+ value: "tanstack-router",
82
+ hint: "Modern and scalable routing for React Applications",
83
+ },
84
+ {
85
+ name: "React Router",
86
+ value: "react-router",
87
+ hint: "A user‑obsessed, standards‑focused, multi‑strategy router",
88
+ },
89
+ { name: "Next.js", value: "next", hint: "The React Framework for the Web" },
90
+ { name: "Nuxt", value: "nuxt", hint: "The Progressive Web Framework for Vue.js" },
91
+ { name: "Svelte", value: "svelte", hint: "web development for the rest of us" },
92
+ {
93
+ name: "Solid",
94
+ value: "solid",
95
+ hint: "Simple and performant reactivity for building user interfaces",
96
+ },
97
+ {
98
+ name: "TanStack Start",
99
+ value: "tanstack-start",
100
+ hint: "SSR, Server Functions, API Routes and more with TanStack Router",
101
+ },
102
+ ],
103
+ },
104
+
105
+ // Native Framework - matching frontend.ts exactly
106
+ {
107
+ id: "nativeFramework",
108
+ title: "Choose native",
109
+ type: "select",
110
+ skip: (c) => !c.projectType?.includes("native"),
111
+ options: [
112
+ { name: "Bare", value: "native-bare", hint: "Bare Expo without styling library" },
113
+ {
114
+ name: "Uniwind",
115
+ value: "native-uniwind",
116
+ hint: "Fastest Tailwind bindings for React Native with HeroUI Native",
117
+ },
118
+ { name: "Unistyles", value: "native-unistyles", hint: "Consistent styling for React Native" },
119
+ ],
120
+ },
121
+
122
+ // Backend - matching backend.ts exactly
123
+ {
124
+ id: "backend",
125
+ title: "Select backend",
126
+ type: "select",
127
+ getOptions: (c) => {
128
+ const frontends = c.frontend || [];
129
+ const hasFullstack = frontends.some((f: string) => ["next", "tanstack-start"].includes(f));
130
+ const hasSolid = frontends.some((f: string) => f === "solid");
131
+
132
+ const opts = [];
133
+ if (hasFullstack) {
134
+ opts.push({
135
+ name: "Self (Fullstack)",
136
+ value: "self",
137
+ hint: "Use frontend's built-in api routes",
138
+ });
139
+ }
140
+ opts.push(
141
+ { name: "Hono", value: "hono", hint: "Lightweight, ultrafast web framework" },
142
+ {
143
+ name: "Express",
144
+ value: "express",
145
+ hint: "Fast, unopinionated, minimalist web framework for Node.js",
146
+ },
147
+ { name: "Fastify", value: "fastify", hint: "Fast, low-overhead web framework for Node.js" },
148
+ {
149
+ name: "Elysia",
150
+ value: "elysia",
151
+ hint: "Ergonomic web framework for building backend servers",
152
+ },
153
+ );
154
+ if (!hasSolid) {
155
+ opts.push({
156
+ name: "Convex",
157
+ value: "convex",
158
+ hint: "Reactive backend-as-a-service platform",
159
+ });
160
+ }
161
+ opts.push({ name: "None", value: "none", hint: "No backend server" });
162
+ return opts;
163
+ },
164
+ },
165
+
166
+ // Runtime - matching runtime.ts exactly
167
+ {
168
+ id: "runtime",
169
+ title: "Select runtime",
170
+ type: "select",
171
+ skip: (c) => c.backend === "none" || c.backend === "convex" || c.backend === "self",
172
+ getDefault: () => "none",
173
+ getOptions: (c) => {
174
+ const opts = [
175
+ { name: "Bun", value: "bun", hint: "Fast all-in-one JavaScript runtime" },
176
+ { name: "Node.js", value: "node", hint: "Traditional Node.js runtime" },
177
+ ];
178
+ if (c.backend === "hono") {
179
+ opts.push({
180
+ name: "Cloudflare Workers",
181
+ value: "workers",
182
+ hint: "Edge runtime on Cloudflare's global network",
183
+ });
184
+ }
185
+ return opts;
186
+ },
187
+ },
188
+
189
+ // Database - matching database.ts exactly
190
+ {
191
+ id: "database",
192
+ title: "Select database",
193
+ type: "select",
194
+ skip: (c) => c.backend === "none" || c.backend === "convex",
195
+ getDefault: () => "none",
196
+ getOptions: (c) => {
197
+ const opts = [
198
+ { name: "None", value: "none", hint: "No database setup" },
199
+ {
200
+ name: "SQLite",
201
+ value: "sqlite",
202
+ hint: "lightweight, server-less, embedded relational database",
203
+ },
204
+ {
205
+ name: "PostgreSQL",
206
+ value: "postgres",
207
+ hint: "powerful, open source object-relational database system",
208
+ },
209
+ { name: "MySQL", value: "mysql", hint: "popular open-source relational database system" },
210
+ ];
211
+ if (c.runtime !== "workers") {
212
+ opts.push({
213
+ name: "MongoDB",
214
+ value: "mongodb",
215
+ hint: "open-source NoSQL database that stores data in JSON-like documents",
216
+ });
217
+ }
218
+ return opts;
219
+ },
220
+ },
221
+
222
+ // ORM - matching orm.ts exactly
223
+ {
224
+ id: "orm",
225
+ title: "Select ORM",
226
+ type: "select",
227
+ skip: (c) => c.database === "none" || c.backend === "convex",
228
+ getDefault: () => "none",
229
+ getOptions: (c) => {
230
+ if (c.database === "mongodb") {
231
+ return [
232
+ { name: "Prisma", value: "prisma", hint: "Powerful, feature-rich ORM" },
233
+ { name: "Mongoose", value: "mongoose", hint: "Elegant object modeling tool" },
234
+ ];
235
+ }
236
+ return [
237
+ { name: "Drizzle", value: "drizzle", hint: "Lightweight and performant TypeScript ORM" },
238
+ { name: "Prisma", value: "prisma", hint: "Powerful, feature-rich ORM" },
239
+ ];
240
+ },
241
+ },
242
+
243
+ // API - matching api.ts exactly
244
+ {
245
+ id: "api",
246
+ title: "Select API type",
247
+ type: "select",
248
+ skip: (c) => c.backend === "none" || c.backend === "convex",
249
+ getDefault: () => "none",
250
+ options: [
251
+ { name: "tRPC", value: "trpc", hint: "End-to-end typesafe APIs made easy" },
252
+ {
253
+ name: "oRPC",
254
+ value: "orpc",
255
+ hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
256
+ },
257
+ {
258
+ name: "None",
259
+ value: "none",
260
+ hint: "No API layer (e.g. for full-stack frameworks with Route Handlers)",
261
+ },
262
+ ],
263
+ },
264
+
265
+ // Auth - matching auth.ts exactly
266
+ {
267
+ id: "auth",
268
+ title: "Select authentication provider",
269
+ type: "select",
270
+ skip: (c) => c.backend === "none",
271
+ getDefault: () => "none",
272
+ getOptions: (c) => {
273
+ if (c.backend === "convex") {
274
+ const frontends = c.frontend || [];
275
+ const opts = [];
276
+ const supportsBetterAuth = frontends.some((f: string) =>
277
+ [
278
+ "tanstack-router",
279
+ "tanstack-start",
280
+ "next",
281
+ "native-bare",
282
+ "native-uniwind",
283
+ "native-unistyles",
284
+ ].includes(f),
285
+ );
286
+ const supportsClerk = frontends.some((f: string) =>
287
+ [
288
+ "react-router",
289
+ "tanstack-router",
290
+ "tanstack-start",
291
+ "next",
292
+ "native-bare",
293
+ "native-uniwind",
294
+ "native-unistyles",
295
+ ].includes(f),
296
+ );
297
+ if (supportsBetterAuth) {
298
+ opts.push({
299
+ name: "Better-Auth",
300
+ value: "better-auth",
301
+ hint: "comprehensive auth framework for TypeScript",
302
+ });
303
+ }
304
+ if (supportsClerk) {
305
+ opts.push({
306
+ name: "Clerk",
307
+ value: "clerk",
308
+ hint: "More than auth, Complete User Management",
309
+ });
310
+ }
311
+ opts.push({ name: "None", value: "none", hint: "No auth" });
312
+ return opts;
313
+ }
314
+ return [
315
+ {
316
+ name: "Better-Auth",
317
+ value: "better-auth",
318
+ hint: "comprehensive auth framework for TypeScript",
319
+ },
320
+ { name: "None", value: "none" },
321
+ ];
322
+ },
323
+ },
324
+
325
+ // Payments - matching payments.ts exactly
326
+ {
327
+ id: "payments",
328
+ title: "Select payments provider",
329
+ type: "select",
330
+ skip: (c) => {
331
+ if (c.backend === "none") return true;
332
+ if (c.auth !== "better-auth") return true;
333
+ if (c.backend === "convex") return true;
334
+ const frontends = c.frontend || [];
335
+ const hasWeb = frontends.some((f: string) =>
336
+ [
337
+ "tanstack-router",
338
+ "react-router",
339
+ "next",
340
+ "nuxt",
341
+ "svelte",
342
+ "solid",
343
+ "tanstack-start",
344
+ ].includes(f),
345
+ );
346
+ if (frontends.length > 0 && !hasWeb) return true;
347
+ return false;
348
+ },
349
+ getDefault: () => "none",
350
+ options: [
351
+ {
352
+ name: "Polar",
353
+ value: "polar",
354
+ hint: "Turn your software into a business. 6 lines of code.",
355
+ },
356
+ { name: "None", value: "none", hint: "No payments integration" },
357
+ ],
358
+ },
359
+
360
+ // Addons - matching addons.ts exactly (grouped)
361
+ {
362
+ id: "addons",
363
+ title: "Select addons",
364
+ type: "multiselect",
365
+ options: [
366
+ // Documentation
367
+ { name: "Starlight", value: "starlight", hint: "Build stellar docs with astro" },
368
+ { name: "Fumadocs", value: "fumadocs", hint: "Build excellent documentation site" },
369
+ // Linting
370
+ { name: "Biome", value: "biome", hint: "Format, lint, and more" },
371
+ { name: "Oxlint", value: "oxlint", hint: "Oxlint + Oxfmt (linting & formatting)" },
372
+ {
373
+ name: "Ultracite",
374
+ value: "ultracite",
375
+ hint: "Zero-config Biome preset with AI integration",
376
+ },
377
+ // Other
378
+ { name: "Ruler", value: "ruler", hint: "Centralize your AI rules" },
379
+ { name: "PWA", value: "pwa", hint: "Make your app installable and work offline" },
380
+ { name: "Tauri", value: "tauri", hint: "Build native desktop apps from your web frontend" },
381
+ { name: "Husky", value: "husky", hint: "Modern native Git hooks made easy" },
382
+ { name: "OpenTUI", value: "opentui", hint: "Build terminal user interfaces" },
383
+ { name: "WXT", value: "wxt", hint: "Build browser extensions" },
384
+ { name: "Turborepo", value: "turborepo", hint: "High-performance build system" },
385
+ ],
386
+ },
387
+
388
+ // Examples - matching examples.ts exactly
389
+ {
390
+ id: "examples",
391
+ title: "Include examples",
392
+ type: "multiselect",
393
+ skip: (c) => {
394
+ if (c.backend === "none") return true;
395
+ if (c.backend !== "convex") {
396
+ if (c.api === "none" || c.database === "none") return true;
397
+ }
398
+ return false;
399
+ },
400
+ options: [
401
+ { name: "Todo App", value: "todo", hint: "A simple CRUD example app" },
402
+ { name: "AI Chat", value: "ai", hint: "A simple AI chat interface using AI SDK" },
403
+ ],
404
+ },
405
+
406
+ // Database Setup - matching database-setup.ts
407
+ {
408
+ id: "dbSetup",
409
+ title: "Select database setup option",
410
+ type: "select",
411
+ skip: (c) => c.database === "none" || c.backend === "convex",
412
+ getDefault: () => "none",
413
+ getOptions: (c) => {
414
+ if (c.database === "sqlite") {
415
+ const opts = [
416
+ { name: "Turso", value: "turso", hint: "SQLite for Production. Powered by libSQL" },
417
+ ];
418
+ if (c.runtime === "workers") {
419
+ opts.push({
420
+ name: "Cloudflare D1",
421
+ value: "d1",
422
+ hint: "Cloudflare's managed, serverless database",
423
+ });
424
+ }
425
+ opts.push({ name: "None", value: "none", hint: "Manual setup" });
426
+ return opts;
427
+ }
428
+ if (c.database === "postgres") {
429
+ return [
430
+ {
431
+ name: "Neon Postgres",
432
+ value: "neon",
433
+ hint: "Serverless Postgres with branching capability",
434
+ },
435
+ { name: "PlanetScale", value: "planetscale", hint: "Postgres & Vitess (MySQL) on NVMe" },
436
+ { name: "Supabase", value: "supabase", hint: "Local Supabase stack (requires Docker)" },
437
+ {
438
+ name: "Prisma Postgres",
439
+ value: "prisma-postgres",
440
+ hint: "Instant Postgres for Global Applications",
441
+ },
442
+ { name: "Docker", value: "docker", hint: "Run locally with docker compose" },
443
+ { name: "None", value: "none", hint: "Manual setup" },
444
+ ];
445
+ }
446
+ if (c.database === "mysql") {
447
+ return [
448
+ { name: "PlanetScale", value: "planetscale", hint: "MySQL on Vitess (NVMe, HA)" },
449
+ { name: "Docker", value: "docker", hint: "Run locally with docker compose" },
450
+ { name: "None", value: "none", hint: "Manual setup" },
451
+ ];
452
+ }
453
+ if (c.database === "mongodb") {
454
+ return [
455
+ {
456
+ name: "MongoDB Atlas",
457
+ value: "mongodb-atlas",
458
+ hint: "The most effective way to deploy MongoDB",
459
+ },
460
+ { name: "Docker", value: "docker", hint: "Run locally with docker compose" },
461
+ { name: "None", value: "none", hint: "Manual setup" },
462
+ ];
463
+ }
464
+ return [{ name: "None", value: "none", hint: "Manual setup" }];
465
+ },
466
+ },
467
+
468
+ // Web Deploy - matching web-deploy.ts
469
+ {
470
+ id: "webDeploy",
471
+ title: "Select web deployment",
472
+ type: "select",
473
+ skip: (c) => {
474
+ const frontends = c.frontend || [];
475
+ const hasWeb = frontends.some((f: string) =>
476
+ [
477
+ "tanstack-router",
478
+ "react-router",
479
+ "next",
480
+ "nuxt",
481
+ "svelte",
482
+ "solid",
483
+ "tanstack-start",
484
+ ].includes(f),
485
+ );
486
+ return !hasWeb;
487
+ },
488
+ getDefault: () => "none",
489
+ options: [
490
+ { name: "Alchemy", value: "alchemy", hint: "Deploy to Cloudflare Workers using Alchemy" },
491
+ { name: "None", value: "none", hint: "No deployment" },
492
+ ],
493
+ },
494
+
495
+ // Git - matching git.ts (confirm)
496
+ {
497
+ id: "git",
498
+ title: "Initialize git repository?",
499
+ type: "confirm",
500
+ },
501
+
502
+ // Package Manager - matching package-manager.ts exactly (NO YARN!)
503
+ {
504
+ id: "packageManager",
505
+ title: "Choose package manager",
506
+ type: "select",
507
+ options: [
508
+ { name: "npm", value: "npm", hint: "Node Package Manager" },
509
+ { name: "pnpm", value: "pnpm", hint: "Fast, disk space efficient package manager" },
510
+ { name: "bun", value: "bun", hint: "All-in-one JavaScript runtime & toolkit" },
511
+ ],
512
+ },
513
+
514
+ // Install - matching install.ts (confirm)
515
+ {
516
+ id: "install",
517
+ title: "Install dependencies?",
518
+ type: "confirm",
519
+ },
520
+ ];
521
+
522
+ export async function renderTui(options: TuiOptions): Promise<void> {
523
+ const renderer = await createCliRenderer({ exitOnCtrlC: false });
524
+
525
+ return new Promise((resolve) => {
526
+ const handleExit = () => {
527
+ options.onCancel();
528
+ process.exit(0);
529
+ };
530
+
531
+ createRoot(renderer).render(
532
+ <App
533
+ initialConfig={options.initialConfig}
534
+ onComplete={options.onComplete}
535
+ onExit={handleExit}
536
+ />,
537
+ );
538
+ });
539
+ }
540
+
541
+ // Spinner component using useEffect interval
542
+ function Spinner(props: { text: string }) {
543
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
544
+ const [frame, setFrame] = useState(0);
545
+
546
+ useEffect(() => {
547
+ const interval = setInterval(() => {
548
+ setFrame((f: number) => (f + 1) % frames.length);
549
+ }, 80);
550
+ return () => clearInterval(interval);
551
+ }, []);
552
+
553
+ return (
554
+ <text>
555
+ <span fg={theme.primary}>{frames[frame]}</span>
556
+ <span fg={theme.text}> {props.text}</span>
557
+ </text>
558
+ );
559
+ }
560
+
561
+ type Phase = "prompts" | "creating" | "done";
562
+
563
+ function App(props: {
564
+ initialConfig?: Partial<ProjectConfig>;
565
+ onComplete: (config: ProjectConfig) => Promise<void>;
566
+ onExit: () => void;
567
+ }) {
568
+ const { width, height } = useTerminalDimensions();
569
+ const [stepIndex, setStepIndex] = useState(0);
570
+ const [config, setConfig] = useState<any>(props.initialConfig ?? {});
571
+ const [completed, setCompleted] = useState(false);
572
+ const [phase, setPhase] = useState<Phase>("prompts");
573
+ const [finalConfig, setFinalConfig] = useState<ProjectConfig | null>(null);
574
+ const [creationStatus, setCreationStatus] = useState("Preparing...");
575
+
576
+ useKeyboard((key) => {
577
+ if (key.ctrl && key.name === "c") props.onExit();
578
+ // Exit on any key when done
579
+ if (phase === "done" && key.name !== "c") {
580
+ process.exit(0);
581
+ }
582
+ });
583
+
584
+ const updateConfig = useCallback((key: string, value: any) => {
585
+ setConfig((prev: any) => {
586
+ const newConfig = { ...prev, [key]: value };
587
+ if (key === "webFramework" || key === "nativeFramework" || key === "projectType") {
588
+ const frontends: string[] = [];
589
+ if (newConfig.webFramework) frontends.push(newConfig.webFramework);
590
+ if (newConfig.nativeFramework) frontends.push(newConfig.nativeFramework);
591
+ newConfig.frontend = frontends.length > 0 ? frontends : [];
592
+ }
593
+ return newConfig;
594
+ });
595
+ }, []);
596
+
597
+ const getVisibleSteps = useCallback(() => {
598
+ const visible: { step: StepConfig; index: number }[] = [];
599
+ for (let i = 0; i < STEPS.length; i++) {
600
+ const step = STEPS[i];
601
+ if (!step.skip || !step.skip(config)) visible.push({ step, index: i });
602
+ }
603
+ return visible;
604
+ }, [config]);
605
+
606
+ const visibleSteps = getVisibleSteps();
607
+ const currentVisibleIndex = visibleSteps.findIndex((v) => v.index === stepIndex);
608
+
609
+ const goNext = useCallback(() => {
610
+ let next = stepIndex + 1;
611
+ while (next < STEPS.length) {
612
+ const step = STEPS[next];
613
+ if (!step.skip || !step.skip(config)) {
614
+ setStepIndex(next);
615
+ return;
616
+ }
617
+ if (step.getDefault) updateConfig(step.id, step.getDefault(config));
618
+ next++;
619
+ }
620
+ setCompleted(true);
621
+ }, [stepIndex, config, updateConfig]);
622
+
623
+ const goPrev = useCallback(() => {
624
+ let prev = stepIndex - 1;
625
+ while (prev >= 0) {
626
+ const step = STEPS[prev];
627
+ if (!step.skip || !step.skip(config)) {
628
+ setStepIndex(prev);
629
+ return;
630
+ }
631
+ prev--;
632
+ }
633
+ }, [stepIndex, config]);
634
+
635
+ const handleComplete = useCallback(async () => {
636
+ const defaultConfig = getDefaultConfig();
637
+ const cfg: ProjectConfig = {
638
+ ...defaultConfig,
639
+ ...config,
640
+ projectName: config.projectName ?? "my-app",
641
+ projectDir: process.cwd() + "/" + (config.projectName ?? "my-app"),
642
+ relativePath: config.projectName ?? "my-app",
643
+ frontend: config.frontend?.length ? config.frontend : ["tanstack-router"],
644
+ addons: config.addons?.length ? config.addons : ["none"],
645
+ examples: config.examples ?? [],
646
+ git: config.git === true,
647
+ install: config.install === true,
648
+ } as ProjectConfig;
649
+
650
+ setFinalConfig(cfg);
651
+ setPhase("creating");
652
+ setCreationStatus("Creating project...");
653
+
654
+ try {
655
+ await props.onComplete(cfg);
656
+ setPhase("done");
657
+ } catch (error) {
658
+ setCreationStatus(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
659
+ }
660
+ }, [config, props.onComplete]);
661
+
662
+ const currentStep = STEPS[stepIndex];
663
+ const getValue = (stepId: string) => {
664
+ const val = config[stepId];
665
+ if (typeof val === "boolean") return val ? "yes" : "no";
666
+ if (Array.isArray(val)) return val.length > 0 ? val.join(", ") : "none";
667
+ return val;
668
+ };
669
+ const getOptions = (step: StepConfig) =>
670
+ step.getOptions ? step.getOptions(config) : step.options || [];
671
+
672
+ // Get run command for package manager
673
+ const runCmd =
674
+ config.packageManager === "npm"
675
+ ? "npm run"
676
+ : config.packageManager === "pnpm"
677
+ ? "pnpm run"
678
+ : "bun run";
679
+
680
+ return (
681
+ <box style={{ width, height, backgroundColor: theme.bg, flexDirection: "column" }}>
682
+ <box
683
+ style={{
684
+ height: 5,
685
+ justifyContent: "center",
686
+ alignItems: "center",
687
+ backgroundColor: theme.surface,
688
+ }}
689
+ >
690
+ <ascii-font text="Better T Stack" font="tiny" />
691
+ </box>
692
+
693
+ <box
694
+ style={{
695
+ flexGrow: 1,
696
+ flexDirection: "column",
697
+ padding: 1,
698
+ paddingLeft: 2,
699
+ overflow: "scroll",
700
+ }}
701
+ >
702
+ {/* Prompts Phase */}
703
+ {phase === "prompts" && (
704
+ <>
705
+ {visibleSteps.slice(0, currentVisibleIndex).map(({ step }) => (
706
+ <box key={step.id} style={{ flexDirection: "row" }}>
707
+ <text>
708
+ <span fg={theme.success}>◆</span>
709
+ <span fg={theme.muted}> {step.title}: </span>
710
+ <span fg={theme.text}>{getValue(step.id) || "none"}</span>
711
+ </text>
712
+ </box>
713
+ ))}
714
+
715
+ {!completed && currentStep && (
716
+ <box style={{ marginTop: 1 }}>
717
+ <box style={{ flexDirection: "row", marginBottom: 1 }}>
718
+ <text>
719
+ <span fg={theme.primary}>◇</span>
720
+ <span fg={theme.text}> {currentStep.title}</span>
721
+ </text>
722
+ </box>
723
+ {currentStep.type === "input" && (
724
+ <InputPrompt
725
+ onSubmit={(v) => {
726
+ updateConfig(currentStep.id, v);
727
+ if (currentStep.id === "projectName") {
728
+ updateConfig("projectDir", process.cwd() + "/" + v);
729
+ updateConfig("relativePath", v);
730
+ }
731
+ goNext();
732
+ }}
733
+ onBack={stepIndex > 0 ? goPrev : undefined}
734
+ />
735
+ )}
736
+ {currentStep.type === "select" && (
737
+ <SelectPrompt
738
+ options={getOptions(currentStep)}
739
+ onSelect={(v) => {
740
+ updateConfig(currentStep.id, v);
741
+ goNext();
742
+ }}
743
+ onBack={stepIndex > 0 ? goPrev : undefined}
744
+ />
745
+ )}
746
+ {currentStep.type === "multiselect" && (
747
+ <MultiSelectPrompt
748
+ options={getOptions(currentStep)}
749
+ selected={config[currentStep.id] ?? []}
750
+ onSubmit={(v) => {
751
+ updateConfig(
752
+ currentStep.id,
753
+ v.length > 0 ? v : currentStep.id === "projectType" ? ["web"] : [],
754
+ );
755
+ goNext();
756
+ }}
757
+ onBack={stepIndex > 0 ? goPrev : undefined}
758
+ />
759
+ )}
760
+ {currentStep.type === "confirm" && (
761
+ <ConfirmPrompt
762
+ onSubmit={(v) => {
763
+ updateConfig(currentStep.id, v);
764
+ goNext();
765
+ }}
766
+ onBack={stepIndex > 0 ? goPrev : undefined}
767
+ />
768
+ )}
769
+ </box>
770
+ )}
771
+
772
+ {completed && (
773
+ <ConfirmStep
774
+ config={config}
775
+ onComplete={handleComplete}
776
+ onBack={() => setCompleted(false)}
777
+ />
778
+ )}
779
+
780
+ {!completed &&
781
+ visibleSteps.slice(currentVisibleIndex + 1).map(({ step }) => (
782
+ <box key={step.id} style={{ flexDirection: "row" }}>
783
+ <text>
784
+ <span fg={theme.muted}>○ {step.title}</span>
785
+ </text>
786
+ </box>
787
+ ))}
788
+ </>
789
+ )}
790
+
791
+ {/* Creating Phase - Show spinner */}
792
+ {phase === "creating" && (
793
+ <box style={{ flexDirection: "column", marginTop: 2 }}>
794
+ <Spinner text={creationStatus} />
795
+ <box style={{ marginTop: 2 }}>
796
+ <text>
797
+ <span fg={theme.muted}>Creating </span>
798
+ <span fg={theme.primary}>{config.projectName}</span>
799
+ <span fg={theme.muted}>...</span>
800
+ </text>
801
+ </box>
802
+ </box>
803
+ )}
804
+
805
+ {/* Done Phase - Show post-installation */}
806
+ {phase === "done" && finalConfig && (
807
+ <box style={{ flexDirection: "column" }}>
808
+ <text>
809
+ <span fg={theme.success}>✓</span>
810
+ <span fg={theme.text}> Project created successfully!</span>
811
+ </text>
812
+
813
+ <box style={{ marginTop: 2 }}>
814
+ <text>
815
+ <span fg={theme.text}>Next steps:</span>
816
+ </text>
817
+ </box>
818
+
819
+ <box style={{ paddingLeft: 2, marginTop: 1, flexDirection: "column" }}>
820
+ <text>
821
+ <span fg={theme.primary}>1.</span>
822
+ <span fg={theme.text}> cd {finalConfig.relativePath}</span>
823
+ </text>
824
+ {!finalConfig.install && (
825
+ <text>
826
+ <span fg={theme.primary}>2.</span>
827
+ <span fg={theme.text}> {finalConfig.packageManager} install</span>
828
+ </text>
829
+ )}
830
+ <text>
831
+ <span fg={theme.primary}>{finalConfig.install ? "2" : "3"}.</span>
832
+ <span fg={theme.text}> {runCmd} dev</span>
833
+ </text>
834
+ </box>
835
+
836
+ <box style={{ marginTop: 2 }}>
837
+ <text>
838
+ <span fg={theme.text}>Your project will be available at:</span>
839
+ </text>
840
+ </box>
841
+ <box style={{ paddingLeft: 2, marginTop: 1, flexDirection: "column" }}>
842
+ <text>
843
+ <span fg={theme.primary}>•</span>
844
+ <span fg={theme.text}> Frontend: http://localhost:3001</span>
845
+ </text>
846
+ {finalConfig.backend !== "none" &&
847
+ finalConfig.backend !== "self" &&
848
+ finalConfig.backend !== "convex" && (
849
+ <text>
850
+ <span fg={theme.primary}>•</span>
851
+ <span fg={theme.text}> Backend: http://localhost:3000</span>
852
+ </text>
853
+ )}
854
+ </box>
855
+
856
+ <box style={{ marginTop: 3 }}>
857
+ <text>
858
+ <span fg={theme.success}>★</span>
859
+ <span fg={theme.text}> Like Better-T-Stack? Give us a star on GitHub!</span>
860
+ </text>
861
+ </box>
862
+ <box style={{ paddingLeft: 2 }}>
863
+ <text>
864
+ <span fg={theme.primary}>
865
+ https://github.com/AmanVarshney01/create-better-t-stack
866
+ </span>
867
+ </text>
868
+ </box>
869
+
870
+ <box style={{ marginTop: 3 }}>
871
+ <text>
872
+ <span fg={theme.muted}>Press any key to exit...</span>
873
+ </text>
874
+ </box>
875
+ </box>
876
+ )}
877
+ </box>
878
+
879
+ <box
880
+ style={{
881
+ height: 1,
882
+ backgroundColor: theme.surface,
883
+ paddingLeft: 2,
884
+ paddingRight: 2,
885
+ flexDirection: "row",
886
+ justifyContent: "space-between",
887
+ }}
888
+ >
889
+ <text>
890
+ <span fg={theme.muted}>ctrl+c</span>
891
+ <span fg={theme.subtext}> exit</span>
892
+ </text>
893
+ <text>
894
+ <span fg={theme.primary}>better-t-stack.dev</span>
895
+ </text>
896
+ </box>
897
+ </box>
898
+ );
899
+ }
900
+
901
+ function InputPrompt(props: { onSubmit: (v: string) => void; onBack?: () => void }) {
902
+ useKeyboard((key) => {
903
+ if (key.name === "escape" && props.onBack) props.onBack();
904
+ });
905
+ return (
906
+ <box style={{ flexDirection: "column", paddingLeft: 2 }}>
907
+ <box
908
+ style={{
909
+ border: true,
910
+ borderColor: theme.border,
911
+ height: 3,
912
+ width: 40,
913
+ backgroundColor: theme.surface,
914
+ }}
915
+ >
916
+ <input
917
+ placeholder="my-app"
918
+ focused
919
+ onSubmit={(v: string) => props.onSubmit(v || "my-app")}
920
+ />
921
+ </box>
922
+ <text>
923
+ <span fg={theme.muted}>↵ confirm{props.onBack ? " esc back" : ""}</span>
924
+ </text>
925
+ </box>
926
+ );
927
+ }
928
+
929
+ function SelectPrompt(props: {
930
+ options: { name: string; value: string; hint?: string }[];
931
+ onSelect: (v: string) => void;
932
+ onBack?: () => void;
933
+ }) {
934
+ const [i, setI] = useState(0);
935
+ useKeyboard((key) => {
936
+ if (key.name === "up" || key.name === "k") setI((x: number) => Math.max(0, x - 1));
937
+ else if (key.name === "down" || key.name === "j")
938
+ setI((x: number) => Math.min(props.options.length - 1, x + 1));
939
+ else if (key.name === "return") props.onSelect(props.options[i].value);
940
+ else if (key.name === "escape" && props.onBack) props.onBack();
941
+ });
942
+ return (
943
+ <box style={{ flexDirection: "column", paddingLeft: 2 }}>
944
+ {props.options.map((o, idx) => (
945
+ <text key={o.value}>
946
+ <span fg={idx === i ? theme.primary : theme.muted}>{idx === i ? "❯ " : " "}</span>
947
+ <span fg={idx === i ? theme.text : theme.subtext}>{o.name}</span>
948
+ {o.hint && <span fg={theme.muted}> · {o.hint}</span>}
949
+ </text>
950
+ ))}
951
+ <box style={{ marginTop: 1 }}>
952
+ <text>
953
+ <span fg={theme.muted}>↑↓ navigate ↵ select{props.onBack ? " esc back" : ""}</span>
954
+ </text>
955
+ </box>
956
+ </box>
957
+ );
958
+ }
959
+
960
+ function MultiSelectPrompt(props: {
961
+ options: { name: string; value: string; hint?: string }[];
962
+ selected: string[];
963
+ onSubmit: (v: string[]) => void;
964
+ onBack?: () => void;
965
+ }) {
966
+ const [i, setI] = useState(0);
967
+ const [sel, setSel] = useState<string[]>(props.selected);
968
+ useKeyboard((key) => {
969
+ if (key.name === "up" || key.name === "k") setI((x: number) => Math.max(0, x - 1));
970
+ else if (key.name === "down" || key.name === "j")
971
+ setI((x: number) => Math.min(props.options.length - 1, x + 1));
972
+ else if (key.name === "space") {
973
+ const v = props.options[i].value;
974
+ setSel((s: string[]) => (s.includes(v) ? s.filter((x: string) => x !== v) : [...s, v]));
975
+ } else if (key.name === "return") props.onSubmit(sel);
976
+ else if (key.name === "escape" && props.onBack) props.onBack();
977
+ });
978
+ return (
979
+ <box style={{ flexDirection: "column", paddingLeft: 2 }}>
980
+ {props.options.map((o, idx) => (
981
+ <text key={o.value}>
982
+ <span fg={idx === i ? theme.primary : theme.muted}>{idx === i ? "❯ " : " "}</span>
983
+ <span fg={sel.includes(o.value) ? theme.success : theme.muted}>
984
+ {sel.includes(o.value) ? "◉ " : "○ "}
985
+ </span>
986
+ <span fg={idx === i ? theme.text : theme.subtext}>{o.name}</span>
987
+ {o.hint && <span fg={theme.muted}> · {o.hint}</span>}
988
+ </text>
989
+ ))}
990
+ {sel.length > 0 && (
991
+ <text>
992
+ <span fg={theme.success}>Selected: {sel.join(", ")}</span>
993
+ </text>
994
+ )}
995
+ <box style={{ marginTop: 1 }}>
996
+ <text>
997
+ <span fg={theme.muted}>
998
+ ↑↓ navigate space toggle ↵ confirm{props.onBack ? " esc back" : ""}
999
+ </span>
1000
+ </text>
1001
+ </box>
1002
+ </box>
1003
+ );
1004
+ }
1005
+
1006
+ function ConfirmPrompt(props: { onSubmit: (v: boolean) => void; onBack?: () => void }) {
1007
+ const [yes, setYes] = useState(true);
1008
+ useKeyboard((key) => {
1009
+ if (key.name === "left" || key.name === "right" || key.name === "h" || key.name === "l")
1010
+ setYes((v: boolean) => !v);
1011
+ else if (key.name === "return") props.onSubmit(yes);
1012
+ else if (key.name === "escape" && props.onBack) props.onBack();
1013
+ });
1014
+ return (
1015
+ <box style={{ flexDirection: "column", paddingLeft: 2 }}>
1016
+ <box style={{ flexDirection: "row" }}>
1017
+ <text>
1018
+ <span fg={yes ? theme.success : theme.muted}>{yes ? "● " : "○ "}</span>
1019
+ <span fg={yes ? theme.text : theme.subtext}>Yes</span>
1020
+ </text>
1021
+ <text>
1022
+ <span fg={theme.muted}> / </span>
1023
+ </text>
1024
+ <text>
1025
+ <span fg={!yes ? theme.error : theme.muted}>{!yes ? "● " : "○ "}</span>
1026
+ <span fg={!yes ? theme.text : theme.subtext}>No</span>
1027
+ </text>
1028
+ </box>
1029
+ <box style={{ marginTop: 1 }}>
1030
+ <text>
1031
+ <span fg={theme.muted}>←→ toggle ↵ confirm{props.onBack ? " esc back" : ""}</span>
1032
+ </text>
1033
+ </box>
1034
+ </box>
1035
+ );
1036
+ }
1037
+
1038
+ function ConfirmStep(props: { config: any; onComplete: () => void; onBack: () => void }) {
1039
+ useKeyboard((key) => {
1040
+ if (key.name === "return") props.onComplete();
1041
+ else if (key.name === "escape") props.onBack();
1042
+ });
1043
+ return (
1044
+ <box style={{ flexDirection: "column", marginTop: 1 }}>
1045
+ <text>
1046
+ <span fg={theme.success}>◆</span>
1047
+ <span fg={theme.text}> Ready to create {props.config.projectName}</span>
1048
+ </text>
1049
+ <box style={{ marginTop: 1, paddingLeft: 2 }}>
1050
+ <text>
1051
+ <span fg={theme.primary}>❯ </span>
1052
+ <span fg={theme.text}>Press Enter to create project</span>
1053
+ </text>
1054
+ </box>
1055
+ <box style={{ marginTop: 1, paddingLeft: 2 }}>
1056
+ <text>
1057
+ <span fg={theme.muted}>↵ create esc back</span>
1058
+ </text>
1059
+ </box>
1060
+ </box>
1061
+ );
1062
+ }