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.
Files changed (182) hide show
  1. package/README.md +2 -1
  2. package/bin/create-better-t-stack +98 -0
  3. package/package.json +69 -59
  4. package/src/api.ts +203 -0
  5. package/src/cli.ts +185 -0
  6. package/src/constants.ts +270 -0
  7. package/src/helpers/addons/addons-setup.ts +201 -0
  8. package/src/helpers/addons/examples-setup.ts +137 -0
  9. package/src/helpers/addons/fumadocs-setup.ts +99 -0
  10. package/src/helpers/addons/oxlint-setup.ts +36 -0
  11. package/src/helpers/addons/ruler-setup.ts +135 -0
  12. package/src/helpers/addons/starlight-setup.ts +45 -0
  13. package/src/helpers/addons/tauri-setup.ts +90 -0
  14. package/src/helpers/addons/tui-setup.ts +64 -0
  15. package/src/helpers/addons/ultracite-setup.ts +228 -0
  16. package/src/helpers/addons/vite-pwa-setup.ts +59 -0
  17. package/src/helpers/addons/wxt-setup.ts +86 -0
  18. package/src/helpers/core/add-addons.ts +85 -0
  19. package/src/helpers/core/add-deployment.ts +102 -0
  20. package/src/helpers/core/api-setup.ts +280 -0
  21. package/src/helpers/core/auth-setup.ts +203 -0
  22. package/src/helpers/core/backend-setup.ts +69 -0
  23. package/src/helpers/core/command-handlers.ts +354 -0
  24. package/src/helpers/core/convex-codegen.ts +14 -0
  25. package/src/helpers/core/create-project.ts +134 -0
  26. package/src/helpers/core/create-readme.ts +694 -0
  27. package/src/helpers/core/db-setup.ts +184 -0
  28. package/src/helpers/core/detect-project-config.ts +41 -0
  29. package/src/helpers/core/env-setup.ts +481 -0
  30. package/src/helpers/core/git.ts +23 -0
  31. package/src/helpers/core/install-dependencies.ts +29 -0
  32. package/src/helpers/core/payments-setup.ts +48 -0
  33. package/src/helpers/core/post-installation.ts +403 -0
  34. package/src/helpers/core/project-config.ts +250 -0
  35. package/src/helpers/core/runtime-setup.ts +76 -0
  36. package/src/helpers/core/template-manager.ts +917 -0
  37. package/src/helpers/core/workspace-setup.ts +184 -0
  38. package/src/helpers/database-providers/d1-setup.ts +28 -0
  39. package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
  40. package/src/helpers/database-providers/mongodb-atlas-setup.ts +182 -0
  41. package/src/helpers/database-providers/neon-setup.ts +240 -0
  42. package/src/helpers/database-providers/planetscale-setup.ts +78 -0
  43. package/src/helpers/database-providers/prisma-postgres-setup.ts +193 -0
  44. package/src/helpers/database-providers/supabase-setup.ts +196 -0
  45. package/src/helpers/database-providers/turso-setup.ts +309 -0
  46. package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
  47. package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +52 -0
  48. package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +105 -0
  49. package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +33 -0
  50. package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +33 -0
  51. package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +99 -0
  52. package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +34 -0
  53. package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +99 -0
  54. package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
  55. package/src/helpers/deployment/alchemy/index.ts +7 -0
  56. package/src/helpers/deployment/server-deploy-setup.ts +55 -0
  57. package/src/helpers/deployment/web-deploy-setup.ts +58 -0
  58. package/src/index.ts +51 -0
  59. package/src/prompts/addons.ts +200 -0
  60. package/src/prompts/api.ts +49 -0
  61. package/src/prompts/auth.ts +84 -0
  62. package/src/prompts/backend.ts +83 -0
  63. package/src/prompts/config-prompts.ts +138 -0
  64. package/src/prompts/database-setup.ts +112 -0
  65. package/src/prompts/database.ts +57 -0
  66. package/src/prompts/examples.ts +60 -0
  67. package/src/prompts/frontend.ts +118 -0
  68. package/src/prompts/git.ts +16 -0
  69. package/src/prompts/install.ts +16 -0
  70. package/src/prompts/orm.ts +53 -0
  71. package/src/prompts/package-manager.ts +32 -0
  72. package/src/prompts/payments.ts +50 -0
  73. package/src/prompts/project-name.ts +86 -0
  74. package/src/prompts/runtime.ts +47 -0
  75. package/src/prompts/server-deploy.ts +91 -0
  76. package/src/prompts/web-deploy.ts +107 -0
  77. package/src/tui/app.tsx +1062 -0
  78. package/src/types.ts +70 -0
  79. package/src/utils/add-package-deps.ts +57 -0
  80. package/src/utils/analytics.ts +39 -0
  81. package/src/utils/better-auth-plugin-setup.ts +71 -0
  82. package/src/utils/bts-config.ts +122 -0
  83. package/src/utils/command-exists.ts +16 -0
  84. package/src/utils/compatibility-rules.ts +337 -0
  85. package/src/utils/compatibility.ts +11 -0
  86. package/src/utils/config-processing.ts +130 -0
  87. package/src/utils/config-validation.ts +470 -0
  88. package/src/utils/display-config.ts +96 -0
  89. package/src/utils/docker-utils.ts +70 -0
  90. package/src/utils/errors.ts +30 -0
  91. package/src/utils/file-formatter.ts +11 -0
  92. package/src/utils/generate-reproducible-command.ts +53 -0
  93. package/src/utils/get-latest-cli-version.ts +27 -0
  94. package/src/utils/get-package-manager.ts +13 -0
  95. package/src/utils/open-url.ts +18 -0
  96. package/src/utils/package-runner.ts +23 -0
  97. package/src/utils/project-directory.ts +102 -0
  98. package/src/utils/project-name-validation.ts +43 -0
  99. package/src/utils/render-title.ts +48 -0
  100. package/src/utils/setup-catalogs.ts +192 -0
  101. package/src/utils/sponsors.ts +101 -0
  102. package/src/utils/telemetry.ts +19 -0
  103. package/src/utils/template-processor.ts +64 -0
  104. package/src/utils/templates.ts +94 -0
  105. package/src/utils/ts-morph.ts +26 -0
  106. package/src/validation.ts +117 -0
  107. package/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs +5 -7
  108. package/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +17 -17
  109. package/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +4 -4
  110. package/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs +2 -2
  111. package/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs +10 -10
  112. package/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs +13 -5
  113. package/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs +14 -12
  114. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs +13 -16
  115. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs +11 -5
  116. package/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs +4 -4
  117. package/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs +1 -1
  118. package/templates/auth/better-auth/web/react/next/src/components/user-menu.tsx.hbs +17 -15
  119. package/templates/auth/better-auth/web/react/react-router/src/components/user-menu.tsx.hbs +16 -15
  120. package/templates/auth/better-auth/web/react/tanstack-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  121. package/templates/auth/better-auth/web/react/tanstack-start/src/components/{user-menu.tsx → user-menu.tsx.hbs} +16 -15
  122. package/templates/backend/convex/packages/backend/convex/README.md +4 -4
  123. package/templates/backend/convex/packages/backend/convex/convex.config.ts.hbs +17 -0
  124. package/templates/backend/convex/packages/backend/convex/tsconfig.json.hbs +1 -1
  125. package/templates/examples/ai/convex/packages/backend/convex/agent.ts.hbs +9 -0
  126. package/templates/examples/ai/convex/packages/backend/convex/chat.ts.hbs +67 -0
  127. package/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs +301 -3
  128. package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +296 -10
  129. package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +180 -1
  130. package/templates/examples/ai/web/react/next/src/app/ai/page.tsx.hbs +172 -9
  131. package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx.hbs +156 -6
  132. package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx.hbs +156 -4
  133. package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +159 -6
  134. package/templates/frontend/react/next/package.json.hbs +8 -7
  135. package/templates/frontend/react/next/src/app/layout.tsx.hbs +28 -1
  136. package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +4 -6
  137. package/templates/frontend/react/next/src/components/providers.tsx.hbs +14 -4
  138. package/templates/frontend/react/react-router/package.json.hbs +2 -1
  139. package/templates/frontend/react/{tanstack-router/src/components/mode-toggle.tsx → react-router/src/components/mode-toggle.tsx.hbs} +4 -6
  140. package/templates/frontend/react/tanstack-router/package.json.hbs +2 -1
  141. package/templates/frontend/react/{react-router/src/components/mode-toggle.tsx → tanstack-router/src/components/mode-toggle.tsx.hbs} +4 -6
  142. package/templates/frontend/react/tanstack-start/package.json.hbs +2 -1
  143. package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +6 -0
  144. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +13 -14
  145. package/templates/frontend/react/tanstack-start/vite.config.ts.hbs +5 -0
  146. package/templates/frontend/react/web-base/components.json +5 -2
  147. package/templates/frontend/react/web-base/src/components/ui/button.tsx.hbs +57 -0
  148. package/templates/frontend/react/web-base/src/components/ui/card.tsx.hbs +103 -0
  149. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx.hbs +26 -0
  150. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx.hbs +262 -0
  151. package/templates/frontend/react/web-base/src/components/ui/input.tsx.hbs +20 -0
  152. package/templates/frontend/react/web-base/src/components/ui/label.tsx.hbs +20 -0
  153. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +13 -0
  154. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx.hbs +44 -0
  155. package/templates/frontend/react/web-base/src/index.css.hbs +58 -64
  156. package/dist/cli.d.mts +0 -1
  157. package/dist/cli.mjs +0 -8
  158. package/dist/index.d.mts +0 -347
  159. package/dist/index.mjs +0 -4
  160. package/dist/src-DLvUK0Qf.mjs +0 -7069
  161. package/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs +0 -7
  162. package/templates/examples/ai/web/react/base/src/components/response.tsx.hbs +0 -22
  163. package/templates/frontend/react/web-base/src/components/ui/button.tsx +0 -56
  164. package/templates/frontend/react/web-base/src/components/ui/card.tsx +0 -75
  165. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +0 -27
  166. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +0 -228
  167. package/templates/frontend/react/web-base/src/components/ui/input.tsx +0 -21
  168. package/templates/frontend/react/web-base/src/components/ui/label.tsx +0 -19
  169. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +0 -13
  170. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +0 -25
  171. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  172. /package/templates/auth/better-auth/web/react/tanstack-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  173. /package/templates/auth/better-auth/web/react/tanstack-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  174. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  175. /package/templates/auth/better-auth/web/react/tanstack-start/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  176. /package/templates/auth/better-auth/web/react/tanstack-start/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  177. /package/templates/auth/better-auth/web/solid/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  178. /package/templates/auth/better-auth/web/solid/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  179. /package/templates/auth/better-auth/web/solid/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  180. /package/templates/frontend/react/react-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  181. /package/templates/frontend/react/tanstack-router/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  182. /package/templates/frontend/react/web-base/src/lib/{utils.ts → utils.ts.hbs} +0 -0
@@ -0,0 +1,203 @@
1
+ import path from "node:path";
2
+ import consola from "consola";
3
+ import fs from "fs-extra";
4
+ import pc from "picocolors";
5
+ import type { ProjectConfig } from "../../types";
6
+ import { addPackageDependency } from "../../utils/add-package-deps";
7
+ import { setupBetterAuthPlugins } from "../../utils/better-auth-plugin-setup";
8
+
9
+ export async function setupAuth(config: ProjectConfig) {
10
+ const { auth, frontend, backend, projectDir } = config;
11
+ if (!auth || auth === "none") {
12
+ return;
13
+ }
14
+
15
+ const serverDir = path.join(projectDir, "apps/server");
16
+ const clientDir = path.join(projectDir, "apps/web");
17
+ const nativeDir = path.join(projectDir, "apps/native");
18
+
19
+ const clientDirExists = await fs.pathExists(clientDir);
20
+ const nativeDirExists = await fs.pathExists(nativeDir);
21
+ const _serverDirExists = await fs.pathExists(serverDir);
22
+
23
+ try {
24
+ if (backend === "convex") {
25
+ if (auth === "clerk" && clientDirExists) {
26
+ const hasNextJs = frontend.includes("next");
27
+ const hasTanStackStart = frontend.includes("tanstack-start");
28
+ const hasViteReactOther = frontend.some((f) =>
29
+ ["tanstack-router", "react-router"].includes(f),
30
+ );
31
+
32
+ if (hasNextJs) {
33
+ await addPackageDependency({
34
+ dependencies: ["@clerk/nextjs"],
35
+ projectDir: clientDir,
36
+ });
37
+ } else if (hasTanStackStart) {
38
+ await addPackageDependency({
39
+ dependencies: ["@clerk/tanstack-react-start", "srvx"],
40
+ projectDir: clientDir,
41
+ });
42
+ } else if (hasViteReactOther) {
43
+ await addPackageDependency({
44
+ dependencies: ["@clerk/clerk-react"],
45
+ projectDir: clientDir,
46
+ });
47
+ }
48
+ }
49
+
50
+ if (auth === "better-auth") {
51
+ const convexBackendDir = path.join(projectDir, "packages/backend");
52
+ const convexBackendDirExists = await fs.pathExists(convexBackendDir);
53
+
54
+ const hasNativeForBA =
55
+ frontend.includes("native-bare") ||
56
+ frontend.includes("native-uniwind") ||
57
+ frontend.includes("native-unistyles");
58
+
59
+ if (convexBackendDirExists) {
60
+ await addPackageDependency({
61
+ dependencies: ["better-auth", "@convex-dev/better-auth"],
62
+ customDependencies: { "better-auth": "1.4.7" },
63
+ projectDir: convexBackendDir,
64
+ });
65
+ if (hasNativeForBA) {
66
+ await addPackageDependency({
67
+ dependencies: ["@better-auth/expo"],
68
+ customDependencies: { "@better-auth/expo": "1.4.7" },
69
+ projectDir: convexBackendDir,
70
+ });
71
+ }
72
+ }
73
+
74
+ if (clientDirExists) {
75
+ const hasNextJs = frontend.includes("next");
76
+ const hasTanStackStart = frontend.includes("tanstack-start");
77
+ const hasViteReactOther = frontend.some((f) =>
78
+ ["tanstack-router", "react-router"].includes(f),
79
+ );
80
+
81
+ if (hasNextJs) {
82
+ await addPackageDependency({
83
+ dependencies: ["better-auth", "@convex-dev/better-auth"],
84
+ customDependencies: { "better-auth": "1.4.7" },
85
+ projectDir: clientDir,
86
+ });
87
+ } else if (hasTanStackStart) {
88
+ await addPackageDependency({
89
+ dependencies: ["better-auth", "@convex-dev/better-auth"],
90
+ customDependencies: { "better-auth": "1.4.7" },
91
+ projectDir: clientDir,
92
+ });
93
+ } else if (hasViteReactOther) {
94
+ await addPackageDependency({
95
+ dependencies: ["better-auth", "@convex-dev/better-auth"],
96
+ customDependencies: { "better-auth": "1.4.7" },
97
+ projectDir: clientDir,
98
+ });
99
+ }
100
+ }
101
+
102
+ const hasNativeBare = frontend.includes("native-bare");
103
+ const hasNativeUniwind = frontend.includes("native-uniwind");
104
+ const hasUnistyles = frontend.includes("native-unistyles");
105
+ if (nativeDirExists && (hasNativeBare || hasNativeUniwind || hasUnistyles)) {
106
+ await addPackageDependency({
107
+ dependencies: ["better-auth", "@better-auth/expo", "@convex-dev/better-auth"],
108
+ customDependencies: {
109
+ "better-auth": "1.4.7",
110
+ "@better-auth/expo": "1.4.7",
111
+ },
112
+ projectDir: nativeDir,
113
+ });
114
+ }
115
+ }
116
+
117
+ const hasNativeBare = frontend.includes("native-bare");
118
+ const hasNativeUniwind = frontend.includes("native-uniwind");
119
+ const hasUnistyles = frontend.includes("native-unistyles");
120
+ if (
121
+ auth === "clerk" &&
122
+ nativeDirExists &&
123
+ (hasNativeBare || hasNativeUniwind || hasUnistyles)
124
+ ) {
125
+ await addPackageDependency({
126
+ dependencies: ["@clerk/clerk-expo"],
127
+ projectDir: nativeDir,
128
+ });
129
+ }
130
+ return;
131
+ }
132
+
133
+ const authPackageDir = path.join(projectDir, "packages/auth");
134
+ const authPackageDirExists = await fs.pathExists(authPackageDir);
135
+
136
+ if (authPackageDirExists && auth === "better-auth") {
137
+ await addPackageDependency({
138
+ dependencies: ["better-auth"],
139
+ projectDir: authPackageDir,
140
+ });
141
+ }
142
+
143
+ const hasWebFrontend = frontend.some((f) =>
144
+ [
145
+ "react-router",
146
+ "tanstack-router",
147
+ "tanstack-start",
148
+ "next",
149
+ "nuxt",
150
+ "svelte",
151
+ "solid",
152
+ ].includes(f),
153
+ );
154
+
155
+ if (hasWebFrontend && clientDirExists) {
156
+ if (auth === "better-auth") {
157
+ await addPackageDependency({
158
+ dependencies: ["better-auth"],
159
+ projectDir: clientDir,
160
+ });
161
+ }
162
+ }
163
+
164
+ if (
165
+ (frontend.includes("native-bare") ||
166
+ frontend.includes("native-uniwind") ||
167
+ frontend.includes("native-unistyles")) &&
168
+ nativeDirExists
169
+ ) {
170
+ if (auth === "better-auth") {
171
+ await addPackageDependency({
172
+ dependencies: ["better-auth", "@better-auth/expo"],
173
+ projectDir: nativeDir,
174
+ });
175
+ if (authPackageDirExists) {
176
+ await addPackageDependency({
177
+ dependencies: ["@better-auth/expo"],
178
+ projectDir: authPackageDir,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ if (authPackageDirExists && auth === "better-auth") {
185
+ await setupBetterAuthPlugins(projectDir, config);
186
+ }
187
+ } catch (error) {
188
+ consola.error(pc.red("Failed to configure authentication dependencies"));
189
+ if (error instanceof Error) {
190
+ consola.error(pc.red(error.message));
191
+ }
192
+ }
193
+ }
194
+
195
+ export function generateAuthSecret(length = 32) {
196
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
197
+ let result = "";
198
+ const charactersLength = characters.length;
199
+ for (let i = 0; i < length; i++) {
200
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
201
+ }
202
+ return result;
203
+ }
@@ -0,0 +1,69 @@
1
+ import path from "node:path";
2
+ import type { AvailableDependencies } from "../../constants";
3
+ import type { ProjectConfig } from "../../types";
4
+ import { addPackageDependency } from "../../utils/add-package-deps";
5
+
6
+ export async function setupBackendDependencies(config: ProjectConfig) {
7
+ const { backend, runtime, api, auth, projectDir } = config;
8
+
9
+ if (backend === "convex") {
10
+ const convexBackendDir = path.join(projectDir, "packages/backend");
11
+ await addPackageDependency({
12
+ dependencies: ["convex"],
13
+ projectDir: convexBackendDir,
14
+ });
15
+ return;
16
+ }
17
+
18
+ const framework = backend;
19
+ const serverDir = path.join(projectDir, "apps/server");
20
+
21
+ const dependencies: AvailableDependencies[] = [];
22
+ const devDependencies: AvailableDependencies[] = [];
23
+
24
+ if (framework === "hono") {
25
+ dependencies.push("hono");
26
+ if (runtime === "node") {
27
+ dependencies.push("@hono/node-server");
28
+ }
29
+ } else if (framework === "elysia") {
30
+ dependencies.push("elysia", "@elysiajs/cors");
31
+ if (runtime === "node") {
32
+ dependencies.push("@elysiajs/node");
33
+ }
34
+ } else if (framework === "express") {
35
+ dependencies.push("express", "cors");
36
+ devDependencies.push("@types/express", "@types/cors");
37
+ } else if (framework === "fastify") {
38
+ dependencies.push("fastify", "@fastify/cors");
39
+ }
40
+
41
+ if (api === "trpc") {
42
+ dependencies.push("@trpc/server");
43
+ if (framework === "hono") {
44
+ dependencies.push("@hono/trpc-server");
45
+ } else if (framework === "elysia") {
46
+ dependencies.push("@elysiajs/trpc");
47
+ }
48
+ } else if (api === "orpc") {
49
+ dependencies.push("@orpc/server", "@orpc/openapi", "@orpc/zod");
50
+ }
51
+
52
+ if (auth === "better-auth") {
53
+ dependencies.push("better-auth");
54
+ }
55
+
56
+ if (runtime === "node") {
57
+ devDependencies.push("tsx", "@types/node");
58
+ } else if (runtime === "bun") {
59
+ devDependencies.push("@types/bun");
60
+ }
61
+
62
+ if (dependencies.length > 0 || devDependencies.length > 0) {
63
+ await addPackageDependency({
64
+ dependencies,
65
+ devDependencies,
66
+ projectDir: serverDir,
67
+ });
68
+ }
69
+ }
@@ -0,0 +1,354 @@
1
+ import path from "node:path";
2
+ import { intro, log, outro } from "@clack/prompts";
3
+ import consola from "consola";
4
+ import fs from "fs-extra";
5
+ import pc from "picocolors";
6
+ import { getDefaultConfig } from "../../constants";
7
+ import { getAddonsToAdd } from "../../prompts/addons";
8
+ import { gatherConfig } from "../../prompts/config-prompts";
9
+ import { getProjectName } from "../../prompts/project-name";
10
+ import { getServerDeploymentToAdd } from "../../prompts/server-deploy";
11
+ import { getDeploymentToAdd } from "../../prompts/web-deploy";
12
+ import type { AddInput, CreateInput, DirectoryConflict, ProjectConfig } from "../../types";
13
+ import { trackProjectCreation } from "../../utils/analytics";
14
+
15
+ import { displayConfig } from "../../utils/display-config";
16
+ import { exitWithError, handleError } from "../../utils/errors";
17
+ import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
18
+ import { handleDirectoryConflict, setupProjectDirectory } from "../../utils/project-directory";
19
+ import { renderTitle } from "../../utils/render-title";
20
+ import { getTemplateConfig, getTemplateDescription } from "../../utils/templates";
21
+ import {
22
+ getProvidedFlags,
23
+ processAndValidateFlags,
24
+ processProvidedFlagsWithoutValidation,
25
+ validateConfigCompatibility,
26
+ } from "../../validation";
27
+ import { addAddonsToProject } from "./add-addons";
28
+ import { addDeploymentToProject } from "./add-deployment";
29
+ import { createProject } from "./create-project";
30
+ import { detectProjectConfig } from "./detect-project-config";
31
+ import { installDependencies } from "./install-dependencies";
32
+
33
+ export async function createProjectHandler(input: CreateInput & { projectName?: string }) {
34
+ const startTime = Date.now();
35
+ const timeScaffolded = new Date().toISOString();
36
+
37
+ if (input.renderTitle !== false) {
38
+ renderTitle();
39
+ }
40
+ intro(pc.magenta("Creating a new Better-T-Stack project"));
41
+
42
+ if (input.yolo) {
43
+ consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
44
+ }
45
+
46
+ let currentPathInput: string;
47
+ if (input.yes && input.projectName) {
48
+ currentPathInput = input.projectName;
49
+ } else if (input.yes) {
50
+ const defaultConfig = getDefaultConfig();
51
+ let defaultName: string = defaultConfig.relativePath;
52
+ let counter = 1;
53
+ while (
54
+ (await fs.pathExists(path.resolve(process.cwd(), defaultName))) &&
55
+ (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0
56
+ ) {
57
+ defaultName = `${defaultConfig.projectName}-${counter}`;
58
+ counter++;
59
+ }
60
+ currentPathInput = defaultName;
61
+ } else {
62
+ currentPathInput = await getProjectName(input.projectName);
63
+ }
64
+
65
+ let finalPathInput: string;
66
+ let shouldClearDirectory: boolean;
67
+
68
+ try {
69
+ if (input.directoryConflict) {
70
+ const result = await handleDirectoryConflictProgrammatically(
71
+ currentPathInput,
72
+ input.directoryConflict,
73
+ );
74
+ finalPathInput = result.finalPathInput;
75
+ shouldClearDirectory = result.shouldClearDirectory;
76
+ } else {
77
+ const result = await handleDirectoryConflict(currentPathInput);
78
+ finalPathInput = result.finalPathInput;
79
+ shouldClearDirectory = result.shouldClearDirectory;
80
+ }
81
+ } catch (error) {
82
+ const elapsedTimeMs = Date.now() - startTime;
83
+ return {
84
+ success: false,
85
+ projectConfig: {
86
+ projectName: "",
87
+ projectDir: "",
88
+ relativePath: "",
89
+ database: "none",
90
+ orm: "none",
91
+ backend: "none",
92
+ runtime: "none",
93
+ frontend: [],
94
+ addons: [],
95
+ examples: [],
96
+ auth: "none",
97
+ payments: "none",
98
+ git: false,
99
+ packageManager: "npm",
100
+ install: false,
101
+ dbSetup: "none",
102
+ api: "none",
103
+ webDeploy: "none",
104
+ serverDeploy: "none",
105
+ } satisfies ProjectConfig,
106
+ reproducibleCommand: "",
107
+ timeScaffolded,
108
+ elapsedTimeMs,
109
+ projectDirectory: "",
110
+ relativePath: "",
111
+ error: error instanceof Error ? error.message : String(error),
112
+ };
113
+ }
114
+
115
+ const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
116
+ finalPathInput,
117
+ shouldClearDirectory,
118
+ );
119
+
120
+ const originalInput = {
121
+ ...input,
122
+ projectDirectory: input.projectName,
123
+ };
124
+
125
+ const providedFlags = getProvidedFlags(originalInput);
126
+
127
+ let cliInput = originalInput;
128
+
129
+ if (input.template && input.template !== "none") {
130
+ const templateConfig = getTemplateConfig(input.template);
131
+ if (templateConfig) {
132
+ const templateName = input.template.toUpperCase();
133
+ const templateDescription = getTemplateDescription(input.template);
134
+ log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
135
+ log.message(pc.dim(` ${templateDescription}`));
136
+ const userOverrides: Record<string, unknown> = {};
137
+ for (const [key, value] of Object.entries(originalInput)) {
138
+ if (value !== undefined) {
139
+ userOverrides[key] = value;
140
+ }
141
+ }
142
+ cliInput = {
143
+ ...templateConfig,
144
+ ...userOverrides,
145
+ template: input.template,
146
+ projectDirectory: originalInput.projectDirectory,
147
+ };
148
+ }
149
+ }
150
+
151
+ let config: ProjectConfig;
152
+ if (cliInput.yes) {
153
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
154
+
155
+ config = {
156
+ ...getDefaultConfig(),
157
+ ...flagConfig,
158
+ projectName: finalBaseName,
159
+ projectDir: finalResolvedPath,
160
+ relativePath: finalPathInput,
161
+ };
162
+
163
+ validateConfigCompatibility(config, providedFlags, cliInput);
164
+
165
+ log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
166
+ log.message(displayConfig(config));
167
+ } else {
168
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
169
+ const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
170
+
171
+ if (Object.keys(otherFlags).length > 0) {
172
+ log.info(pc.yellow("Using these pre-selected options:"));
173
+ log.message(displayConfig(otherFlags));
174
+ log.message("");
175
+ }
176
+
177
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
178
+ }
179
+
180
+ await createProject(config, {
181
+ manualDb: cliInput.manualDb ?? input.manualDb,
182
+ });
183
+
184
+ const reproducibleCommand = generateReproducibleCommand(config);
185
+ log.success(
186
+ pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`),
187
+ );
188
+
189
+ await trackProjectCreation(config, input.disableAnalytics);
190
+
191
+ const elapsedTimeMs = Date.now() - startTime;
192
+ const elapsedTimeInSeconds = (elapsedTimeMs / 1000).toFixed(2);
193
+ outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
194
+
195
+ return {
196
+ success: true,
197
+ projectConfig: config,
198
+ reproducibleCommand,
199
+ timeScaffolded,
200
+ elapsedTimeMs,
201
+ projectDirectory: config.projectDir,
202
+ relativePath: config.relativePath,
203
+ };
204
+ }
205
+
206
+ async function handleDirectoryConflictProgrammatically(
207
+ currentPathInput: string,
208
+ strategy: DirectoryConflict,
209
+ ) {
210
+ const currentPath = path.resolve(process.cwd(), currentPathInput);
211
+
212
+ if (!(await fs.pathExists(currentPath))) {
213
+ return { finalPathInput: currentPathInput, shouldClearDirectory: false };
214
+ }
215
+
216
+ const dirContents = await fs.readdir(currentPath);
217
+ const isNotEmpty = dirContents.length > 0;
218
+
219
+ if (!isNotEmpty) {
220
+ return { finalPathInput: currentPathInput, shouldClearDirectory: false };
221
+ }
222
+
223
+ switch (strategy) {
224
+ case "overwrite":
225
+ return { finalPathInput: currentPathInput, shouldClearDirectory: true };
226
+
227
+ case "merge":
228
+ return { finalPathInput: currentPathInput, shouldClearDirectory: false };
229
+
230
+ case "increment": {
231
+ let counter = 1;
232
+ const baseName = currentPathInput;
233
+ let finalPathInput = `${baseName}-${counter}`;
234
+
235
+ while (
236
+ (await fs.pathExists(path.resolve(process.cwd(), finalPathInput))) &&
237
+ (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0
238
+ ) {
239
+ counter++;
240
+ finalPathInput = `${baseName}-${counter}`;
241
+ }
242
+
243
+ return { finalPathInput, shouldClearDirectory: false };
244
+ }
245
+
246
+ case "error":
247
+ throw new Error(
248
+ `Directory "${currentPathInput}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`,
249
+ );
250
+
251
+ default:
252
+ throw new Error(`Unknown directory conflict strategy: ${strategy}`);
253
+ }
254
+ }
255
+
256
+ export async function addAddonsHandler(input: AddInput) {
257
+ try {
258
+ const projectDir = input.projectDir || process.cwd();
259
+ const detectedConfig = await detectProjectConfig(projectDir);
260
+
261
+ if (!detectedConfig) {
262
+ exitWithError(
263
+ "Could not detect project configuration. Please ensure this is a valid Better-T-Stack project.",
264
+ );
265
+ }
266
+
267
+ if (!input.addons || input.addons.length === 0) {
268
+ const addonsPrompt = await getAddonsToAdd(
269
+ detectedConfig.frontend || [],
270
+ detectedConfig.addons || [],
271
+ detectedConfig.auth,
272
+ );
273
+
274
+ if (addonsPrompt.length > 0) {
275
+ input.addons = addonsPrompt;
276
+ }
277
+ }
278
+
279
+ if (!input.webDeploy) {
280
+ const deploymentPrompt = await getDeploymentToAdd(
281
+ detectedConfig.frontend || [],
282
+ detectedConfig.webDeploy,
283
+ );
284
+
285
+ if (deploymentPrompt !== "none") {
286
+ input.webDeploy = deploymentPrompt;
287
+ }
288
+ }
289
+
290
+ if (!input.serverDeploy) {
291
+ const serverDeploymentPrompt = await getServerDeploymentToAdd(
292
+ detectedConfig.runtime,
293
+ detectedConfig.serverDeploy,
294
+ detectedConfig.backend,
295
+ );
296
+
297
+ if (serverDeploymentPrompt !== "none") {
298
+ input.serverDeploy = serverDeploymentPrompt;
299
+ }
300
+ }
301
+
302
+ const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
303
+
304
+ let somethingAdded = false;
305
+
306
+ if (input.addons && input.addons.length > 0) {
307
+ await addAddonsToProject({
308
+ ...input,
309
+ install: false,
310
+ suppressInstallMessage: true,
311
+ addons: input.addons,
312
+ });
313
+ somethingAdded = true;
314
+ }
315
+
316
+ if (input.webDeploy && input.webDeploy !== "none") {
317
+ await addDeploymentToProject({
318
+ ...input,
319
+ install: false,
320
+ suppressInstallMessage: true,
321
+ webDeploy: input.webDeploy,
322
+ });
323
+ somethingAdded = true;
324
+ }
325
+
326
+ if (input.serverDeploy && input.serverDeploy !== "none") {
327
+ await addDeploymentToProject({
328
+ ...input,
329
+ install: false,
330
+ suppressInstallMessage: true,
331
+ serverDeploy: input.serverDeploy,
332
+ });
333
+ somethingAdded = true;
334
+ }
335
+
336
+ if (!somethingAdded) {
337
+ outro(pc.yellow("No addons or deployment configurations to add."));
338
+ return;
339
+ }
340
+
341
+ if (input.install) {
342
+ await installDependencies({
343
+ projectDir,
344
+ packageManager,
345
+ });
346
+ } else {
347
+ log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
348
+ }
349
+
350
+ outro("Add command completed successfully!");
351
+ } catch (error) {
352
+ handleError(error, "Failed to add addons or deployment");
353
+ }
354
+ }
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+ import { $ } from "bun";
3
+ import type { PackageManager } from "../../types";
4
+ import { getPackageExecutionCommand } from "../../utils/package-runner";
5
+
6
+ // having problems running this in convex + better-auth
7
+ export async function runConvexCodegen(
8
+ projectDir: string,
9
+ packageManager: PackageManager | null | undefined,
10
+ ) {
11
+ const backendDir = path.join(projectDir, "packages/backend");
12
+ const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
13
+ await $`${{ raw: cmd }}`.cwd(backendDir);
14
+ }