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,99 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
4
+ import type { PackageManager } from "../../../types";
5
+ import { addPackageDependency } from "../../../utils/add-package-deps";
6
+
7
+ export async function setupSvelteAlchemyDeploy(
8
+ projectDir: string,
9
+ _packageManager: PackageManager,
10
+ options?: { skipAppScripts?: boolean },
11
+ ) {
12
+ const webAppDir = path.join(projectDir, "apps/web");
13
+ if (!(await fs.pathExists(webAppDir))) return;
14
+
15
+ await addPackageDependency({
16
+ devDependencies: ["alchemy", "@sveltejs/adapter-cloudflare"],
17
+ projectDir: webAppDir,
18
+ });
19
+
20
+ const pkgPath = path.join(webAppDir, "package.json");
21
+ if (await fs.pathExists(pkgPath)) {
22
+ const pkg = await fs.readJson(pkgPath);
23
+
24
+ if (!options?.skipAppScripts) {
25
+ pkg.scripts = {
26
+ ...pkg.scripts,
27
+ dev: "alchemy dev",
28
+ deploy: "alchemy deploy",
29
+ destroy: "alchemy destroy",
30
+ };
31
+ }
32
+
33
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
34
+ }
35
+
36
+ const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
37
+ if (!(await fs.pathExists(svelteConfigPath))) return;
38
+
39
+ try {
40
+ const project = new Project({
41
+ manipulationSettings: {
42
+ indentationText: IndentationText.TwoSpaces,
43
+ quoteKind: QuoteKind.Single,
44
+ },
45
+ });
46
+
47
+ project.addSourceFileAtPath(svelteConfigPath);
48
+ const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
49
+
50
+ const importDeclarations = sourceFile.getImportDeclarations();
51
+ const adapterImport = importDeclarations.find((imp) =>
52
+ imp.getModuleSpecifierValue().includes("@sveltejs/adapter"),
53
+ );
54
+
55
+ if (adapterImport) {
56
+ adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
57
+ adapterImport.removeDefaultImport();
58
+ adapterImport.setDefaultImport("alchemy");
59
+ } else {
60
+ sourceFile.insertImportDeclaration(0, {
61
+ moduleSpecifier: "alchemy/cloudflare/sveltekit",
62
+ defaultImport: "alchemy",
63
+ });
64
+ }
65
+
66
+ const configVariable = sourceFile.getVariableDeclaration("config");
67
+ if (configVariable) {
68
+ const initializer = configVariable.getInitializer();
69
+ if (Node.isObjectLiteralExpression(initializer)) {
70
+ updateAdapterInConfig(initializer);
71
+ }
72
+ }
73
+
74
+ await project.save();
75
+ } catch (error) {
76
+ console.warn("Failed to update svelte.config.js:", error);
77
+ }
78
+ }
79
+
80
+ function updateAdapterInConfig(configObject: Node) {
81
+ if (!Node.isObjectLiteralExpression(configObject)) return;
82
+
83
+ const kitProperty = configObject.getProperty("kit");
84
+ if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
85
+ const kitInitializer = kitProperty.getInitializer();
86
+ if (Node.isObjectLiteralExpression(kitInitializer)) {
87
+ const adapterProperty = kitInitializer.getProperty("adapter");
88
+ if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
89
+ const initializer = adapterProperty.getInitializer();
90
+ if (Node.isCallExpression(initializer)) {
91
+ const expression = initializer.getExpression();
92
+ if (Node.isIdentifier(expression) && expression.getText() === "adapter") {
93
+ expression.replaceWithText("alchemy");
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import type { PackageManager } from "../../../types";
4
+ import { addPackageDependency } from "../../../utils/add-package-deps";
5
+
6
+ export async function setupTanStackRouterAlchemyDeploy(
7
+ projectDir: string,
8
+ _packageManager: PackageManager,
9
+ options?: { skipAppScripts?: boolean },
10
+ ) {
11
+ const webAppDir = path.join(projectDir, "apps/web");
12
+ if (!(await fs.pathExists(webAppDir))) return;
13
+
14
+ await addPackageDependency({
15
+ devDependencies: ["alchemy"],
16
+ projectDir: webAppDir,
17
+ });
18
+
19
+ const pkgPath = path.join(webAppDir, "package.json");
20
+ if (await fs.pathExists(pkgPath)) {
21
+ const pkg = await fs.readJson(pkgPath);
22
+
23
+ if (!options?.skipAppScripts) {
24
+ pkg.scripts = {
25
+ ...pkg.scripts,
26
+ dev: "alchemy dev",
27
+ deploy: "alchemy deploy",
28
+ destroy: "alchemy destroy",
29
+ };
30
+ }
31
+
32
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
33
+ }
34
+ }
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
4
+ import type { PackageManager } from "../../../types";
5
+ import { addPackageDependency } from "../../../utils/add-package-deps";
6
+
7
+ export async function setupTanStackStartAlchemyDeploy(
8
+ projectDir: string,
9
+ _packageManager: PackageManager,
10
+ options?: { skipAppScripts?: boolean },
11
+ ) {
12
+ const webAppDir = path.join(projectDir, "apps/web");
13
+ if (!(await fs.pathExists(webAppDir))) return;
14
+
15
+ await addPackageDependency({
16
+ devDependencies: ["alchemy", "@cloudflare/vite-plugin"],
17
+ projectDir: webAppDir,
18
+ });
19
+
20
+ const pkgPath = path.join(webAppDir, "package.json");
21
+ if (await fs.pathExists(pkgPath)) {
22
+ const pkg = await fs.readJson(pkgPath);
23
+
24
+ if (!options?.skipAppScripts) {
25
+ pkg.scripts = {
26
+ ...pkg.scripts,
27
+ dev: "alchemy dev",
28
+ deploy: "alchemy deploy",
29
+ destroy: "alchemy destroy",
30
+ };
31
+ }
32
+
33
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
34
+ }
35
+
36
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
37
+ if (await fs.pathExists(viteConfigPath)) {
38
+ try {
39
+ const project = new Project({
40
+ manipulationSettings: {
41
+ indentationText: IndentationText.TwoSpaces,
42
+ quoteKind: QuoteKind.Double,
43
+ },
44
+ });
45
+
46
+ project.addSourceFileAtPath(viteConfigPath);
47
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
48
+
49
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
50
+ if (!alchemyImport) {
51
+ sourceFile.addImportDeclaration({
52
+ moduleSpecifier: "alchemy/cloudflare/tanstack-start",
53
+ defaultImport: "alchemy",
54
+ });
55
+ } else {
56
+ alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
57
+ }
58
+
59
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
60
+ if (!exportAssignment) return;
61
+
62
+ const defineConfigCall = exportAssignment.getExpression();
63
+ if (
64
+ !Node.isCallExpression(defineConfigCall) ||
65
+ defineConfigCall.getExpression().getText() !== "defineConfig"
66
+ )
67
+ return;
68
+
69
+ let configObject = defineConfigCall.getArguments()[0];
70
+ if (!configObject) {
71
+ configObject = defineConfigCall.addArgument("{}");
72
+ }
73
+
74
+ if (Node.isObjectLiteralExpression(configObject)) {
75
+ const pluginsProperty = configObject.getProperty("plugins");
76
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
77
+ const initializer = pluginsProperty.getInitializer();
78
+ if (Node.isArrayLiteralExpression(initializer)) {
79
+ const hasAlchemy = initializer
80
+ .getElements()
81
+ .some((el) => el.getText().includes("alchemy("));
82
+ if (!hasAlchemy) {
83
+ initializer.addElement("alchemy()");
84
+ }
85
+ }
86
+ } else {
87
+ configObject.addPropertyAssignment({
88
+ name: "plugins",
89
+ initializer: "[alchemy()]",
90
+ });
91
+ }
92
+ }
93
+
94
+ await project.save();
95
+ } catch (error) {
96
+ console.warn("Failed to update vite.config.ts:", error);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { Project } from "ts-morph";
4
+ import type { ProjectConfig } from "../../../types";
5
+
6
+ const tsProject = new Project({
7
+ useInMemoryFileSystem: false,
8
+ skipAddingFilesFromTsConfig: true,
9
+ });
10
+
11
+ function determineImportPath(
12
+ envDtsPath: string,
13
+ projectDir: string,
14
+ config: ProjectConfig,
15
+ ): string {
16
+ const { webDeploy, serverDeploy, backend } = config;
17
+ const isBackendSelf = backend === "self";
18
+
19
+ let alchemyRunPath: string;
20
+
21
+ if (webDeploy === "alchemy" && (serverDeploy === "alchemy" || isBackendSelf)) {
22
+ // Both web and server are alchemy, or web + backend=self
23
+ if (isBackendSelf) {
24
+ alchemyRunPath = path.join(projectDir, "apps/web/alchemy.run.ts");
25
+ } else {
26
+ alchemyRunPath = path.join(projectDir, "alchemy.run.ts");
27
+ }
28
+ } else if (webDeploy === "alchemy") {
29
+ // Only web is alchemy
30
+ alchemyRunPath = path.join(projectDir, "apps/web/alchemy.run.ts");
31
+ } else if (serverDeploy === "alchemy") {
32
+ // Only server is alchemy
33
+ alchemyRunPath = path.join(projectDir, "apps/server/alchemy.run.ts");
34
+ } else {
35
+ // Should not happen, but fallback
36
+ alchemyRunPath = path.join(projectDir, "alchemy.run.ts");
37
+ }
38
+
39
+ // Calculate relative path from env.d.ts to alchemy.run.ts
40
+ const relativePath = path.relative(path.dirname(envDtsPath), alchemyRunPath.replace(/\.ts$/, ""));
41
+
42
+ // Normalize the path for imports (use forward slashes, handle relative paths)
43
+ const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
44
+
45
+ return importPath.replace(/\\/g, "/");
46
+ }
47
+
48
+ export async function setupEnvDtsImport(
49
+ envDtsPath: string,
50
+ projectDir: string,
51
+ config: ProjectConfig,
52
+ ) {
53
+ if (!(await fs.pathExists(envDtsPath))) {
54
+ return;
55
+ }
56
+
57
+ const importPath = determineImportPath(envDtsPath, projectDir, config);
58
+
59
+ const sourceFile = tsProject.addSourceFileAtPath(envDtsPath);
60
+
61
+ const existingImports = sourceFile.getImportDeclarations();
62
+ const alreadyHasImport = existingImports.some(
63
+ (imp) =>
64
+ imp.getModuleSpecifierValue() === importPath &&
65
+ imp.getNamedImports().some((named) => named.getName() === "server"),
66
+ );
67
+
68
+ if (!alreadyHasImport) {
69
+ sourceFile.insertImportDeclaration(0, {
70
+ moduleSpecifier: importPath,
71
+ namedImports: [{ name: "server", isTypeOnly: true }],
72
+ });
73
+ }
74
+
75
+ await sourceFile.save();
76
+ }
@@ -0,0 +1,7 @@
1
+ export { setupNextAlchemyDeploy } from "./alchemy-next-setup";
2
+ export { setupNuxtAlchemyDeploy } from "./alchemy-nuxt-setup";
3
+ export { setupReactRouterAlchemyDeploy } from "./alchemy-react-router-setup";
4
+ export { setupSolidAlchemyDeploy } from "./alchemy-solid-setup";
5
+ export { setupSvelteAlchemyDeploy } from "./alchemy-svelte-setup";
6
+ export { setupTanStackRouterAlchemyDeploy } from "./alchemy-tanstack-router-setup";
7
+ export { setupTanStackStartAlchemyDeploy } from "./alchemy-tanstack-start-setup";
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import type { ProjectConfig } from "../../types";
4
+ import { addPackageDependency } from "../../utils/add-package-deps";
5
+
6
+ export async function setupServerDeploy(config: ProjectConfig) {
7
+ const { serverDeploy, webDeploy, projectDir } = config;
8
+
9
+ if (serverDeploy === "none") return;
10
+
11
+ if (serverDeploy === "alchemy" && webDeploy === "alchemy") {
12
+ return;
13
+ }
14
+
15
+ const serverDir = path.join(projectDir, "apps/server");
16
+ if (!(await fs.pathExists(serverDir))) return;
17
+
18
+ if (serverDeploy === "alchemy") {
19
+ await setupAlchemyServerDeploy(serverDir, projectDir);
20
+ }
21
+ }
22
+
23
+ export async function setupAlchemyServerDeploy(serverDir: string, projectDir?: string) {
24
+ if (!(await fs.pathExists(serverDir))) return;
25
+
26
+ await addPackageDependency({
27
+ devDependencies: ["alchemy", "wrangler", "@types/node", "@cloudflare/workers-types"],
28
+ projectDir: serverDir,
29
+ });
30
+
31
+ if (projectDir) {
32
+ await addAlchemyPackagesDependencies(projectDir);
33
+ }
34
+
35
+ const packageJsonPath = path.join(serverDir, "package.json");
36
+ if (await fs.pathExists(packageJsonPath)) {
37
+ const packageJson = await fs.readJson(packageJsonPath);
38
+
39
+ packageJson.scripts = {
40
+ ...packageJson.scripts,
41
+ dev: "alchemy dev",
42
+ deploy: "alchemy deploy",
43
+ destroy: "alchemy destroy",
44
+ };
45
+
46
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
47
+ }
48
+ }
49
+
50
+ async function addAlchemyPackagesDependencies(projectDir: string) {
51
+ await addPackageDependency({
52
+ devDependencies: ["@cloudflare/workers-types"],
53
+ projectDir,
54
+ });
55
+ }
@@ -0,0 +1,58 @@
1
+ import type { ProjectConfig } from "../../types";
2
+ import { addPackageDependency } from "../../utils/add-package-deps";
3
+ import { setupCombinedAlchemyDeploy } from "./alchemy/alchemy-combined-setup";
4
+ import { setupNextAlchemyDeploy } from "./alchemy/alchemy-next-setup";
5
+ import { setupNuxtAlchemyDeploy } from "./alchemy/alchemy-nuxt-setup";
6
+ import { setupReactRouterAlchemyDeploy } from "./alchemy/alchemy-react-router-setup";
7
+ import { setupSolidAlchemyDeploy } from "./alchemy/alchemy-solid-setup";
8
+ import { setupSvelteAlchemyDeploy } from "./alchemy/alchemy-svelte-setup";
9
+ import { setupTanStackRouterAlchemyDeploy } from "./alchemy/alchemy-tanstack-router-setup";
10
+ import { setupTanStackStartAlchemyDeploy } from "./alchemy/alchemy-tanstack-start-setup";
11
+
12
+ export async function setupWebDeploy(config: ProjectConfig) {
13
+ const { webDeploy, serverDeploy, frontend, projectDir } = config;
14
+ const { packageManager } = config;
15
+
16
+ if (webDeploy === "none") return;
17
+
18
+ if (webDeploy !== "alchemy") return;
19
+
20
+ if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
21
+ await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
22
+ await addAlchemyPackagesDependencies(projectDir);
23
+ return;
24
+ }
25
+
26
+ const isNext = frontend.includes("next");
27
+ const isNuxt = frontend.includes("nuxt");
28
+ const isSvelte = frontend.includes("svelte");
29
+ const isTanstackRouter = frontend.includes("tanstack-router");
30
+ const isTanstackStart = frontend.includes("tanstack-start");
31
+ const isReactRouter = frontend.includes("react-router");
32
+ const isSolid = frontend.includes("solid");
33
+
34
+ if (isNext) {
35
+ await setupNextAlchemyDeploy(projectDir, packageManager);
36
+ } else if (isNuxt) {
37
+ await setupNuxtAlchemyDeploy(projectDir, packageManager);
38
+ } else if (isSvelte) {
39
+ await setupSvelteAlchemyDeploy(projectDir, packageManager);
40
+ } else if (isTanstackStart) {
41
+ await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
42
+ } else if (isTanstackRouter) {
43
+ await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
44
+ } else if (isReactRouter) {
45
+ await setupReactRouterAlchemyDeploy(projectDir, packageManager);
46
+ } else if (isSolid) {
47
+ await setupSolidAlchemyDeploy(projectDir, packageManager);
48
+ }
49
+
50
+ await addAlchemyPackagesDependencies(projectDir);
51
+ }
52
+
53
+ async function addAlchemyPackagesDependencies(projectDir: string) {
54
+ await addPackageDependency({
55
+ devDependencies: ["@cloudflare/workers-types"],
56
+ projectDir,
57
+ });
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * create-better-t-stack
3
+ *
4
+ * A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects.
5
+ *
6
+ * @example Programmatic usage
7
+ * ```typescript
8
+ * import { create } from "create-better-t-stack";
9
+ *
10
+ * const result = await create({
11
+ * projectName: "my-app",
12
+ * frontend: ["tanstack-router"],
13
+ * backend: "hono",
14
+ * database: "sqlite",
15
+ * orm: "drizzle",
16
+ * defaults: true,
17
+ * });
18
+ *
19
+ * if (result.success) {
20
+ * console.log(`Created at: ${result.projectDirectory}`);
21
+ * }
22
+ * ```
23
+ */
24
+
25
+ // Main programmatic API
26
+ export { create, type CreateOptions, type CreateResult } from "./api";
27
+
28
+ // Re-export types for type-safe usage
29
+ export type {
30
+ Addons,
31
+ API,
32
+ Auth,
33
+ Backend,
34
+ Database,
35
+ DatabaseSetup,
36
+ Examples,
37
+ Frontend,
38
+ ORM,
39
+ PackageManager,
40
+ Payments,
41
+ ProjectConfig,
42
+ Runtime,
43
+ ServerDeploy,
44
+ WebDeploy,
45
+ DirectoryConflict,
46
+ Template,
47
+ BetterTStackConfig,
48
+ } from "./types";
49
+
50
+ // Legacy exports for backwards compatibility (deprecated)
51
+ export { create as init } from "./api";
@@ -0,0 +1,200 @@
1
+ import { groupMultiselect, isCancel } from "@clack/prompts";
2
+ import { DEFAULT_CONFIG } from "../constants";
3
+ import { type Addons, AddonsSchema, type Auth, type Frontend } from "../types";
4
+ import { getCompatibleAddons, validateAddonCompatibility } from "../utils/compatibility-rules";
5
+ import { exitCancelled } from "../utils/errors";
6
+
7
+ type AddonOption = {
8
+ value: Addons;
9
+ label: string;
10
+ hint: string;
11
+ };
12
+
13
+ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
14
+ let label: string;
15
+ let hint: string;
16
+
17
+ switch (addon) {
18
+ case "turborepo":
19
+ label = "Turborepo";
20
+ hint = "High-performance build system";
21
+ break;
22
+ case "pwa":
23
+ label = "PWA";
24
+ hint = "Make your app installable and work offline";
25
+ break;
26
+ case "tauri":
27
+ label = "Tauri";
28
+ hint = "Build native desktop apps from your web frontend";
29
+ break;
30
+ case "biome":
31
+ label = "Biome";
32
+ hint = "Format, lint, and more";
33
+ break;
34
+ case "oxlint":
35
+ label = "Oxlint";
36
+ hint = "Oxlint + Oxfmt (linting & formatting)";
37
+ break;
38
+ case "ultracite":
39
+ label = "Ultracite";
40
+ hint = "Zero-config Biome preset with AI integration";
41
+ break;
42
+ case "ruler":
43
+ label = "Ruler";
44
+ hint = "Centralize your AI rules";
45
+ break;
46
+ case "husky":
47
+ label = "Husky";
48
+ hint = "Modern native Git hooks made easy";
49
+ break;
50
+ case "starlight":
51
+ label = "Starlight";
52
+ hint = "Build stellar docs with astro";
53
+ break;
54
+ case "fumadocs":
55
+ label = "Fumadocs";
56
+ hint = "Build excellent documentation site";
57
+ break;
58
+ case "opentui":
59
+ label = "OpenTUI";
60
+ hint = "Build terminal user interfaces";
61
+ break;
62
+ case "wxt":
63
+ label = "WXT";
64
+ hint = "Build browser extensions";
65
+ break;
66
+ default:
67
+ label = addon;
68
+ hint = `Add ${addon}`;
69
+ }
70
+
71
+ return { label, hint };
72
+ }
73
+
74
+ const ADDON_GROUPS = {
75
+ Documentation: ["starlight", "fumadocs"],
76
+ Linting: ["biome", "oxlint", "ultracite"],
77
+ Other: ["ruler", "pwa", "tauri", "husky", "opentui", "wxt", "turborepo"],
78
+ };
79
+
80
+ export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], auth?: Auth) {
81
+ if (addons !== undefined) return addons;
82
+
83
+ const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
84
+ const groupedOptions: Record<string, AddonOption[]> = {
85
+ Documentation: [],
86
+ Linting: [],
87
+ Other: [],
88
+ };
89
+
90
+ const frontendsArray = frontends || [];
91
+
92
+ for (const addon of allAddons) {
93
+ const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
94
+ if (!isCompatible) continue;
95
+
96
+ const { label, hint } = getAddonDisplay(addon);
97
+ const option = { value: addon, label, hint };
98
+
99
+ if (ADDON_GROUPS.Documentation.includes(addon)) {
100
+ groupedOptions.Documentation.push(option);
101
+ } else if (ADDON_GROUPS.Linting.includes(addon)) {
102
+ groupedOptions.Linting.push(option);
103
+ } else if (ADDON_GROUPS.Other.includes(addon)) {
104
+ groupedOptions.Other.push(option);
105
+ }
106
+ }
107
+
108
+ Object.keys(groupedOptions).forEach((group) => {
109
+ if (groupedOptions[group].length === 0) {
110
+ delete groupedOptions[group];
111
+ } else {
112
+ const groupOrder = ADDON_GROUPS[group as keyof typeof ADDON_GROUPS] || [];
113
+ groupedOptions[group].sort((a, b) => {
114
+ const indexA = groupOrder.indexOf(a.value);
115
+ const indexB = groupOrder.indexOf(b.value);
116
+ return indexA - indexB;
117
+ });
118
+ }
119
+ });
120
+
121
+ const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) =>
122
+ Object.values(groupedOptions).some((options) =>
123
+ options.some((opt) => opt.value === addonValue),
124
+ ),
125
+ );
126
+
127
+ const response = await groupMultiselect<Addons>({
128
+ message: "Select addons",
129
+ options: groupedOptions,
130
+ initialValues: initialValues,
131
+ required: false,
132
+ selectableGroups: false,
133
+ });
134
+
135
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
136
+
137
+ return response;
138
+ }
139
+
140
+ export async function getAddonsToAdd(
141
+ frontend: Frontend[],
142
+ existingAddons: Addons[] = [],
143
+ auth?: Auth,
144
+ ) {
145
+ const groupedOptions: Record<string, AddonOption[]> = {
146
+ Documentation: [],
147
+ Linting: [],
148
+ Other: [],
149
+ };
150
+
151
+ const frontendArray = frontend || [];
152
+
153
+ const compatibleAddons = getCompatibleAddons(
154
+ AddonsSchema.options.filter((addon) => addon !== "none"),
155
+ frontendArray,
156
+ existingAddons,
157
+ auth,
158
+ );
159
+
160
+ for (const addon of compatibleAddons) {
161
+ const { label, hint } = getAddonDisplay(addon);
162
+ const option = { value: addon, label, hint };
163
+
164
+ if (ADDON_GROUPS.Documentation.includes(addon)) {
165
+ groupedOptions.Documentation.push(option);
166
+ } else if (ADDON_GROUPS.Linting.includes(addon)) {
167
+ groupedOptions.Linting.push(option);
168
+ } else if (ADDON_GROUPS.Other.includes(addon)) {
169
+ groupedOptions.Other.push(option);
170
+ }
171
+ }
172
+
173
+ Object.keys(groupedOptions).forEach((group) => {
174
+ if (groupedOptions[group].length === 0) {
175
+ delete groupedOptions[group];
176
+ } else {
177
+ const groupOrder = ADDON_GROUPS[group as keyof typeof ADDON_GROUPS] || [];
178
+ groupedOptions[group].sort((a, b) => {
179
+ const indexA = groupOrder.indexOf(a.value);
180
+ const indexB = groupOrder.indexOf(b.value);
181
+ return indexA - indexB;
182
+ });
183
+ }
184
+ });
185
+
186
+ if (Object.keys(groupedOptions).length === 0) {
187
+ return [];
188
+ }
189
+
190
+ const response = await groupMultiselect<Addons>({
191
+ message: "Select addons to add",
192
+ options: groupedOptions,
193
+ required: false,
194
+ selectableGroups: false,
195
+ });
196
+
197
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
198
+
199
+ return response;
200
+ }