create-better-t-stack 3.9.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.
- package/README.md +2 -1
- package/bin/create-better-t-stack +98 -0
- package/package.json +69 -59
- package/src/api.ts +203 -0
- package/src/cli.ts +185 -0
- package/src/constants.ts +270 -0
- package/src/helpers/addons/addons-setup.ts +201 -0
- package/src/helpers/addons/examples-setup.ts +137 -0
- package/src/helpers/addons/fumadocs-setup.ts +99 -0
- package/src/helpers/addons/oxlint-setup.ts +36 -0
- package/src/helpers/addons/ruler-setup.ts +135 -0
- package/src/helpers/addons/starlight-setup.ts +45 -0
- package/src/helpers/addons/tauri-setup.ts +90 -0
- package/src/helpers/addons/tui-setup.ts +64 -0
- package/src/helpers/addons/ultracite-setup.ts +228 -0
- package/src/helpers/addons/vite-pwa-setup.ts +59 -0
- package/src/helpers/addons/wxt-setup.ts +86 -0
- package/src/helpers/core/add-addons.ts +85 -0
- package/src/helpers/core/add-deployment.ts +102 -0
- package/src/helpers/core/api-setup.ts +280 -0
- package/src/helpers/core/auth-setup.ts +203 -0
- package/src/helpers/core/backend-setup.ts +69 -0
- package/src/helpers/core/command-handlers.ts +354 -0
- package/src/helpers/core/convex-codegen.ts +14 -0
- package/src/helpers/core/create-project.ts +134 -0
- package/src/helpers/core/create-readme.ts +694 -0
- package/src/helpers/core/db-setup.ts +184 -0
- package/src/helpers/core/detect-project-config.ts +41 -0
- package/src/helpers/core/env-setup.ts +481 -0
- package/src/helpers/core/git.ts +23 -0
- package/src/helpers/core/install-dependencies.ts +29 -0
- package/src/helpers/core/payments-setup.ts +48 -0
- package/src/helpers/core/post-installation.ts +403 -0
- package/src/helpers/core/project-config.ts +250 -0
- package/src/helpers/core/runtime-setup.ts +76 -0
- package/src/helpers/core/template-manager.ts +917 -0
- package/src/helpers/core/workspace-setup.ts +184 -0
- package/src/helpers/database-providers/d1-setup.ts +28 -0
- package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
- package/src/helpers/database-providers/mongodb-atlas-setup.ts +182 -0
- package/src/helpers/database-providers/neon-setup.ts +240 -0
- package/src/helpers/database-providers/planetscale-setup.ts +78 -0
- package/src/helpers/database-providers/prisma-postgres-setup.ts +193 -0
- package/src/helpers/database-providers/supabase-setup.ts +196 -0
- package/src/helpers/database-providers/turso-setup.ts +309 -0
- package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
- package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +52 -0
- package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +105 -0
- package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +33 -0
- package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +33 -0
- package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +99 -0
- package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +34 -0
- package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +99 -0
- package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
- package/src/helpers/deployment/alchemy/index.ts +7 -0
- package/src/helpers/deployment/server-deploy-setup.ts +55 -0
- package/src/helpers/deployment/web-deploy-setup.ts +58 -0
- package/src/index.ts +51 -0
- package/src/prompts/addons.ts +200 -0
- package/src/prompts/api.ts +49 -0
- package/src/prompts/auth.ts +84 -0
- package/src/prompts/backend.ts +83 -0
- package/src/prompts/config-prompts.ts +138 -0
- package/src/prompts/database-setup.ts +112 -0
- package/src/prompts/database.ts +57 -0
- package/src/prompts/examples.ts +60 -0
- package/src/prompts/frontend.ts +118 -0
- package/src/prompts/git.ts +16 -0
- package/src/prompts/install.ts +16 -0
- package/src/prompts/orm.ts +53 -0
- package/src/prompts/package-manager.ts +32 -0
- package/src/prompts/payments.ts +50 -0
- package/src/prompts/project-name.ts +86 -0
- package/src/prompts/runtime.ts +47 -0
- package/src/prompts/server-deploy.ts +91 -0
- package/src/prompts/web-deploy.ts +107 -0
- package/src/tui/app.tsx +1062 -0
- package/src/types.ts +70 -0
- package/src/utils/add-package-deps.ts +57 -0
- package/src/utils/analytics.ts +39 -0
- package/src/utils/better-auth-plugin-setup.ts +71 -0
- package/src/utils/bts-config.ts +122 -0
- package/src/utils/command-exists.ts +16 -0
- package/src/utils/compatibility-rules.ts +337 -0
- package/src/utils/compatibility.ts +11 -0
- package/src/utils/config-processing.ts +130 -0
- package/src/utils/config-validation.ts +470 -0
- package/src/utils/display-config.ts +96 -0
- package/src/utils/docker-utils.ts +70 -0
- package/src/utils/errors.ts +30 -0
- package/src/utils/file-formatter.ts +11 -0
- package/src/utils/generate-reproducible-command.ts +53 -0
- package/src/utils/get-latest-cli-version.ts +27 -0
- package/src/utils/get-package-manager.ts +13 -0
- package/src/utils/open-url.ts +18 -0
- package/src/utils/package-runner.ts +23 -0
- package/src/utils/project-directory.ts +102 -0
- package/src/utils/project-name-validation.ts +43 -0
- package/src/utils/render-title.ts +48 -0
- package/src/utils/setup-catalogs.ts +192 -0
- package/src/utils/sponsors.ts +101 -0
- package/src/utils/telemetry.ts +19 -0
- package/src/utils/template-processor.ts +64 -0
- package/src/utils/templates.ts +94 -0
- package/src/utils/ts-morph.ts +26 -0
- package/src/validation.ts +117 -0
- package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
- package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
- package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
- package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
- package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
- package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
- package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
- package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
- package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
- package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
- package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
- package/templates/backend/convex/packages/backend/convex/README.md +4 -4
- package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
- package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
- package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
- package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
- package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
- package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
- package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
- package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
- package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
- package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
- package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
- package/templates/frontend/react/next/package.json.hbs +8 -7
- package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
- package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
- package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
- package/templates/frontend/react/react-router/package.json.hbs +2 -1
- package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
- package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
- package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
- package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
- package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
- package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
- package/templates/frontend/react/web-base/components.json +5 -2
- package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
- package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
- package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
- package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -8
- package/dist/index.d.mts +0 -347
- package/dist/index.mjs +0 -4
- package/dist/src-DLvUK0Qf.mjs +0 -7069
- package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
- package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
- package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
- package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
- package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
- package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
- package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
- package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
- package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
- /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
- /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
- /package/templates/frontend/react/web-base/src/lib/{utils.ts → utils.ts.hbs} +0 -0
package/src/tui/app.tsx
ADDED
|
@@ -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
|
+
}
|