tvi-cli 0.1.4

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 (306) hide show
  1. package/README.md +147 -0
  2. package/dist/index.js +4315 -0
  3. package/package.json +75 -0
  4. package/templates/addons/biome/biome.json.hbs +83 -0
  5. package/templates/addons/husky/.husky/pre-commit +1 -0
  6. package/templates/addons/pwa/apps/web/next/public/favicon/apple-touch-icon.png +0 -0
  7. package/templates/addons/pwa/apps/web/next/public/favicon/favicon-96x96.png +0 -0
  8. package/templates/addons/pwa/apps/web/next/public/favicon/favicon.svg +6 -0
  9. package/templates/addons/pwa/apps/web/next/public/favicon/site.webmanifest.hbs +21 -0
  10. package/templates/addons/pwa/apps/web/next/public/favicon/web-app-manifest-192x192.png +0 -0
  11. package/templates/addons/pwa/apps/web/next/public/favicon/web-app-manifest-512x512.png +0 -0
  12. package/templates/addons/pwa/apps/web/next/src/app/manifest.ts.hbs +26 -0
  13. package/templates/addons/pwa/apps/web/vite/public/logo.png +0 -0
  14. package/templates/addons/pwa/apps/web/vite/pwa-assets.config.ts.hbs +12 -0
  15. package/templates/addons/turborepo/turbo.json.hbs +43 -0
  16. package/templates/api/orpc/native/utils/orpc.ts.hbs +35 -0
  17. package/templates/api/orpc/server/base/src/lib/context.ts.hbs +125 -0
  18. package/templates/api/orpc/server/base/src/lib/orpc.ts.hbs +21 -0
  19. package/templates/api/orpc/server/next/src/app/rpc/[...all]/route.ts.hbs +23 -0
  20. package/templates/api/orpc/web/nuxt/app/plugins/orpc.ts.hbs +35 -0
  21. package/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs +42 -0
  22. package/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs +30 -0
  23. package/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbs +31 -0
  24. package/templates/api/trpc/native/utils/trpc.ts.hbs +32 -0
  25. package/templates/api/trpc/server/base/src/lib/context.ts.hbs +127 -0
  26. package/templates/api/trpc/server/base/src/lib/trpc.ts.hbs +26 -0
  27. package/templates/api/trpc/server/next/src/app/trpc/[trpc]/route.ts +14 -0
  28. package/templates/api/trpc/web/react/base/src/utils/trpc.ts.hbs +97 -0
  29. package/templates/auth/native/native-base/lib/auth-client.ts.hbs +13 -0
  30. package/templates/auth/native/nativewind/app/(drawer)/index.tsx.hbs +95 -0
  31. package/templates/auth/native/nativewind/components/sign-in.tsx.hbs +93 -0
  32. package/templates/auth/native/nativewind/components/sign-up.tsx.hbs +104 -0
  33. package/templates/auth/native/unistyles/app/(drawer)/index.tsx.hbs +179 -0
  34. package/templates/auth/native/unistyles/components/sign-in.tsx.hbs +134 -0
  35. package/templates/auth/native/unistyles/components/sign-up.tsx.hbs +152 -0
  36. package/templates/auth/server/base/src/lib/auth.ts.hbs +141 -0
  37. package/templates/auth/server/db/drizzle/mysql/src/db/schema/auth.ts +58 -0
  38. package/templates/auth/server/db/drizzle/postgres/src/db/schema/auth.ts +47 -0
  39. package/templates/auth/server/db/drizzle/sqlite/src/db/schema/auth.ts +55 -0
  40. package/templates/auth/server/db/mongoose/mongodb/src/db/models/auth.model.ts +68 -0
  41. package/templates/auth/server/db/prisma/mongodb/prisma/schema/auth.prisma +59 -0
  42. package/templates/auth/server/db/prisma/mysql/prisma/schema/auth.prisma +59 -0
  43. package/templates/auth/server/db/prisma/postgres/prisma/schema/auth.prisma +59 -0
  44. package/templates/auth/server/db/prisma/sqlite/prisma/schema/auth.prisma +59 -0
  45. package/templates/auth/server/next/src/app/api/auth/[...all]/route.ts +4 -0
  46. package/templates/auth/web/nuxt/app/components/SignInForm.vue +77 -0
  47. package/templates/auth/web/nuxt/app/components/SignUpForm.vue +84 -0
  48. package/templates/auth/web/nuxt/app/components/UserMenu.vue +42 -0
  49. package/templates/auth/web/nuxt/app/middleware/auth.ts +12 -0
  50. package/templates/auth/web/nuxt/app/pages/dashboard.vue +27 -0
  51. package/templates/auth/web/nuxt/app/pages/login.vue +24 -0
  52. package/templates/auth/web/nuxt/app/plugins/auth-client.ts +16 -0
  53. package/templates/auth/web/react/base/src/lib/auth-client.ts.hbs +10 -0
  54. package/templates/auth/web/react/next/src/app/dashboard/page.tsx.hbs +47 -0
  55. package/templates/auth/web/react/next/src/app/login/page.tsx +16 -0
  56. package/templates/auth/web/react/next/src/components/sign-in-form.tsx +135 -0
  57. package/templates/auth/web/react/next/src/components/sign-up-form.tsx +160 -0
  58. package/templates/auth/web/react/next/src/components/theme-provider.tsx +11 -0
  59. package/templates/auth/web/react/next/src/components/user-menu.tsx +60 -0
  60. package/templates/auth/web/react/react-router/src/components/sign-in-form.tsx +135 -0
  61. package/templates/auth/web/react/react-router/src/components/sign-up-form.tsx +160 -0
  62. package/templates/auth/web/react/react-router/src/components/user-menu.tsx +60 -0
  63. package/templates/auth/web/react/react-router/src/routes/dashboard.tsx.hbs +40 -0
  64. package/templates/auth/web/react/react-router/src/routes/login.tsx +13 -0
  65. package/templates/auth/web/react/tanstack-router/src/components/sign-in-form.tsx +139 -0
  66. package/templates/auth/web/react/tanstack-router/src/components/sign-up-form.tsx +164 -0
  67. package/templates/auth/web/react/tanstack-router/src/components/user-menu.tsx +62 -0
  68. package/templates/auth/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +47 -0
  69. package/templates/auth/web/react/tanstack-router/src/routes/login.tsx +18 -0
  70. package/templates/auth/web/react/tanstack-start/src/components/sign-in-form.tsx +139 -0
  71. package/templates/auth/web/react/tanstack-start/src/components/sign-up-form.tsx +164 -0
  72. package/templates/auth/web/react/tanstack-start/src/components/user-menu.tsx +62 -0
  73. package/templates/auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +51 -0
  74. package/templates/auth/web/react/tanstack-start/src/routes/login.tsx +18 -0
  75. package/templates/auth/web/solid/src/components/sign-in-form.tsx +132 -0
  76. package/templates/auth/web/solid/src/components/sign-up-form.tsx +158 -0
  77. package/templates/auth/web/solid/src/components/user-menu.tsx.hbs +55 -0
  78. package/templates/auth/web/solid/src/lib/auth-client.ts +5 -0
  79. package/templates/auth/web/solid/src/routes/dashboard.tsx +38 -0
  80. package/templates/auth/web/solid/src/routes/login.tsx +23 -0
  81. package/templates/auth/web/svelte/src/components/SignInForm.svelte +108 -0
  82. package/templates/auth/web/svelte/src/components/SignUpForm.svelte +142 -0
  83. package/templates/auth/web/svelte/src/components/UserMenu.svelte +54 -0
  84. package/templates/auth/web/svelte/src/lib/auth-client.ts +6 -0
  85. package/templates/auth/web/svelte/src/routes/dashboard/+page.svelte +31 -0
  86. package/templates/auth/web/svelte/src/routes/login/+page.svelte +12 -0
  87. package/templates/backend/convex/packages/backend/_gitignore +2 -0
  88. package/templates/backend/convex/packages/backend/convex/README.md +90 -0
  89. package/templates/backend/convex/packages/backend/convex/healthCheck.ts +7 -0
  90. package/templates/backend/convex/packages/backend/convex/schema.ts +9 -0
  91. package/templates/backend/convex/packages/backend/convex/todos.ts +42 -0
  92. package/templates/backend/convex/packages/backend/convex/tsconfig.json +25 -0
  93. package/templates/backend/convex/packages/backend/package.json.hbs +17 -0
  94. package/templates/backend/server/elysia/src/index.ts.hbs +72 -0
  95. package/templates/backend/server/express/src/index.ts.hbs +88 -0
  96. package/templates/backend/server/fastify/src/index.ts.hbs +155 -0
  97. package/templates/backend/server/hono/src/index.ts.hbs +133 -0
  98. package/templates/backend/server/next/next-env.d.ts +5 -0
  99. package/templates/backend/server/next/next.config.ts +7 -0
  100. package/templates/backend/server/next/package.json.hbs +24 -0
  101. package/templates/backend/server/next/src/app/route.ts +5 -0
  102. package/templates/backend/server/next/src/middleware.ts +19 -0
  103. package/templates/backend/server/next/tsconfig.json.hbs +33 -0
  104. package/templates/backend/server/server-base/_gitignore +52 -0
  105. package/templates/backend/server/server-base/package.json.hbs +28 -0
  106. package/templates/backend/server/server-base/src/routers/index.ts.hbs +53 -0
  107. package/templates/backend/server/server-base/tsconfig.json.hbs +39 -0
  108. package/templates/base/_gitignore +2 -0
  109. package/templates/base/package.json.hbs +11 -0
  110. package/templates/db/drizzle/mysql/drizzle.config.ts.hbs +10 -0
  111. package/templates/db/drizzle/mysql/src/db/index.ts.hbs +20 -0
  112. package/templates/db/drizzle/postgres/drizzle.config.ts.hbs +10 -0
  113. package/templates/db/drizzle/postgres/src/db/index.ts.hbs +12 -0
  114. package/templates/db/drizzle/sqlite/drizzle.config.ts.hbs +24 -0
  115. package/templates/db/drizzle/sqlite/src/db/index.ts.hbs +35 -0
  116. package/templates/db/mongoose/mongodb/src/db/index.ts.hbs +9 -0
  117. package/templates/db/prisma/mongodb/prisma/index.ts.hbs +5 -0
  118. package/templates/db/prisma/mongodb/prisma/schema/schema.prisma +10 -0
  119. package/templates/db/prisma/mongodb/prisma.config.ts.hbs +8 -0
  120. package/templates/db/prisma/mysql/prisma/index.ts +5 -0
  121. package/templates/db/prisma/mysql/prisma/schema/schema.prisma +10 -0
  122. package/templates/db/prisma/mysql/prisma.config.ts +8 -0
  123. package/templates/db/prisma/postgres/prisma/index.ts +5 -0
  124. package/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs +13 -0
  125. package/templates/db/prisma/postgres/prisma.config.ts.hbs +12 -0
  126. package/templates/db/prisma/sqlite/prisma/index.ts +5 -0
  127. package/templates/db/prisma/sqlite/prisma/schema/schema.prisma +10 -0
  128. package/templates/db/prisma/sqlite/prisma.config.ts +8 -0
  129. package/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs +155 -0
  130. package/templates/examples/ai/native/nativewind/polyfills.js +25 -0
  131. package/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs +279 -0
  132. package/templates/examples/ai/native/unistyles/polyfills.js +25 -0
  133. package/templates/examples/ai/server/next/src/app/ai/route.ts +15 -0
  134. package/templates/examples/ai/web/nuxt/app/pages/ai.vue +63 -0
  135. package/templates/examples/ai/web/react/next/src/app/ai/page.tsx +67 -0
  136. package/templates/examples/ai/web/react/react-router/src/routes/ai.tsx +64 -0
  137. package/templates/examples/ai/web/react/tanstack-router/src/routes/ai.tsx +69 -0
  138. package/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx +69 -0
  139. package/templates/examples/ai/web/svelte/src/routes/ai/+page.svelte +98 -0
  140. package/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs +295 -0
  141. package/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs +340 -0
  142. package/templates/examples/todo/server/drizzle/base/src/routers/todo.ts.hbs +79 -0
  143. package/templates/examples/todo/server/drizzle/mysql/src/db/schema/todo.ts +7 -0
  144. package/templates/examples/todo/server/drizzle/postgres/src/db/schema/todo.ts +7 -0
  145. package/templates/examples/todo/server/drizzle/sqlite/src/db/schema/todo.ts +7 -0
  146. package/templates/examples/todo/server/mongoose/base/src/routers/todo.ts.hbs +66 -0
  147. package/templates/examples/todo/server/mongoose/mongodb/src/db/models/todo.model.ts +24 -0
  148. package/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs +118 -0
  149. package/templates/examples/todo/server/prisma/mongodb/prisma/schema/todo.prisma +7 -0
  150. package/templates/examples/todo/server/prisma/mysql/prisma/schema/todo.prisma +7 -0
  151. package/templates/examples/todo/server/prisma/postgres/prisma/schema/todo.prisma +7 -0
  152. package/templates/examples/todo/server/prisma/sqlite/prisma/schema/todo.prisma +7 -0
  153. package/templates/examples/todo/web/nuxt/app/pages/todos.vue +108 -0
  154. package/templates/examples/todo/web/react/next/src/app/todos/page.tsx.hbs +245 -0
  155. package/templates/examples/todo/web/react/react-router/src/routes/todos.tsx.hbs +242 -0
  156. package/templates/examples/todo/web/react/tanstack-router/src/routes/todos.tsx.hbs +247 -0
  157. package/templates/examples/todo/web/react/tanstack-start/src/routes/todos.tsx.hbs +268 -0
  158. package/templates/examples/todo/web/solid/src/routes/todos.tsx.hbs +132 -0
  159. package/templates/examples/todo/web/svelte/src/routes/todos/+page.svelte.hbs +317 -0
  160. package/templates/extras/_npmrc.hbs +5 -0
  161. package/templates/extras/pnpm-workspace.yaml +3 -0
  162. package/templates/frontend/native/native-base/assets/adaptive-icon.png +0 -0
  163. package/templates/frontend/native/native-base/assets/favicon.png +0 -0
  164. package/templates/frontend/native/native-base/assets/icon.png +0 -0
  165. package/templates/frontend/native/native-base/assets/splash.png +0 -0
  166. package/templates/frontend/native/nativewind/_gitignore +25 -0
  167. package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx +46 -0
  168. package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx +19 -0
  169. package/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx +19 -0
  170. package/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs +67 -0
  171. package/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs +95 -0
  172. package/templates/frontend/native/nativewind/app/+html.tsx +47 -0
  173. package/templates/frontend/native/nativewind/app/+not-found.tsx +29 -0
  174. package/templates/frontend/native/nativewind/app/_layout.tsx.hbs +126 -0
  175. package/templates/frontend/native/nativewind/app/modal.tsx +14 -0
  176. package/templates/frontend/native/nativewind/app-env.d.ts +2 -0
  177. package/templates/frontend/native/nativewind/app.json +46 -0
  178. package/templates/frontend/native/nativewind/babel.config.js +11 -0
  179. package/templates/frontend/native/nativewind/components/container.tsx +8 -0
  180. package/templates/frontend/native/nativewind/components/header-button.tsx +26 -0
  181. package/templates/frontend/native/nativewind/components/tabbar-icon.tsx +8 -0
  182. package/templates/frontend/native/nativewind/global.css +50 -0
  183. package/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx +11 -0
  184. package/templates/frontend/native/nativewind/lib/constants.ts +18 -0
  185. package/templates/frontend/native/nativewind/lib/use-color-scheme.ts +12 -0
  186. package/templates/frontend/native/nativewind/metro.config.js +59 -0
  187. package/templates/frontend/native/nativewind/package.json.hbs +49 -0
  188. package/templates/frontend/native/nativewind/tailwind.config.js +59 -0
  189. package/templates/frontend/native/nativewind/tsconfig.json.hbs +23 -0
  190. package/templates/frontend/native/unistyles/_gitignore +24 -0
  191. package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx +39 -0
  192. package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx +37 -0
  193. package/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx +37 -0
  194. package/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx.hbs +87 -0
  195. package/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs +194 -0
  196. package/templates/frontend/native/unistyles/app/+html.tsx +48 -0
  197. package/templates/frontend/native/unistyles/app/+not-found.tsx +65 -0
  198. package/templates/frontend/native/unistyles/app/_layout.tsx.hbs +104 -0
  199. package/templates/frontend/native/unistyles/app/modal.tsx +33 -0
  200. package/templates/frontend/native/unistyles/app.json +44 -0
  201. package/templates/frontend/native/unistyles/babel.config.js +20 -0
  202. package/templates/frontend/native/unistyles/breakpoints.ts +9 -0
  203. package/templates/frontend/native/unistyles/components/container.tsx +15 -0
  204. package/templates/frontend/native/unistyles/components/header-button.tsx +36 -0
  205. package/templates/frontend/native/unistyles/components/tabbar-icon.tsx +8 -0
  206. package/templates/frontend/native/unistyles/expo-env.d.ts +3 -0
  207. package/templates/frontend/native/unistyles/index.js +2 -0
  208. package/templates/frontend/native/unistyles/metro.config.js +20 -0
  209. package/templates/frontend/native/unistyles/package.json.hbs +50 -0
  210. package/templates/frontend/native/unistyles/theme.ts +98 -0
  211. package/templates/frontend/native/unistyles/tsconfig.json.hbs +17 -0
  212. package/templates/frontend/native/unistyles/unistyles.ts +27 -0
  213. package/templates/frontend/nuxt/_gitignore +24 -0
  214. package/templates/frontend/nuxt/app/app.config.ts +15 -0
  215. package/templates/frontend/nuxt/app/app.vue +13 -0
  216. package/templates/frontend/nuxt/app/assets/css/main.css +2 -0
  217. package/templates/frontend/nuxt/app/components/Header.vue.hbs +45 -0
  218. package/templates/frontend/nuxt/app/components/Loader.vue +5 -0
  219. package/templates/frontend/nuxt/app/components/ModeToggle.vue +23 -0
  220. package/templates/frontend/nuxt/app/layouts/default.vue.hbs +11 -0
  221. package/templates/frontend/nuxt/app/pages/index.vue.hbs +57 -0
  222. package/templates/frontend/nuxt/app/plugins/vue-query.ts.hbs +44 -0
  223. package/templates/frontend/nuxt/nuxt.config.ts.hbs +19 -0
  224. package/templates/frontend/nuxt/package.json.hbs +25 -0
  225. package/templates/frontend/nuxt/public/favicon.ico +0 -0
  226. package/templates/frontend/nuxt/public/robots.txt +2 -0
  227. package/templates/frontend/nuxt/server/tsconfig.json +3 -0
  228. package/templates/frontend/nuxt/tsconfig.json.hbs +9 -0
  229. package/templates/frontend/react/next/next-env.d.ts.hbs +5 -0
  230. package/templates/frontend/react/next/next.config.ts.hbs +5 -0
  231. package/templates/frontend/react/next/package.json.hbs +34 -0
  232. package/templates/frontend/react/next/postcss.config.mjs.hbs +5 -0
  233. package/templates/frontend/react/next/src/app/favicon.ico +0 -0
  234. package/templates/frontend/react/next/src/app/layout.tsx.hbs +41 -0
  235. package/templates/frontend/react/next/src/app/page.tsx.hbs +68 -0
  236. package/templates/frontend/react/next/src/components/mode-toggle.tsx.hbs +39 -0
  237. package/templates/frontend/react/next/src/components/providers.tsx.hbs +56 -0
  238. package/templates/frontend/react/next/src/components/theme-provider.tsx.hbs +11 -0
  239. package/templates/frontend/react/next/tsconfig.json.hbs +33 -0
  240. package/templates/frontend/react/react-router/package.json.hbs +42 -0
  241. package/templates/frontend/react/react-router/public/favicon.ico +0 -0
  242. package/templates/frontend/react/react-router/react-router.config.ts +6 -0
  243. package/templates/frontend/react/react-router/src/components/mode-toggle.tsx +37 -0
  244. package/templates/frontend/react/react-router/src/components/theme-provider.tsx +73 -0
  245. package/templates/frontend/react/react-router/src/root.tsx.hbs +152 -0
  246. package/templates/frontend/react/react-router/src/routes/_index.tsx.hbs +74 -0
  247. package/templates/frontend/react/react-router/src/routes.ts +4 -0
  248. package/templates/frontend/react/react-router/tsconfig.json.hbs +32 -0
  249. package/templates/frontend/react/react-router/vite.config.ts.hbs +33 -0
  250. package/templates/frontend/react/tanstack-router/index.html +12 -0
  251. package/templates/frontend/react/tanstack-router/package.json.hbs +41 -0
  252. package/templates/frontend/react/tanstack-router/src/components/mode-toggle.tsx +37 -0
  253. package/templates/frontend/react/tanstack-router/src/components/theme-provider.tsx +73 -0
  254. package/templates/frontend/react/tanstack-router/src/main.tsx.hbs +66 -0
  255. package/templates/frontend/react/tanstack-router/src/routes/__root.tsx.hbs +100 -0
  256. package/templates/frontend/react/tanstack-router/src/routes/index.tsx.hbs +74 -0
  257. package/templates/frontend/react/tanstack-router/tsconfig.json.hbs +23 -0
  258. package/templates/frontend/react/tanstack-router/vite.config.ts.hbs +39 -0
  259. package/templates/frontend/react/tanstack-start/package.json.hbs +44 -0
  260. package/templates/frontend/react/tanstack-start/public/robots.txt +3 -0
  261. package/templates/frontend/react/tanstack-start/src/router.tsx.hbs +144 -0
  262. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +97 -0
  263. package/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs +74 -0
  264. package/templates/frontend/react/tanstack-start/tsconfig.json.hbs +33 -0
  265. package/templates/frontend/react/tanstack-start/vite.config.ts +8 -0
  266. package/templates/frontend/react/web-base/_gitignore +52 -0
  267. package/templates/frontend/react/web-base/components.json +21 -0
  268. package/templates/frontend/react/web-base/src/components/header.tsx.hbs +79 -0
  269. package/templates/frontend/react/web-base/src/components/loader.tsx +9 -0
  270. package/templates/frontend/react/web-base/src/components/ui/button.tsx +59 -0
  271. package/templates/frontend/react/web-base/src/components/ui/card.tsx +92 -0
  272. package/templates/frontend/react/web-base/src/components/ui/checkbox.tsx +30 -0
  273. package/templates/frontend/react/web-base/src/components/ui/dropdown-menu.tsx +257 -0
  274. package/templates/frontend/react/web-base/src/components/ui/input.tsx +21 -0
  275. package/templates/frontend/react/web-base/src/components/ui/label.tsx +22 -0
  276. package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx +13 -0
  277. package/templates/frontend/react/web-base/src/components/ui/sonner.tsx +25 -0
  278. package/templates/frontend/react/web-base/src/index.css +134 -0
  279. package/templates/frontend/react/web-base/src/lib/utils.ts +6 -0
  280. package/templates/frontend/solid/_gitignore +7 -0
  281. package/templates/frontend/solid/index.html +13 -0
  282. package/templates/frontend/solid/package.json.hbs +27 -0
  283. package/templates/frontend/solid/public/robots.txt +3 -0
  284. package/templates/frontend/solid/src/components/header.tsx.hbs +38 -0
  285. package/templates/frontend/solid/src/components/loader.tsx +9 -0
  286. package/templates/frontend/solid/src/main.tsx.hbs +38 -0
  287. package/templates/frontend/solid/src/routes/__root.tsx.hbs +34 -0
  288. package/templates/frontend/solid/src/routes/index.tsx.hbs +61 -0
  289. package/templates/frontend/solid/src/styles.css +5 -0
  290. package/templates/frontend/solid/tsconfig.json.hbs +34 -0
  291. package/templates/frontend/solid/vite.config.js.hbs +39 -0
  292. package/templates/frontend/svelte/_gitignore +23 -0
  293. package/templates/frontend/svelte/_npmrc +1 -0
  294. package/templates/frontend/svelte/package.json.hbs +31 -0
  295. package/templates/frontend/svelte/src/app.css +5 -0
  296. package/templates/frontend/svelte/src/app.d.ts +13 -0
  297. package/templates/frontend/svelte/src/app.html +12 -0
  298. package/templates/frontend/svelte/src/components/Header.svelte.hbs +40 -0
  299. package/templates/frontend/svelte/src/lib/index.ts +1 -0
  300. package/templates/frontend/svelte/src/routes/+layout.svelte.hbs +54 -0
  301. package/templates/frontend/svelte/src/routes/+page.svelte.hbs +72 -0
  302. package/templates/frontend/svelte/static/favicon.png +0 -0
  303. package/templates/frontend/svelte/svelte.config.js +18 -0
  304. package/templates/frontend/svelte/tsconfig.json.hbs +24 -0
  305. package/templates/frontend/svelte/vite.config.ts +7 -0
  306. package/templates/runtime/workers/apps/server/wrangler.jsonc.hbs +34 -0
package/dist/index.js ADDED
@@ -0,0 +1,4315 @@
1
+ #!/usr/bin/env node
2
+ import { cancel, confirm, group, intro, isCancel, log, multiselect, outro, password, select, spinner, text } from "@clack/prompts";
3
+ import consola, { consola as consola$1 } from "consola";
4
+ import pc from "picocolors";
5
+ import { createCli, trpcServer, zod } from "trpc-cli";
6
+ import path from "node:path";
7
+ import fs from "fs-extra";
8
+ import { fileURLToPath } from "node:url";
9
+ import { z } from "zod";
10
+ import { PostHog } from "posthog-node";
11
+ import gradient from "gradient-string";
12
+ import * as JSONC from "jsonc-parser";
13
+ import { $, execa } from "execa";
14
+ import { globby } from "globby";
15
+ import handlebars from "handlebars";
16
+ import os from "node:os";
17
+
18
+ //#region src/utils/get-package-manager.ts
19
+ const getUserPkgManager = () => {
20
+ const userAgent = process.env.npm_config_user_agent;
21
+ if (userAgent?.startsWith("pnpm")) return "pnpm";
22
+ if (userAgent?.startsWith("bun")) return "bun";
23
+ return "npm";
24
+ };
25
+
26
+ //#endregion
27
+ //#region src/constants.ts
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const distPath = path.dirname(__filename);
30
+ const PKG_ROOT = path.join(distPath, "../");
31
+ const DEFAULT_CONFIG = {
32
+ projectName: "my-tvi-app",
33
+ projectDir: path.resolve(process.cwd(), "my-tvi-app"),
34
+ relativePath: "my-tvi-app",
35
+ frontend: ["tanstack-router"],
36
+ database: "sqlite",
37
+ orm: "drizzle",
38
+ auth: true,
39
+ addons: ["turborepo"],
40
+ examples: [],
41
+ git: true,
42
+ packageManager: getUserPkgManager(),
43
+ install: true,
44
+ dbSetup: "none",
45
+ backend: "hono",
46
+ runtime: "bun",
47
+ api: "trpc"
48
+ };
49
+ const dependencyVersionMap = {
50
+ "better-auth": "^1.2.10",
51
+ "@better-auth/expo": "^1.2.10",
52
+ "drizzle-orm": "^0.44.2",
53
+ "drizzle-kit": "^0.31.2",
54
+ "@libsql/client": "^0.15.9",
55
+ pg: "^8.14.1",
56
+ "@types/pg": "^8.11.11",
57
+ mysql2: "^3.14.0",
58
+ "@prisma/client": "^6.9.0",
59
+ prisma: "^6.9.0",
60
+ mongoose: "^8.14.0",
61
+ "vite-plugin-pwa": "^0.21.2",
62
+ "@vite-pwa/assets-generator": "^0.2.6",
63
+ "@tauri-apps/cli": "^2.4.0",
64
+ "@biomejs/biome": "^2.0.0",
65
+ husky: "^9.1.7",
66
+ "lint-staged": "^15.5.0",
67
+ tsx: "^4.19.2",
68
+ "@types/node": "^22.13.11",
69
+ "@types/bun": "^1.2.6",
70
+ "@elysiajs/node": "^1.2.6",
71
+ "@elysiajs/cors": "^1.2.0",
72
+ "@elysiajs/trpc": "^1.1.0",
73
+ elysia: "^1.2.25",
74
+ "@hono/node-server": "^1.14.4",
75
+ "@hono/trpc-server": "^0.4.0",
76
+ hono: "^4.8.2",
77
+ cors: "^2.8.5",
78
+ express: "^5.1.0",
79
+ "@types/express": "^5.0.1",
80
+ "@types/cors": "^2.8.17",
81
+ fastify: "^5.3.3",
82
+ "@fastify/cors": "^11.0.1",
83
+ turbo: "^2.5.4",
84
+ ai: "^4.3.16",
85
+ "@ai-sdk/google": "^1.2.3",
86
+ "@ai-sdk/vue": "^1.2.8",
87
+ "@ai-sdk/svelte": "^2.1.9",
88
+ "@ai-sdk/react": "^1.2.12",
89
+ "@prisma/extension-accelerate": "^1.3.0",
90
+ "@orpc/server": "^1.5.0",
91
+ "@orpc/client": "^1.5.0",
92
+ "@orpc/tanstack-query": "^1.5.0",
93
+ "@trpc/tanstack-react-query": "^11.4.2",
94
+ "@trpc/server": "^11.4.2",
95
+ "@trpc/client": "^11.4.2",
96
+ convex: "^1.23.0",
97
+ "@convex-dev/react-query": "^0.0.0-alpha.8",
98
+ "convex-svelte": "^0.0.11",
99
+ "@tanstack/svelte-query": "^5.74.4",
100
+ "@tanstack/react-query-devtools": "^5.80.5",
101
+ "@tanstack/react-query": "^5.80.5",
102
+ "@tanstack/solid-query": "^5.75.0",
103
+ "@tanstack/solid-query-devtools": "^5.75.0",
104
+ wrangler: "^4.20.0"
105
+ };
106
+ const ADDON_COMPATIBILITY = {
107
+ pwa: [
108
+ "tanstack-router",
109
+ "react-router",
110
+ "solid"
111
+ ],
112
+ tauri: [
113
+ "tanstack-router",
114
+ "react-router",
115
+ "nuxt",
116
+ "svelte",
117
+ "solid"
118
+ ],
119
+ biome: [],
120
+ husky: [],
121
+ turborepo: [],
122
+ starlight: [],
123
+ none: []
124
+ };
125
+
126
+ //#endregion
127
+ //#region src/types.ts
128
+ const DatabaseSchema = z.enum([
129
+ "none",
130
+ "sqlite",
131
+ "postgres",
132
+ "mysql",
133
+ "mongodb"
134
+ ]).describe("Database type");
135
+ const ORMSchema = z.enum([
136
+ "drizzle",
137
+ "prisma",
138
+ "mongoose",
139
+ "none"
140
+ ]).describe("ORM type");
141
+ const BackendSchema = z.enum([
142
+ "hono",
143
+ "express",
144
+ "fastify",
145
+ "next",
146
+ "elysia",
147
+ "convex",
148
+ "none"
149
+ ]).describe("Backend framework");
150
+ const RuntimeSchema = z.enum([
151
+ "bun",
152
+ "node",
153
+ "workers",
154
+ "none"
155
+ ]).describe("Runtime environment (workers only available with hono backend and drizzle orm)");
156
+ const FrontendSchema = z.enum([
157
+ "tanstack-router",
158
+ "react-router",
159
+ "tanstack-start",
160
+ "next",
161
+ "nuxt",
162
+ "native-nativewind",
163
+ "native-unistyles",
164
+ "svelte",
165
+ "solid",
166
+ "none"
167
+ ]).describe("Frontend framework");
168
+ const AddonsSchema = z.enum([
169
+ "pwa",
170
+ "tauri",
171
+ "starlight",
172
+ "biome",
173
+ "husky",
174
+ "turborepo",
175
+ "none"
176
+ ]).describe("Additional addons");
177
+ const ExamplesSchema = z.enum([
178
+ "todo",
179
+ "ai",
180
+ "none"
181
+ ]).describe("Example templates to include");
182
+ const PackageManagerSchema = z.enum([
183
+ "npm",
184
+ "pnpm",
185
+ "bun"
186
+ ]).describe("Package manager");
187
+ const DatabaseSetupSchema = z.enum([
188
+ "turso",
189
+ "neon",
190
+ "prisma-postgres",
191
+ "mongodb-atlas",
192
+ "supabase",
193
+ "d1",
194
+ "none"
195
+ ]).describe("Database hosting setup");
196
+ const APISchema = z.enum([
197
+ "trpc",
198
+ "orpc",
199
+ "none"
200
+ ]).describe("API type");
201
+ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(255, "Project name must be less than 255 characters").refine((name) => name === "." || !name.startsWith("."), "Project name cannot start with a dot (except for '.')").refine((name) => name === "." || !name.startsWith("-"), "Project name cannot start with a dash").refine((name) => {
202
+ const invalidChars = [
203
+ "<",
204
+ ">",
205
+ ":",
206
+ "\"",
207
+ "|",
208
+ "?",
209
+ "*"
210
+ ];
211
+ return !invalidChars.some((char) => name.includes(char));
212
+ }, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
213
+
214
+ //#endregion
215
+ //#region src/utils/addon-compatibility.ts
216
+ function validateAddonCompatibility(addon, frontend) {
217
+ const compatibleFrontends = ADDON_COMPATIBILITY[addon];
218
+ if (compatibleFrontends.length === 0) return { isCompatible: true };
219
+ const hasCompatibleFrontend = frontend.some((f) => compatibleFrontends.includes(f));
220
+ if (!hasCompatibleFrontend) {
221
+ const frontendList = compatibleFrontends.join(", ");
222
+ return {
223
+ isCompatible: false,
224
+ reason: `${addon} addon requires one of these frontends: ${frontendList}`
225
+ };
226
+ }
227
+ return { isCompatible: true };
228
+ }
229
+ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
230
+ return allAddons.filter((addon) => {
231
+ if (existingAddons.includes(addon)) return false;
232
+ if (addon === "none") return false;
233
+ const { isCompatible } = validateAddonCompatibility(addon, frontend);
234
+ return isCompatible;
235
+ });
236
+ }
237
+
238
+ //#endregion
239
+ //#region src/prompts/addons.ts
240
+ function getAddonDisplay(addon, isRecommended = false) {
241
+ let label;
242
+ let hint;
243
+ if (addon === "turborepo") {
244
+ label = isRecommended ? "Turborepo (Recommended)" : "Turborepo";
245
+ hint = "High-performance build system for JavaScript and TypeScript";
246
+ } else if (addon === "pwa") {
247
+ label = "PWA (Progressive Web App)";
248
+ hint = "Make your app installable and work offline";
249
+ } else if (addon === "tauri") {
250
+ label = isRecommended ? "Tauri Desktop App" : "Tauri";
251
+ hint = "Build native desktop apps from your web frontend";
252
+ } else if (addon === "biome") {
253
+ label = "Biome";
254
+ hint = isRecommended ? "Add Biome for linting and formatting" : "Fast formatter and linter for JavaScript, TypeScript, JSX";
255
+ } else if (addon === "husky") {
256
+ label = "Husky";
257
+ hint = isRecommended ? "Add Git hooks with Husky, lint-staged (requires Biome)" : "Git hooks made easy";
258
+ } else if (addon === "starlight") {
259
+ label = "Starlight";
260
+ hint = isRecommended ? "Add Astro Starlight documentation site" : "Documentation site with Astro";
261
+ } else {
262
+ label = addon;
263
+ hint = `Add ${addon}`;
264
+ }
265
+ return {
266
+ label,
267
+ hint
268
+ };
269
+ }
270
+ async function getAddonsChoice(addons, frontends) {
271
+ if (addons !== void 0) return addons;
272
+ const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
273
+ const allPossibleOptions = [];
274
+ for (const addon of allAddons) {
275
+ const { isCompatible } = validateAddonCompatibility(addon, frontends || []);
276
+ if (isCompatible) {
277
+ const { label, hint } = getAddonDisplay(addon, true);
278
+ allPossibleOptions.push({
279
+ value: addon,
280
+ label,
281
+ hint
282
+ });
283
+ }
284
+ }
285
+ const options = allPossibleOptions.sort((a, b) => {
286
+ if (a.value === "turborepo") return -1;
287
+ if (b.value === "turborepo") return 1;
288
+ return 0;
289
+ });
290
+ const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => options.some((opt) => opt.value === addonValue));
291
+ const response = await multiselect({
292
+ message: "Select addons",
293
+ options,
294
+ initialValues,
295
+ required: false
296
+ });
297
+ if (isCancel(response)) {
298
+ cancel(pc.red("Operation cancelled"));
299
+ process.exit(0);
300
+ }
301
+ if (response.includes("husky") && !response.includes("biome")) response.push("biome");
302
+ return response;
303
+ }
304
+ async function getAddonsToAdd(frontend, existingAddons = []) {
305
+ const options = [];
306
+ const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
307
+ const compatibleAddons = getCompatibleAddons(allAddons, frontend, existingAddons);
308
+ for (const addon of compatibleAddons) {
309
+ const { label, hint } = getAddonDisplay(addon, false);
310
+ options.push({
311
+ value: addon,
312
+ label,
313
+ hint
314
+ });
315
+ }
316
+ if (options.length === 0) return [];
317
+ const response = await multiselect({
318
+ message: "Select addons",
319
+ options,
320
+ required: true
321
+ });
322
+ if (isCancel(response)) {
323
+ cancel(pc.red("Operation cancelled"));
324
+ process.exit(0);
325
+ }
326
+ return response;
327
+ }
328
+
329
+ //#endregion
330
+ //#region src/prompts/api.ts
331
+ async function getApiChoice(Api, frontend, backend) {
332
+ if (backend === "convex" || backend === "none") return "none";
333
+ if (Api) return Api;
334
+ const includesNuxt = frontend?.includes("nuxt");
335
+ const includesSvelte = frontend?.includes("svelte");
336
+ const includesSolid = frontend?.includes("solid");
337
+ let apiOptions = [
338
+ {
339
+ value: "trpc",
340
+ label: "tRPC",
341
+ hint: "End-to-end typesafe APIs made easy"
342
+ },
343
+ {
344
+ value: "orpc",
345
+ label: "oRPC",
346
+ hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
347
+ },
348
+ {
349
+ value: "none",
350
+ label: "None",
351
+ hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
352
+ }
353
+ ];
354
+ if (includesNuxt || includesSvelte || includesSolid) apiOptions = [{
355
+ value: "orpc",
356
+ label: "oRPC",
357
+ hint: `End-to-end type-safe APIs (Recommended for ${includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"} frontend)`
358
+ }, {
359
+ value: "none",
360
+ label: "None",
361
+ hint: "No API layer"
362
+ }];
363
+ const apiType = await select({
364
+ message: "Select API type",
365
+ options: apiOptions,
366
+ initialValue: apiOptions[0].value
367
+ });
368
+ if (isCancel(apiType)) {
369
+ cancel(pc.red("Operation cancelled"));
370
+ process.exit(0);
371
+ }
372
+ return apiType;
373
+ }
374
+
375
+ //#endregion
376
+ //#region src/prompts/auth.ts
377
+ async function getAuthChoice(auth, hasDatabase, backend) {
378
+ if (backend === "convex") return false;
379
+ if (!hasDatabase) return false;
380
+ if (auth !== void 0) return auth;
381
+ const response = await confirm({
382
+ message: "Add authentication with Better-Auth?",
383
+ initialValue: DEFAULT_CONFIG.auth
384
+ });
385
+ if (isCancel(response)) {
386
+ cancel(pc.red("Operation cancelled"));
387
+ process.exit(0);
388
+ }
389
+ return response;
390
+ }
391
+
392
+ //#endregion
393
+ //#region src/prompts/backend.ts
394
+ async function getBackendFrameworkChoice(backendFramework, frontends) {
395
+ if (backendFramework !== void 0) return backendFramework;
396
+ const hasIncompatibleFrontend = frontends?.some((f) => f === "nuxt" || f === "solid");
397
+ const backendOptions = [
398
+ {
399
+ value: "hono",
400
+ label: "Hono",
401
+ hint: "Lightweight, ultrafast web framework"
402
+ },
403
+ {
404
+ value: "next",
405
+ label: "Next.js",
406
+ hint: "separate api routes only backend"
407
+ },
408
+ {
409
+ value: "express",
410
+ label: "Express",
411
+ hint: "Fast, unopinionated, minimalist web framework for Node.js"
412
+ },
413
+ {
414
+ value: "fastify",
415
+ label: "Fastify",
416
+ hint: "Fast, low-overhead web framework for Node.js"
417
+ },
418
+ {
419
+ value: "elysia",
420
+ label: "Elysia",
421
+ hint: "Ergonomic web framework for building backend servers"
422
+ }
423
+ ];
424
+ if (!hasIncompatibleFrontend) backendOptions.push({
425
+ value: "convex",
426
+ label: "Convex",
427
+ hint: "Reactive backend-as-a-service platform"
428
+ });
429
+ backendOptions.push({
430
+ value: "none",
431
+ label: "None",
432
+ hint: "No backend server"
433
+ });
434
+ let initialValue = DEFAULT_CONFIG.backend;
435
+ if (hasIncompatibleFrontend && initialValue === "convex") initialValue = "hono";
436
+ const response = await select({
437
+ message: "Select backend",
438
+ options: backendOptions,
439
+ initialValue
440
+ });
441
+ if (isCancel(response)) {
442
+ cancel(pc.red("Operation cancelled"));
443
+ process.exit(0);
444
+ }
445
+ return response;
446
+ }
447
+
448
+ //#endregion
449
+ //#region src/prompts/database.ts
450
+ async function getDatabaseChoice(database, backend, runtime) {
451
+ if (backend === "convex" || backend === "none") return "none";
452
+ if (database !== void 0) return database;
453
+ const databaseOptions = [
454
+ {
455
+ value: "none",
456
+ label: "None",
457
+ hint: "No database setup"
458
+ },
459
+ {
460
+ value: "sqlite",
461
+ label: "SQLite",
462
+ hint: "lightweight, server-less, embedded relational database"
463
+ },
464
+ {
465
+ value: "postgres",
466
+ label: "PostgreSQL",
467
+ hint: "powerful, open source object-relational database system"
468
+ },
469
+ {
470
+ value: "mysql",
471
+ label: "MySQL",
472
+ hint: "popular open-source relational database system"
473
+ }
474
+ ];
475
+ if (runtime !== "workers") databaseOptions.push({
476
+ value: "mongodb",
477
+ label: "MongoDB",
478
+ hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
479
+ });
480
+ const response = await select({
481
+ message: "Select database",
482
+ options: databaseOptions,
483
+ initialValue: DEFAULT_CONFIG.database
484
+ });
485
+ if (isCancel(response)) {
486
+ cancel(pc.red("Operation cancelled"));
487
+ process.exit(0);
488
+ }
489
+ return response;
490
+ }
491
+
492
+ //#endregion
493
+ //#region src/prompts/database-setup.ts
494
+ async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
495
+ if (backend === "convex") return "none";
496
+ if (dbSetup !== void 0) return dbSetup;
497
+ if (databaseType === "none") return "none";
498
+ if (databaseType === "sqlite" && orm === "prisma") return "none";
499
+ let options = [];
500
+ if (databaseType === "sqlite") options = [
501
+ {
502
+ value: "turso",
503
+ label: "Turso",
504
+ hint: "SQLite for Production. Powered by libSQL"
505
+ },
506
+ ...runtime === "workers" ? [{
507
+ value: "d1",
508
+ label: "Cloudflare D1",
509
+ hint: "Cloudflare's managed, serverless database with SQLite's SQL semantics"
510
+ }] : [],
511
+ {
512
+ value: "none",
513
+ label: "None",
514
+ hint: "Manual setup"
515
+ }
516
+ ];
517
+ else if (databaseType === "postgres") options = [
518
+ {
519
+ value: "neon",
520
+ label: "Neon Postgres",
521
+ hint: "Serverless Postgres with branching capability"
522
+ },
523
+ {
524
+ value: "supabase",
525
+ label: "Supabase",
526
+ hint: "Local Supabase stack (requires Docker)"
527
+ },
528
+ ...orm === "prisma" ? [{
529
+ value: "prisma-postgres",
530
+ label: "Prisma Postgres",
531
+ hint: "Instant Postgres for Global Applications"
532
+ }] : [],
533
+ {
534
+ value: "none",
535
+ label: "None",
536
+ hint: "Manual setup"
537
+ }
538
+ ];
539
+ else if (databaseType === "mongodb") options = [{
540
+ value: "mongodb-atlas",
541
+ label: "MongoDB Atlas",
542
+ hint: "The most effective way to deploy MongoDB"
543
+ }, {
544
+ value: "none",
545
+ label: "None",
546
+ hint: "Manual setup"
547
+ }];
548
+ else return "none";
549
+ const response = await select({
550
+ message: `Select ${databaseType} setup option`,
551
+ options,
552
+ initialValue: "none"
553
+ });
554
+ if (isCancel(response)) {
555
+ cancel(pc.red("Operation cancelled"));
556
+ process.exit(0);
557
+ }
558
+ return response;
559
+ }
560
+
561
+ //#endregion
562
+ //#region src/prompts/examples.ts
563
+ async function getExamplesChoice(examples, database, frontends, backend, api) {
564
+ if (api === "none") return [];
565
+ if (examples !== void 0) return examples;
566
+ if (backend === "convex") return ["todo"];
567
+ if (backend === "none") return [];
568
+ if (database === "none") return [];
569
+ const noFrontendSelected = !frontends || frontends.length === 0;
570
+ if (noFrontendSelected) return [];
571
+ let response = [];
572
+ const options = [{
573
+ value: "todo",
574
+ label: "Todo App",
575
+ hint: "A simple CRUD example app"
576
+ }];
577
+ if (backend !== "elysia" && !frontends?.includes("solid")) options.push({
578
+ value: "ai",
579
+ label: "AI Chat",
580
+ hint: "A simple AI chat interface using AI SDK"
581
+ });
582
+ response = await multiselect({
583
+ message: "Include examples",
584
+ options,
585
+ required: false,
586
+ initialValues: DEFAULT_CONFIG.examples
587
+ });
588
+ if (isCancel(response)) {
589
+ cancel(pc.red("Operation cancelled"));
590
+ process.exit(0);
591
+ }
592
+ return response;
593
+ }
594
+
595
+ //#endregion
596
+ //#region src/prompts/frontend.ts
597
+ async function getFrontendChoice(frontendOptions, backend) {
598
+ if (frontendOptions !== void 0) return frontendOptions;
599
+ const frontendTypes = await multiselect({
600
+ message: "Select platforms to develop for",
601
+ options: [{
602
+ value: "web",
603
+ label: "Web",
604
+ hint: "React, Vue or Svelte Web Application"
605
+ }, {
606
+ value: "native",
607
+ label: "Native",
608
+ hint: "Create a React Native/Expo app"
609
+ }],
610
+ required: false,
611
+ initialValues: ["web"]
612
+ });
613
+ if (isCancel(frontendTypes)) {
614
+ cancel(pc.red("Operation cancelled"));
615
+ process.exit(0);
616
+ }
617
+ const result = [];
618
+ if (frontendTypes.includes("web")) {
619
+ const allWebOptions = [
620
+ {
621
+ value: "tanstack-router",
622
+ label: "TanStack Router",
623
+ hint: "Modern and scalable routing for React Applications"
624
+ },
625
+ {
626
+ value: "react-router",
627
+ label: "React Router",
628
+ hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
629
+ },
630
+ {
631
+ value: "next",
632
+ label: "Next.js",
633
+ hint: "The React Framework for the Web"
634
+ },
635
+ {
636
+ value: "nuxt",
637
+ label: "Nuxt",
638
+ hint: "The Progressive Web Framework for Vue.js"
639
+ },
640
+ {
641
+ value: "svelte",
642
+ label: "Svelte",
643
+ hint: "web development for the rest of us"
644
+ },
645
+ {
646
+ value: "solid",
647
+ label: "Solid",
648
+ hint: "Simple and performant reactivity for building user interfaces"
649
+ },
650
+ {
651
+ value: "tanstack-start",
652
+ label: "TanStack Start (devinxi)",
653
+ hint: "SSR, Server Functions, API Routes and more with TanStack Router"
654
+ }
655
+ ];
656
+ const webOptions = allWebOptions.filter((option) => {
657
+ if (backend === "convex") return option.value !== "nuxt" && option.value !== "solid";
658
+ return true;
659
+ });
660
+ const webFramework = await select({
661
+ message: "Choose web",
662
+ options: webOptions,
663
+ initialValue: DEFAULT_CONFIG.frontend[0]
664
+ });
665
+ if (isCancel(webFramework)) {
666
+ cancel(pc.red("Operation cancelled"));
667
+ process.exit(0);
668
+ }
669
+ result.push(webFramework);
670
+ }
671
+ if (frontendTypes.includes("native")) {
672
+ const nativeFramework = await select({
673
+ message: "Choose native",
674
+ options: [{
675
+ value: "native-nativewind",
676
+ label: "NativeWind",
677
+ hint: "Use Tailwind CSS for React Native"
678
+ }, {
679
+ value: "native-unistyles",
680
+ label: "Unistyles",
681
+ hint: "Consistent styling for React Native"
682
+ }],
683
+ initialValue: "native-nativewind"
684
+ });
685
+ if (isCancel(nativeFramework)) {
686
+ cancel(pc.red("Operation cancelled"));
687
+ process.exit(0);
688
+ }
689
+ result.push(nativeFramework);
690
+ }
691
+ return result;
692
+ }
693
+
694
+ //#endregion
695
+ //#region src/prompts/git.ts
696
+ async function getGitChoice(git) {
697
+ if (git !== void 0) return git;
698
+ const response = await confirm({
699
+ message: "Initialize git repository?",
700
+ initialValue: DEFAULT_CONFIG.git
701
+ });
702
+ if (isCancel(response)) {
703
+ cancel(pc.red("Operation cancelled"));
704
+ process.exit(0);
705
+ }
706
+ return response;
707
+ }
708
+
709
+ //#endregion
710
+ //#region src/prompts/install.ts
711
+ async function getinstallChoice(install) {
712
+ if (install !== void 0) return install;
713
+ const response = await confirm({
714
+ message: "Install dependencies?",
715
+ initialValue: DEFAULT_CONFIG.install
716
+ });
717
+ if (isCancel(response)) {
718
+ cancel(pc.red("Operation cancelled"));
719
+ process.exit(0);
720
+ }
721
+ return response;
722
+ }
723
+
724
+ //#endregion
725
+ //#region src/prompts/orm.ts
726
+ const ormOptions = {
727
+ prisma: {
728
+ value: "prisma",
729
+ label: "Prisma",
730
+ hint: "Powerful, feature-rich ORM"
731
+ },
732
+ mongoose: {
733
+ value: "mongoose",
734
+ label: "Mongoose",
735
+ hint: "Elegant object modeling tool"
736
+ },
737
+ drizzle: {
738
+ value: "drizzle",
739
+ label: "Drizzle",
740
+ hint: "Lightweight and performant TypeScript ORM"
741
+ }
742
+ };
743
+ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
744
+ if (backend === "convex") return "none";
745
+ if (!hasDatabase) return "none";
746
+ if (orm !== void 0) return orm;
747
+ if (runtime === "workers") return "drizzle";
748
+ const options = [...database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma]];
749
+ const response = await select({
750
+ message: "Select ORM",
751
+ options,
752
+ initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
753
+ });
754
+ if (isCancel(response)) {
755
+ cancel(pc.red("Operation cancelled"));
756
+ process.exit(0);
757
+ }
758
+ return response;
759
+ }
760
+
761
+ //#endregion
762
+ //#region src/prompts/package-manager.ts
763
+ async function getPackageManagerChoice(packageManager) {
764
+ if (packageManager !== void 0) return packageManager;
765
+ const detectedPackageManager = getUserPkgManager();
766
+ const response = await select({
767
+ message: "Choose package manager",
768
+ options: [
769
+ {
770
+ value: "npm",
771
+ label: "npm",
772
+ hint: "Node Package Manager"
773
+ },
774
+ {
775
+ value: "pnpm",
776
+ label: "pnpm",
777
+ hint: "Fast, disk space efficient package manager"
778
+ },
779
+ {
780
+ value: "bun",
781
+ label: "bun",
782
+ hint: "All-in-one JavaScript runtime & toolkit"
783
+ }
784
+ ],
785
+ initialValue: detectedPackageManager
786
+ });
787
+ if (isCancel(response)) {
788
+ cancel(pc.red("Operation cancelled"));
789
+ process.exit(0);
790
+ }
791
+ return response;
792
+ }
793
+
794
+ //#endregion
795
+ //#region src/prompts/runtime.ts
796
+ async function getRuntimeChoice(runtime, backend) {
797
+ if (backend === "convex" || backend === "none") return "none";
798
+ if (runtime !== void 0) return runtime;
799
+ if (backend === "next") return "node";
800
+ const runtimeOptions = [{
801
+ value: "bun",
802
+ label: "Bun",
803
+ hint: "Fast all-in-one JavaScript runtime"
804
+ }, {
805
+ value: "node",
806
+ label: "Node.js",
807
+ hint: "Traditional Node.js runtime"
808
+ }];
809
+ if (backend === "hono") runtimeOptions.push({
810
+ value: "workers",
811
+ label: "Cloudflare Workers (beta)",
812
+ hint: "Edge runtime on Cloudflare's global network"
813
+ });
814
+ const response = await select({
815
+ message: "Select runtime",
816
+ options: runtimeOptions,
817
+ initialValue: DEFAULT_CONFIG.runtime
818
+ });
819
+ if (isCancel(response)) {
820
+ cancel(pc.red("Operation cancelled"));
821
+ process.exit(0);
822
+ }
823
+ return response;
824
+ }
825
+
826
+ //#endregion
827
+ //#region src/prompts/config-prompts.ts
828
+ async function gatherConfig(flags, projectName, projectDir, relativePath) {
829
+ const result = await group({
830
+ frontend: () => getFrontendChoice(flags.frontend, flags.backend),
831
+ backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
832
+ runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
833
+ database: ({ results }) => getDatabaseChoice(flags.database, results.backend, results.runtime),
834
+ orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
835
+ api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
836
+ auth: ({ results }) => getAuthChoice(flags.auth, results.database !== "none", results.backend),
837
+ addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
838
+ examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
839
+ dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
840
+ git: () => getGitChoice(flags.git),
841
+ packageManager: () => getPackageManagerChoice(flags.packageManager),
842
+ install: () => getinstallChoice(flags.install)
843
+ }, { onCancel: () => {
844
+ cancel(pc.red("Operation cancelled"));
845
+ process.exit(0);
846
+ } });
847
+ if (result.backend === "convex") {
848
+ result.runtime = "none";
849
+ result.database = "none";
850
+ result.orm = "none";
851
+ result.api = "none";
852
+ result.auth = false;
853
+ result.dbSetup = "none";
854
+ result.examples = ["todo"];
855
+ }
856
+ if (result.backend === "none") {
857
+ result.runtime = "none";
858
+ result.database = "none";
859
+ result.orm = "none";
860
+ result.api = "none";
861
+ result.auth = false;
862
+ result.dbSetup = "none";
863
+ result.examples = [];
864
+ }
865
+ return {
866
+ projectName,
867
+ projectDir,
868
+ relativePath,
869
+ frontend: result.frontend,
870
+ backend: result.backend,
871
+ runtime: result.runtime,
872
+ database: result.database,
873
+ orm: result.orm,
874
+ auth: result.auth,
875
+ addons: result.addons,
876
+ examples: result.examples,
877
+ git: result.git,
878
+ packageManager: result.packageManager,
879
+ install: result.install,
880
+ dbSetup: result.dbSetup,
881
+ api: result.api
882
+ };
883
+ }
884
+
885
+ //#endregion
886
+ //#region src/prompts/project-name.ts
887
+ function validateDirectoryName(name) {
888
+ if (name === ".") return void 0;
889
+ const result = ProjectNameSchema.safeParse(name);
890
+ if (!result.success) return result.error.issues[0]?.message || "Invalid project name";
891
+ return void 0;
892
+ }
893
+ async function getProjectName(initialName) {
894
+ if (initialName) {
895
+ if (initialName === ".") return initialName;
896
+ const finalDirName = path.basename(initialName);
897
+ const validationError = validateDirectoryName(finalDirName);
898
+ if (!validationError) return initialName;
899
+ }
900
+ let isValid = false;
901
+ let projectPath = "";
902
+ let defaultName = DEFAULT_CONFIG.projectName;
903
+ let counter = 1;
904
+ while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0) {
905
+ defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
906
+ counter++;
907
+ }
908
+ while (!isValid) {
909
+ const response = await text({
910
+ message: "Enter your project name or path (relative to current directory)",
911
+ placeholder: defaultName,
912
+ initialValue: initialName,
913
+ defaultValue: defaultName,
914
+ validate: (value) => {
915
+ const nameToUse = value.trim() || defaultName;
916
+ const finalDirName = path.basename(nameToUse);
917
+ const validationError = validateDirectoryName(finalDirName);
918
+ if (validationError) return validationError;
919
+ if (nameToUse !== ".") {
920
+ const projectDir = path.resolve(process.cwd(), nameToUse);
921
+ if (!projectDir.startsWith(process.cwd())) return "Project path must be within current directory";
922
+ }
923
+ return void 0;
924
+ }
925
+ });
926
+ if (isCancel(response)) {
927
+ cancel(pc.red("Operation cancelled."));
928
+ process.exit(0);
929
+ }
930
+ projectPath = response || defaultName;
931
+ isValid = true;
932
+ }
933
+ return projectPath;
934
+ }
935
+
936
+ //#endregion
937
+ //#region src/utils/get-latest-cli-version.ts
938
+ const getLatestCLIVersion = () => {
939
+ const packageJsonPath = path.join(PKG_ROOT, "package.json");
940
+ const packageJsonContent = fs.readJSONSync(packageJsonPath);
941
+ return packageJsonContent.version ?? "1.0.0";
942
+ };
943
+
944
+ //#endregion
945
+ //#region src/utils/analytics.ts
946
+ const POSTHOG_API_KEY = "lol";
947
+ const POSTHOG_HOST = "lool";
948
+ async function trackProjectCreation(config) {
949
+ if (!POSTHOG_API_KEY) return;
950
+ const posthog = new PostHog(POSTHOG_API_KEY, {
951
+ host: POSTHOG_HOST,
952
+ flushAt: 1,
953
+ flushInterval: 0,
954
+ privacyMode: true,
955
+ disableGeoip: true,
956
+ disabled: true
957
+ });
958
+ try {
959
+ const sessionId = `cli_${crypto.randomUUID().replace(/-/g, "")}`;
960
+ const { projectName, projectDir, relativePath,...safeConfig } = config;
961
+ posthog.capture({
962
+ distinctId: sessionId,
963
+ event: "project_created",
964
+ properties: {
965
+ ...safeConfig,
966
+ cli_version: getLatestCLIVersion(),
967
+ node_version: process.version,
968
+ platform: process.platform,
969
+ $ip: null
970
+ }
971
+ });
972
+ } catch (_error) {} finally {
973
+ await posthog.shutdown();
974
+ }
975
+ }
976
+
977
+ //#endregion
978
+ //#region src/utils/display-config.ts
979
+ function displayConfig(config) {
980
+ const configDisplay = [];
981
+ if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
982
+ if (config.frontend !== void 0) {
983
+ const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
984
+ const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
985
+ configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
986
+ }
987
+ if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
988
+ if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
989
+ if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
990
+ if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
991
+ if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
992
+ if (config.auth !== void 0) {
993
+ const authText = typeof config.auth === "boolean" ? config.auth ? "Yes" : "No" : String(config.auth);
994
+ configDisplay.push(`${pc.blue("Authentication:")} ${authText}`);
995
+ }
996
+ if (config.addons !== void 0) {
997
+ const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
998
+ const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
999
+ configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
1000
+ }
1001
+ if (config.examples !== void 0) {
1002
+ const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
1003
+ const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
1004
+ configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
1005
+ }
1006
+ if (config.git !== void 0) {
1007
+ const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
1008
+ configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
1009
+ }
1010
+ if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
1011
+ if (config.install !== void 0) {
1012
+ const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
1013
+ configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
1014
+ }
1015
+ if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
1016
+ if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
1017
+ return configDisplay.join("\n");
1018
+ }
1019
+
1020
+ //#endregion
1021
+ //#region src/utils/generate-reproducible-command.ts
1022
+ function generateReproducibleCommand(config) {
1023
+ const flags = [];
1024
+ if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
1025
+ else flags.push("--frontend none");
1026
+ flags.push(`--backend ${config.backend}`);
1027
+ flags.push(`--runtime ${config.runtime}`);
1028
+ flags.push(`--database ${config.database}`);
1029
+ flags.push(`--orm ${config.orm}`);
1030
+ flags.push(`--api ${config.api}`);
1031
+ flags.push(config.auth ? "--auth" : "--no-auth");
1032
+ if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
1033
+ else flags.push("--addons none");
1034
+ if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
1035
+ else flags.push("--examples none");
1036
+ flags.push(`--db-setup ${config.dbSetup}`);
1037
+ flags.push(config.git ? "--git" : "--no-git");
1038
+ flags.push(`--package-manager ${config.packageManager}`);
1039
+ flags.push(config.install ? "--install" : "--no-install");
1040
+ let baseCommand = "";
1041
+ const pkgManager = config.packageManager;
1042
+ if (pkgManager === "npm") baseCommand = "npx tvi-cli@latest";
1043
+ else if (pkgManager === "pnpm") baseCommand = "pnpm create tvi@latest";
1044
+ else if (pkgManager === "bun") baseCommand = "bun create tvi@latest";
1045
+ const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
1046
+ return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
1047
+ }
1048
+
1049
+ //#endregion
1050
+ //#region src/utils/project-directory.ts
1051
+ async function handleDirectoryConflict(currentPathInput) {
1052
+ while (true) {
1053
+ const resolvedPath = path.resolve(process.cwd(), currentPathInput);
1054
+ const dirExists = fs.pathExistsSync(resolvedPath);
1055
+ const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
1056
+ if (!dirIsNotEmpty) return {
1057
+ finalPathInput: currentPathInput,
1058
+ shouldClearDirectory: false
1059
+ };
1060
+ log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
1061
+ const action = await select({
1062
+ message: "What would you like to do?",
1063
+ options: [
1064
+ {
1065
+ value: "overwrite",
1066
+ label: "Overwrite",
1067
+ hint: "Empty the directory and create the project"
1068
+ },
1069
+ {
1070
+ value: "merge",
1071
+ label: "Merge",
1072
+ hint: "Create project files inside, potentially overwriting conflicts"
1073
+ },
1074
+ {
1075
+ value: "rename",
1076
+ label: "Choose a different name/path",
1077
+ hint: "Keep the existing directory and create a new one"
1078
+ },
1079
+ {
1080
+ value: "cancel",
1081
+ label: "Cancel",
1082
+ hint: "Abort the process"
1083
+ }
1084
+ ],
1085
+ initialValue: "rename"
1086
+ });
1087
+ if (isCancel(action)) {
1088
+ cancel(pc.red("Operation cancelled."));
1089
+ process.exit(0);
1090
+ }
1091
+ switch (action) {
1092
+ case "overwrite": return {
1093
+ finalPathInput: currentPathInput,
1094
+ shouldClearDirectory: true
1095
+ };
1096
+ case "merge":
1097
+ log.info(`Proceeding into existing directory "${pc.yellow(currentPathInput)}". Files may be overwritten.`);
1098
+ return {
1099
+ finalPathInput: currentPathInput,
1100
+ shouldClearDirectory: false
1101
+ };
1102
+ case "rename": {
1103
+ log.info("Please choose a different project name or path.");
1104
+ const newPathInput = await getProjectName(void 0);
1105
+ return await handleDirectoryConflict(newPathInput);
1106
+ }
1107
+ case "cancel":
1108
+ cancel(pc.red("Operation cancelled."));
1109
+ process.exit(0);
1110
+ }
1111
+ }
1112
+ }
1113
+ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
1114
+ let finalResolvedPath;
1115
+ let finalBaseName;
1116
+ if (finalPathInput === ".") {
1117
+ finalResolvedPath = process.cwd();
1118
+ finalBaseName = path.basename(finalResolvedPath);
1119
+ } else {
1120
+ finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
1121
+ finalBaseName = path.basename(finalResolvedPath);
1122
+ }
1123
+ if (shouldClearDirectory) {
1124
+ const s = spinner();
1125
+ s.start(`Clearing directory "${finalResolvedPath}"...`);
1126
+ try {
1127
+ await fs.emptyDir(finalResolvedPath);
1128
+ s.stop(`Directory "${finalResolvedPath}" cleared.`);
1129
+ } catch (error) {
1130
+ s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
1131
+ consola$1.error(error);
1132
+ process.exit(1);
1133
+ }
1134
+ } else await fs.ensureDir(finalResolvedPath);
1135
+ return {
1136
+ finalResolvedPath,
1137
+ finalBaseName
1138
+ };
1139
+ }
1140
+
1141
+ //#endregion
1142
+ //#region src/utils/render-title.ts
1143
+ const TITLE_TEXT = `
1144
+ TVI TypeScript Initializer
1145
+ A CLI tool to quickly scaffold TypeScript projects with popular frameworks and libraries.
1146
+ `;
1147
+ const catppuccinTheme = {
1148
+ pink: "#F5C2E7",
1149
+ mauve: "#CBA6F7",
1150
+ red: "#F38BA8",
1151
+ maroon: "#E78284",
1152
+ peach: "#FAB387",
1153
+ yellow: "#F9E2AF",
1154
+ green: "#A6E3A1",
1155
+ teal: "#94E2D5",
1156
+ sky: "#89DCEB",
1157
+ sapphire: "#74C7EC",
1158
+ lavender: "#B4BEFE"
1159
+ };
1160
+ const renderTitle = () => {
1161
+ const terminalWidth = process.stdout.columns || 80;
1162
+ const titleLines = TITLE_TEXT.split("\n");
1163
+ const titleWidth = Math.max(...titleLines.map((line) => line.length));
1164
+ if (terminalWidth < titleWidth) {
1165
+ const simplifiedTitle = `
1166
+ ╔══════════════════╗
1167
+ ║ TVI ║
1168
+ ╚══════════════════╝
1169
+ `;
1170
+ console.log(gradient(Object.values(catppuccinTheme)).multiline(simplifiedTitle));
1171
+ } else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
1172
+ };
1173
+
1174
+ //#endregion
1175
+ //#region src/validation.ts
1176
+ function processAndValidateFlags(options, providedFlags, projectName) {
1177
+ const config = {};
1178
+ if (options.api) {
1179
+ config.api = options.api;
1180
+ if (options.api === "none") {
1181
+ if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") {
1182
+ consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1183
+ process.exit(1);
1184
+ }
1185
+ }
1186
+ }
1187
+ if (options.backend) config.backend = options.backend;
1188
+ if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
1189
+ if (providedFlags.has("runtime") && options.runtime === "none") {
1190
+ consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1191
+ process.exit(1);
1192
+ }
1193
+ }
1194
+ if (options.database) config.database = options.database;
1195
+ if (options.orm) config.orm = options.orm;
1196
+ if (options.auth !== void 0) config.auth = options.auth;
1197
+ if (options.git !== void 0) config.git = options.git;
1198
+ if (options.install !== void 0) config.install = options.install;
1199
+ if (options.runtime) config.runtime = options.runtime;
1200
+ if (options.dbSetup) config.dbSetup = options.dbSetup;
1201
+ if (options.packageManager) config.packageManager = options.packageManager;
1202
+ if (projectName) {
1203
+ const result = ProjectNameSchema.safeParse(path.basename(projectName));
1204
+ if (!result.success) {
1205
+ consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1206
+ process.exit(1);
1207
+ }
1208
+ config.projectName = projectName;
1209
+ } else if (options.projectDirectory) {
1210
+ const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
1211
+ const result = ProjectNameSchema.safeParse(baseName);
1212
+ if (!result.success) {
1213
+ consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1214
+ process.exit(1);
1215
+ }
1216
+ config.projectName = baseName;
1217
+ }
1218
+ if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
1219
+ if (options.frontend.length > 1) {
1220
+ consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
1221
+ process.exit(1);
1222
+ }
1223
+ config.frontend = [];
1224
+ } else {
1225
+ const validOptions = options.frontend.filter((f) => f !== "none");
1226
+ const webFrontends = validOptions.filter((f) => f === "tanstack-router" || f === "react-router" || f === "tanstack-start" || f === "next" || f === "nuxt" || f === "svelte" || f === "solid");
1227
+ const nativeFrontends = validOptions.filter((f) => f === "native-nativewind" || f === "native-unistyles");
1228
+ if (webFrontends.length > 1) {
1229
+ consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
1230
+ process.exit(1);
1231
+ }
1232
+ if (nativeFrontends.length > 1) {
1233
+ consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
1234
+ process.exit(1);
1235
+ }
1236
+ config.frontend = validOptions;
1237
+ }
1238
+ if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
1239
+ if (options.addons.length > 1) {
1240
+ consola$1.fatal(`Cannot combine 'none' with other addons.`);
1241
+ process.exit(1);
1242
+ }
1243
+ config.addons = [];
1244
+ } else config.addons = options.addons.filter((addon) => addon !== "none");
1245
+ if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
1246
+ if (options.examples.length > 1) {
1247
+ consola$1.fatal("Cannot combine 'none' with other examples.");
1248
+ process.exit(1);
1249
+ }
1250
+ config.examples = [];
1251
+ } else {
1252
+ config.examples = options.examples.filter((ex) => ex !== "none");
1253
+ if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
1254
+ }
1255
+ if (config.backend === "convex") {
1256
+ const incompatibleFlags = [];
1257
+ if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
1258
+ if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
1259
+ if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
1260
+ if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
1261
+ if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
1262
+ if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
1263
+ if (incompatibleFlags.length > 0) {
1264
+ consola$1.fatal(`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(", ")}. Please remove them.`);
1265
+ process.exit(1);
1266
+ }
1267
+ if (providedFlags.has("frontend") && options.frontend) {
1268
+ const incompatibleFrontends = options.frontend.filter((f) => f === "nuxt" || f === "solid");
1269
+ if (incompatibleFrontends.length > 0) {
1270
+ consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1271
+ process.exit(1);
1272
+ }
1273
+ }
1274
+ config.auth = false;
1275
+ config.database = "none";
1276
+ config.orm = "none";
1277
+ config.api = "none";
1278
+ config.runtime = "none";
1279
+ config.dbSetup = "none";
1280
+ config.examples = ["todo"];
1281
+ } else if (config.backend === "none") {
1282
+ const incompatibleFlags = [];
1283
+ if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
1284
+ if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
1285
+ if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
1286
+ if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
1287
+ if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
1288
+ if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
1289
+ if (providedFlags.has("examples") && options.examples) {
1290
+ const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
1291
+ if (hasNonNoneExamples) incompatibleFlags.push("--examples");
1292
+ }
1293
+ if (incompatibleFlags.length > 0) {
1294
+ consola$1.fatal(`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(", ")}. Please remove them.`);
1295
+ process.exit(1);
1296
+ }
1297
+ config.auth = false;
1298
+ config.database = "none";
1299
+ config.orm = "none";
1300
+ config.api = "none";
1301
+ config.runtime = "none";
1302
+ config.dbSetup = "none";
1303
+ config.examples = [];
1304
+ }
1305
+ if (config.orm === "mongoose" && config.database !== "mongodb") {
1306
+ consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1307
+ process.exit(1);
1308
+ }
1309
+ if (config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
1310
+ consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1311
+ process.exit(1);
1312
+ }
1313
+ if (config.orm === "drizzle" && config.database === "mongodb") {
1314
+ consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1315
+ process.exit(1);
1316
+ }
1317
+ if (config.database && config.database !== "none" && config.orm === "none") {
1318
+ consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1319
+ process.exit(1);
1320
+ }
1321
+ if (config.orm && config.orm !== "none" && config.database === "none") {
1322
+ consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
1323
+ process.exit(1);
1324
+ }
1325
+ if (config.auth && config.database === "none") {
1326
+ consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
1327
+ process.exit(1);
1328
+ }
1329
+ if (config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
1330
+ consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1331
+ process.exit(1);
1332
+ }
1333
+ if (config.dbSetup === "turso" && config.database !== "sqlite") {
1334
+ consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1335
+ process.exit(1);
1336
+ }
1337
+ if (config.dbSetup === "neon" && config.database !== "postgres") {
1338
+ consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1339
+ process.exit(1);
1340
+ }
1341
+ if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
1342
+ consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1343
+ process.exit(1);
1344
+ }
1345
+ if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
1346
+ consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1347
+ process.exit(1);
1348
+ }
1349
+ if (config.dbSetup === "supabase" && config.database !== "postgres") {
1350
+ consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1351
+ process.exit(1);
1352
+ }
1353
+ if (config.dbSetup === "d1") {
1354
+ if (config.database !== "sqlite") {
1355
+ consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1356
+ process.exit(1);
1357
+ }
1358
+ if (config.runtime !== "workers") {
1359
+ consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1360
+ process.exit(1);
1361
+ }
1362
+ }
1363
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
1364
+ consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
1365
+ process.exit(1);
1366
+ }
1367
+ if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
1368
+ consola$1.fatal(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
1369
+ process.exit(1);
1370
+ }
1371
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
1372
+ consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
1373
+ process.exit(1);
1374
+ }
1375
+ if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
1376
+ consola$1.fatal(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
1377
+ process.exit(1);
1378
+ }
1379
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
1380
+ consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
1381
+ process.exit(1);
1382
+ }
1383
+ if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
1384
+ consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
1385
+ process.exit(1);
1386
+ }
1387
+ return config;
1388
+ }
1389
+ function getProvidedFlags(options) {
1390
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1391
+ }
1392
+
1393
+ //#endregion
1394
+ //#region src/utils/tvi-config.ts
1395
+ const TVI_CONFIG_FILE = "tvi.jsonc";
1396
+ async function writeTviConfig(projectConfig) {
1397
+ const tviConfig = {
1398
+ version: getLatestCLIVersion(),
1399
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1400
+ database: projectConfig.database,
1401
+ orm: projectConfig.orm,
1402
+ backend: projectConfig.backend,
1403
+ runtime: projectConfig.runtime,
1404
+ frontend: projectConfig.frontend,
1405
+ addons: projectConfig.addons,
1406
+ examples: projectConfig.examples,
1407
+ auth: projectConfig.auth,
1408
+ packageManager: projectConfig.packageManager,
1409
+ dbSetup: projectConfig.dbSetup,
1410
+ api: projectConfig.api
1411
+ };
1412
+ const baseContent = {
1413
+ $schema: "https://tvi.dev/schema.json",
1414
+ version: tviConfig.version,
1415
+ createdAt: tviConfig.createdAt,
1416
+ database: tviConfig.database,
1417
+ orm: tviConfig.orm,
1418
+ backend: tviConfig.backend,
1419
+ runtime: tviConfig.runtime,
1420
+ frontend: tviConfig.frontend,
1421
+ addons: tviConfig.addons,
1422
+ examples: tviConfig.examples,
1423
+ auth: tviConfig.auth,
1424
+ packageManager: tviConfig.packageManager,
1425
+ dbSetup: tviConfig.dbSetup,
1426
+ api: tviConfig.api
1427
+ };
1428
+ let configContent = JSON.stringify(baseContent);
1429
+ const formatResult = JSONC.format(configContent, void 0, {
1430
+ tabSize: 2,
1431
+ insertSpaces: true,
1432
+ eol: "\n"
1433
+ });
1434
+ configContent = JSONC.applyEdits(configContent, formatResult);
1435
+ const finalContent = `// tvi configuration file
1436
+ // safe to delete
1437
+
1438
+ ${configContent}`;
1439
+ const configPath = path.join(projectConfig.projectDir, TVI_CONFIG_FILE);
1440
+ await fs.writeFile(configPath, finalContent, "utf-8");
1441
+ }
1442
+ async function readTviConfig(projectDir) {
1443
+ try {
1444
+ const configPath = path.join(projectDir, TVI_CONFIG_FILE);
1445
+ if (!await fs.pathExists(configPath)) return null;
1446
+ const configContent = await fs.readFile(configPath, "utf-8");
1447
+ const errors = [];
1448
+ const config = JSONC.parse(configContent, errors, {
1449
+ allowTrailingComma: true,
1450
+ disallowComments: false
1451
+ });
1452
+ if (errors.length > 0) {
1453
+ console.warn("Warning: Found errors parsing tvi.jsonc:", errors);
1454
+ return null;
1455
+ }
1456
+ return config;
1457
+ } catch (_error) {
1458
+ return null;
1459
+ }
1460
+ }
1461
+ async function updateTviConfig(projectDir, updates) {
1462
+ try {
1463
+ const configPath = path.join(projectDir, TVI_CONFIG_FILE);
1464
+ if (!await fs.pathExists(configPath)) return;
1465
+ const configContent = await fs.readFile(configPath, "utf-8");
1466
+ let modifiedContent = configContent;
1467
+ for (const [key, value] of Object.entries(updates)) {
1468
+ const editResult = JSONC.modify(modifiedContent, [key], value, { formattingOptions: {
1469
+ tabSize: 2,
1470
+ insertSpaces: true,
1471
+ eol: "\n"
1472
+ } });
1473
+ modifiedContent = JSONC.applyEdits(modifiedContent, editResult);
1474
+ }
1475
+ await fs.writeFile(configPath, modifiedContent, "utf-8");
1476
+ } catch (_error) {}
1477
+ }
1478
+
1479
+ //#endregion
1480
+ //#region src/utils/add-package-deps.ts
1481
+ const addPackageDependency = async (opts) => {
1482
+ const { dependencies = [], devDependencies = [], projectDir } = opts;
1483
+ const pkgJsonPath = path.join(projectDir, "package.json");
1484
+ const pkgJson = await fs.readJson(pkgJsonPath);
1485
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
1486
+ if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
1487
+ for (const pkgName of dependencies) {
1488
+ const version = dependencyVersionMap[pkgName];
1489
+ if (version) pkgJson.dependencies[pkgName] = version;
1490
+ else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
1491
+ }
1492
+ for (const pkgName of devDependencies) {
1493
+ const version = dependencyVersionMap[pkgName];
1494
+ if (version) pkgJson.devDependencies[pkgName] = version;
1495
+ else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
1496
+ }
1497
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1498
+ };
1499
+
1500
+ //#endregion
1501
+ //#region src/utils/package-runner.ts
1502
+ /**
1503
+ * Returns the appropriate command for running a package without installing it globally,
1504
+ * based on the selected package manager.
1505
+ *
1506
+ * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
1507
+ * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
1508
+ * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
1509
+ */
1510
+ function getPackageExecutionCommand(packageManager, commandWithArgs) {
1511
+ switch (packageManager) {
1512
+ case "pnpm": return `pnpm dlx ${commandWithArgs}`;
1513
+ case "bun": return `bunx ${commandWithArgs}`;
1514
+ default: return `npx ${commandWithArgs}`;
1515
+ }
1516
+ }
1517
+
1518
+ //#endregion
1519
+ //#region src/helpers/setup/starlight-setup.ts
1520
+ async function setupStarlight(config) {
1521
+ const { packageManager, projectDir } = config;
1522
+ const s = spinner();
1523
+ try {
1524
+ s.start("Setting up Starlight docs...");
1525
+ const starlightArgs = [
1526
+ "docs",
1527
+ "--template",
1528
+ "starlight",
1529
+ "--no-install",
1530
+ "--add",
1531
+ "tailwind",
1532
+ "--no-git",
1533
+ "--skip-houston"
1534
+ ];
1535
+ const starlightArgsString = starlightArgs.join(" ");
1536
+ const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
1537
+ const starlightInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
1538
+ await execa(starlightInitCommand, {
1539
+ cwd: path.join(projectDir, "apps"),
1540
+ env: { CI: "true" },
1541
+ shell: true
1542
+ });
1543
+ s.stop("Starlight docs setup successfully!");
1544
+ } catch (error) {
1545
+ s.stop(pc.red("Failed to set up Starlight docs"));
1546
+ if (error instanceof Error) consola.error(pc.red(error.message));
1547
+ }
1548
+ }
1549
+
1550
+ //#endregion
1551
+ //#region src/helpers/setup/tauri-setup.ts
1552
+ async function setupTauri(config) {
1553
+ const { packageManager, frontend, projectDir } = config;
1554
+ const s = spinner();
1555
+ const clientPackageDir = path.join(projectDir, "apps/web");
1556
+ if (!await fs.pathExists(clientPackageDir)) return;
1557
+ try {
1558
+ s.start("Setting up Tauri desktop app support...");
1559
+ await addPackageDependency({
1560
+ devDependencies: ["@tauri-apps/cli"],
1561
+ projectDir: clientPackageDir
1562
+ });
1563
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
1564
+ if (await fs.pathExists(clientPackageJsonPath)) {
1565
+ const packageJson = await fs.readJson(clientPackageJsonPath);
1566
+ packageJson.scripts = {
1567
+ ...packageJson.scripts,
1568
+ tauri: "tauri",
1569
+ "desktop:dev": "tauri dev",
1570
+ "desktop:build": "tauri build"
1571
+ };
1572
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
1573
+ }
1574
+ const _hasTanstackRouter = frontend.includes("tanstack-router");
1575
+ const hasReactRouter = frontend.includes("react-router");
1576
+ const hasNuxt = frontend.includes("nuxt");
1577
+ const hasSvelte = frontend.includes("svelte");
1578
+ const _hasSolid = frontend.includes("solid");
1579
+ const hasNext = frontend.includes("next");
1580
+ const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
1581
+ const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
1582
+ const tauriArgs = [
1583
+ "init",
1584
+ `--app-name=${path.basename(projectDir)}`,
1585
+ `--window-title=${path.basename(projectDir)}`,
1586
+ `--frontend-dist=${frontendDist}`,
1587
+ `--dev-url=${devUrl}`,
1588
+ `--before-dev-command=\"${packageManager} run dev\"`,
1589
+ `--before-build-command=\"${packageManager} run build\"`
1590
+ ];
1591
+ const tauriArgsString = tauriArgs.join(" ");
1592
+ const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
1593
+ const tauriInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
1594
+ await execa(tauriInitCommand, {
1595
+ cwd: clientPackageDir,
1596
+ env: { CI: "true" },
1597
+ shell: true
1598
+ });
1599
+ s.stop("Tauri desktop app support configured successfully!");
1600
+ } catch (error) {
1601
+ s.stop(pc.red("Failed to set up Tauri"));
1602
+ if (error instanceof Error) consola$1.error(pc.red(error.message));
1603
+ }
1604
+ }
1605
+
1606
+ //#endregion
1607
+ //#region src/helpers/setup/addons-setup.ts
1608
+ async function setupAddons(config, isAddCommand = false) {
1609
+ const { addons, frontend, projectDir } = config;
1610
+ const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
1611
+ const hasNuxtFrontend = frontend.includes("nuxt");
1612
+ const hasSvelteFrontend = frontend.includes("svelte");
1613
+ const hasSolidFrontend = frontend.includes("solid");
1614
+ const hasNextFrontend = frontend.includes("next");
1615
+ if (addons.includes("turborepo")) {
1616
+ await addPackageDependency({
1617
+ devDependencies: ["turbo"],
1618
+ projectDir
1619
+ });
1620
+ if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
1621
+
1622
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
1623
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
1624
+
1625
+ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
1626
+ `);
1627
+ }
1628
+ if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
1629
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
1630
+ if (addons.includes("biome")) await setupBiome(projectDir);
1631
+ if (addons.includes("husky")) await setupHusky(projectDir);
1632
+ if (addons.includes("starlight")) await setupStarlight(config);
1633
+ }
1634
+ function getWebAppDir(projectDir, frontends) {
1635
+ if (frontends.some((f) => [
1636
+ "react-router",
1637
+ "tanstack-router",
1638
+ "nuxt",
1639
+ "svelte",
1640
+ "solid"
1641
+ ].includes(f))) return path.join(projectDir, "apps/web");
1642
+ return path.join(projectDir, "apps/web");
1643
+ }
1644
+ async function setupBiome(projectDir) {
1645
+ await addPackageDependency({
1646
+ devDependencies: ["@biomejs/biome"],
1647
+ projectDir
1648
+ });
1649
+ const packageJsonPath = path.join(projectDir, "package.json");
1650
+ if (await fs.pathExists(packageJsonPath)) {
1651
+ const packageJson = await fs.readJson(packageJsonPath);
1652
+ packageJson.scripts = {
1653
+ ...packageJson.scripts,
1654
+ check: "biome check --write ."
1655
+ };
1656
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1657
+ }
1658
+ }
1659
+ async function setupHusky(projectDir) {
1660
+ await addPackageDependency({
1661
+ devDependencies: ["husky", "lint-staged"],
1662
+ projectDir
1663
+ });
1664
+ const packageJsonPath = path.join(projectDir, "package.json");
1665
+ if (await fs.pathExists(packageJsonPath)) {
1666
+ const packageJson = await fs.readJson(packageJsonPath);
1667
+ packageJson.scripts = {
1668
+ ...packageJson.scripts,
1669
+ prepare: "husky"
1670
+ };
1671
+ packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
1672
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1673
+ }
1674
+ }
1675
+ async function setupPwa(projectDir, frontends) {
1676
+ const isCompatibleFrontend = frontends.some((f) => [
1677
+ "react-router",
1678
+ "tanstack-router",
1679
+ "solid"
1680
+ ].includes(f));
1681
+ if (!isCompatibleFrontend) return;
1682
+ const clientPackageDir = getWebAppDir(projectDir, frontends);
1683
+ if (!await fs.pathExists(clientPackageDir)) return;
1684
+ await addPackageDependency({
1685
+ dependencies: ["vite-plugin-pwa"],
1686
+ devDependencies: ["@vite-pwa/assets-generator"],
1687
+ projectDir: clientPackageDir
1688
+ });
1689
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
1690
+ if (await fs.pathExists(clientPackageJsonPath)) {
1691
+ const packageJson = await fs.readJson(clientPackageJsonPath);
1692
+ packageJson.scripts = {
1693
+ ...packageJson.scripts,
1694
+ "generate-pwa-assets": "pwa-assets-generator"
1695
+ };
1696
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
1697
+ }
1698
+ }
1699
+
1700
+ //#endregion
1701
+ //#region src/helpers/project-generation/detect-project-config.ts
1702
+ async function detectProjectConfig(projectDir) {
1703
+ try {
1704
+ const tviConfig = await readTviConfig(projectDir);
1705
+ if (tviConfig) return {
1706
+ projectDir,
1707
+ projectName: path.basename(projectDir),
1708
+ database: tviConfig.database,
1709
+ orm: tviConfig.orm,
1710
+ backend: tviConfig.backend,
1711
+ runtime: tviConfig.runtime,
1712
+ frontend: tviConfig.frontend,
1713
+ addons: tviConfig.addons,
1714
+ examples: tviConfig.examples,
1715
+ auth: tviConfig.auth,
1716
+ packageManager: tviConfig.packageManager,
1717
+ dbSetup: tviConfig.dbSetup,
1718
+ api: tviConfig.api
1719
+ };
1720
+ return null;
1721
+ } catch (_error) {
1722
+ return null;
1723
+ }
1724
+ }
1725
+ async function isBetterTStackProject(projectDir) {
1726
+ try {
1727
+ return await fs.pathExists(path.join(projectDir, "tvi.jsonc"));
1728
+ } catch (_error) {
1729
+ return false;
1730
+ }
1731
+ }
1732
+
1733
+ //#endregion
1734
+ //#region src/helpers/project-generation/install-dependencies.ts
1735
+ async function installDependencies({ projectDir, packageManager }) {
1736
+ const s = spinner();
1737
+ try {
1738
+ s.start(`Running ${packageManager} install...`);
1739
+ await $({
1740
+ cwd: projectDir,
1741
+ stderr: "inherit"
1742
+ })`${packageManager} install`;
1743
+ s.stop("Dependencies installed successfully");
1744
+ } catch (error) {
1745
+ s.stop(pc.red("Failed to install dependencies"));
1746
+ if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
1747
+ }
1748
+ }
1749
+
1750
+ //#endregion
1751
+ //#region src/utils/template-processor.ts
1752
+ /**
1753
+ * Processes a Handlebars template file and writes the output to the destination.
1754
+ * @param srcPath Path to the source .hbs template file.
1755
+ * @param destPath Path to write the processed file.
1756
+ * @param context Data to be passed to the Handlebars template.
1757
+ */
1758
+ async function processTemplate(srcPath, destPath, context) {
1759
+ try {
1760
+ const templateContent = await fs.readFile(srcPath, "utf-8");
1761
+ const template = handlebars.compile(templateContent);
1762
+ const processedContent = template(context);
1763
+ await fs.ensureDir(path.dirname(destPath));
1764
+ await fs.writeFile(destPath, processedContent);
1765
+ } catch (error) {
1766
+ consola.error(`Error processing template ${srcPath}:`, error);
1767
+ throw new Error(`Failed to process template ${srcPath}`);
1768
+ }
1769
+ }
1770
+ handlebars.registerHelper("eq", (a, b) => a === b);
1771
+ handlebars.registerHelper("and", (a, b) => a && b);
1772
+ handlebars.registerHelper("or", (a, b) => a || b);
1773
+ handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
1774
+
1775
+ //#endregion
1776
+ //#region src/helpers/project-generation/template-manager.ts
1777
+ async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
1778
+ const sourceFiles = await globby(sourcePattern, {
1779
+ cwd: baseSourceDir,
1780
+ dot: true,
1781
+ onlyFiles: true,
1782
+ absolute: false
1783
+ });
1784
+ for (const relativeSrcPath of sourceFiles) {
1785
+ const srcPath = path.join(baseSourceDir, relativeSrcPath);
1786
+ let relativeDestPath = relativeSrcPath;
1787
+ if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
1788
+ const basename = path.basename(relativeSrcPath);
1789
+ if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".gitignore");
1790
+ else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeSrcPath), ".npmrc");
1791
+ const destPath = path.join(destDir, relativeDestPath);
1792
+ try {
1793
+ await fs.ensureDir(path.dirname(destPath));
1794
+ if (!overwrite && await fs.pathExists(destPath)) continue;
1795
+ if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
1796
+ else await fs.copy(srcPath, destPath, { overwrite: true });
1797
+ } catch (_error) {}
1798
+ }
1799
+ }
1800
+ async function copyBaseTemplate(projectDir, context) {
1801
+ const templateDir = path.join(PKG_ROOT, "templates/base");
1802
+ await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
1803
+ await fs.ensureDir(path.join(projectDir, "packages"));
1804
+ }
1805
+ async function setupFrontendTemplates(projectDir, context) {
1806
+ const hasReactWeb = context.frontend.some((f) => [
1807
+ "tanstack-router",
1808
+ "react-router",
1809
+ "tanstack-start",
1810
+ "next"
1811
+ ].includes(f));
1812
+ const hasNuxtWeb = context.frontend.includes("nuxt");
1813
+ const hasSvelteWeb = context.frontend.includes("svelte");
1814
+ const hasSolidWeb = context.frontend.includes("solid");
1815
+ const hasNativeWind = context.frontend.includes("native-nativewind");
1816
+ const hasUnistyles = context.frontend.includes("native-unistyles");
1817
+ const _hasNative = hasNativeWind || hasUnistyles;
1818
+ const isConvex = context.backend === "convex";
1819
+ if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
1820
+ const webAppDir = path.join(projectDir, "apps/web");
1821
+ await fs.ensureDir(webAppDir);
1822
+ if (hasReactWeb) {
1823
+ const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
1824
+ if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
1825
+ const reactFramework = context.frontend.find((f) => [
1826
+ "tanstack-router",
1827
+ "react-router",
1828
+ "tanstack-start",
1829
+ "next"
1830
+ ].includes(f));
1831
+ if (reactFramework) {
1832
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
1833
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
1834
+ if (!isConvex && context.api !== "none") {
1835
+ const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
1836
+ if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
1837
+ }
1838
+ }
1839
+ } else if (hasNuxtWeb) {
1840
+ const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
1841
+ if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
1842
+ if (!isConvex && context.api === "orpc") {
1843
+ const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
1844
+ if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
1845
+ }
1846
+ } else if (hasSvelteWeb) {
1847
+ const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
1848
+ if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
1849
+ if (!isConvex && context.api === "orpc") {
1850
+ const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
1851
+ if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
1852
+ }
1853
+ } else if (hasSolidWeb) {
1854
+ const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
1855
+ if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
1856
+ if (!isConvex && context.api === "orpc") {
1857
+ const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
1858
+ if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
1859
+ }
1860
+ }
1861
+ }
1862
+ if (hasNativeWind || hasUnistyles) {
1863
+ const nativeAppDir = path.join(projectDir, "apps/native");
1864
+ await fs.ensureDir(nativeAppDir);
1865
+ const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
1866
+ if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
1867
+ let nativeFrameworkPath = "";
1868
+ if (hasNativeWind) nativeFrameworkPath = "nativewind";
1869
+ else if (hasUnistyles) nativeFrameworkPath = "unistyles";
1870
+ const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
1871
+ if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
1872
+ if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
1873
+ const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
1874
+ if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
1875
+ }
1876
+ }
1877
+ }
1878
+ async function setupBackendFramework(projectDir, context) {
1879
+ if (context.backend === "none") return;
1880
+ const serverAppDir = path.join(projectDir, "apps/server");
1881
+ if (context.backend === "convex") {
1882
+ if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
1883
+ const convexBackendDestDir = path.join(projectDir, "packages/backend");
1884
+ const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
1885
+ await fs.ensureDir(convexBackendDestDir);
1886
+ if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
1887
+ return;
1888
+ }
1889
+ await fs.ensureDir(serverAppDir);
1890
+ const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
1891
+ if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
1892
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
1893
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
1894
+ if (context.api !== "none") {
1895
+ const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
1896
+ if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
1897
+ const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
1898
+ if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
1899
+ }
1900
+ }
1901
+ async function setupDbOrmTemplates(projectDir, context) {
1902
+ if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
1903
+ const serverAppDir = path.join(projectDir, "apps/server");
1904
+ await fs.ensureDir(serverAppDir);
1905
+ const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
1906
+ if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
1907
+ }
1908
+ async function setupAuthTemplate(projectDir, context) {
1909
+ if (context.backend === "convex" || !context.auth) return;
1910
+ const serverAppDir = path.join(projectDir, "apps/server");
1911
+ const webAppDir = path.join(projectDir, "apps/web");
1912
+ const nativeAppDir = path.join(projectDir, "apps/native");
1913
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
1914
+ const webAppDirExists = await fs.pathExists(webAppDir);
1915
+ const nativeAppDirExists = await fs.pathExists(nativeAppDir);
1916
+ const hasReactWeb = context.frontend.some((f) => [
1917
+ "tanstack-router",
1918
+ "react-router",
1919
+ "tanstack-start",
1920
+ "next"
1921
+ ].includes(f));
1922
+ const hasNuxtWeb = context.frontend.includes("nuxt");
1923
+ const hasSvelteWeb = context.frontend.includes("svelte");
1924
+ const hasSolidWeb = context.frontend.includes("solid");
1925
+ const hasNativeWind = context.frontend.includes("native-nativewind");
1926
+ const hasUnistyles = context.frontend.includes("native-unistyles");
1927
+ const hasNative = hasNativeWind || hasUnistyles;
1928
+ if (serverAppDirExists) {
1929
+ const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
1930
+ if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
1931
+ if (context.backend === "next") {
1932
+ const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
1933
+ if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
1934
+ }
1935
+ if (context.orm !== "none" && context.database !== "none") {
1936
+ const orm = context.orm;
1937
+ const db = context.database;
1938
+ let authDbSrc = "";
1939
+ if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/drizzle/${db}`);
1940
+ else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/prisma/${db}`);
1941
+ else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/mongoose/${db}`);
1942
+ if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
1943
+ else if (authDbSrc) {}
1944
+ }
1945
+ }
1946
+ if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
1947
+ if (hasReactWeb) {
1948
+ const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/react/base");
1949
+ if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
1950
+ const reactFramework = context.frontend.find((f) => [
1951
+ "tanstack-router",
1952
+ "react-router",
1953
+ "tanstack-start",
1954
+ "next"
1955
+ ].includes(f));
1956
+ if (reactFramework) {
1957
+ const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/web/react/${reactFramework}`);
1958
+ if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
1959
+ }
1960
+ } else if (hasNuxtWeb) {
1961
+ const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
1962
+ if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
1963
+ } else if (hasSvelteWeb) {
1964
+ if (context.api === "orpc") {
1965
+ const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
1966
+ if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
1967
+ }
1968
+ } else if (hasSolidWeb) {
1969
+ if (context.api === "orpc") {
1970
+ const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
1971
+ if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
1972
+ }
1973
+ }
1974
+ }
1975
+ if (hasNative && nativeAppDirExists) {
1976
+ const authNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/native/native-base");
1977
+ if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
1978
+ let nativeFrameworkAuthPath = "";
1979
+ if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
1980
+ else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
1981
+ if (nativeFrameworkAuthPath) {
1982
+ const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/native/${nativeFrameworkAuthPath}`);
1983
+ if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
1984
+ }
1985
+ }
1986
+ }
1987
+ async function setupAddonsTemplate(projectDir, context) {
1988
+ if (!context.addons || context.addons.length === 0) return;
1989
+ for (const addon of context.addons) {
1990
+ if (addon === "none") continue;
1991
+ let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
1992
+ let addonDestDir = projectDir;
1993
+ if (addon === "pwa") {
1994
+ const webAppDir = path.join(projectDir, "apps/web");
1995
+ if (!await fs.pathExists(webAppDir)) continue;
1996
+ addonDestDir = webAppDir;
1997
+ if (context.frontend.includes("next")) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
1998
+ else if (context.frontend.some((f) => [
1999
+ "tanstack-router",
2000
+ "react-router",
2001
+ "solid"
2002
+ ].includes(f))) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
2003
+ else continue;
2004
+ }
2005
+ if (await fs.pathExists(addonSrcDir)) await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
2006
+ }
2007
+ }
2008
+ async function setupExamplesTemplate(projectDir, context) {
2009
+ if (!context.examples || context.examples.length === 0 || context.examples[0] === "none") return;
2010
+ const serverAppDir = path.join(projectDir, "apps/server");
2011
+ const webAppDir = path.join(projectDir, "apps/web");
2012
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
2013
+ const webAppDirExists = await fs.pathExists(webAppDir);
2014
+ const nativeAppDir = path.join(projectDir, "apps/native");
2015
+ const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2016
+ const hasReactWeb = context.frontend.some((f) => [
2017
+ "tanstack-router",
2018
+ "react-router",
2019
+ "tanstack-start",
2020
+ "next"
2021
+ ].includes(f));
2022
+ const hasNuxtWeb = context.frontend.includes("nuxt");
2023
+ const hasSvelteWeb = context.frontend.includes("svelte");
2024
+ const hasSolidWeb = context.frontend.includes("solid");
2025
+ for (const example of context.examples) {
2026
+ if (example === "none") continue;
2027
+ const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
2028
+ if (serverAppDirExists && context.backend !== "convex" && context.backend !== "none") {
2029
+ const exampleServerSrc = path.join(exampleBaseDir, "server");
2030
+ if (example === "ai" && context.backend === "next") {
2031
+ const aiNextServerSrc = path.join(exampleServerSrc, "next");
2032
+ if (await fs.pathExists(aiNextServerSrc)) await processAndCopyFiles("**/*", aiNextServerSrc, serverAppDir, context, false);
2033
+ }
2034
+ if (context.orm !== "none" && context.database !== "none") {
2035
+ const exampleOrmBaseSrc = path.join(exampleServerSrc, context.orm, "base");
2036
+ if (await fs.pathExists(exampleOrmBaseSrc)) await processAndCopyFiles("**/*", exampleOrmBaseSrc, serverAppDir, context, false);
2037
+ const exampleDbSchemaSrc = path.join(exampleServerSrc, context.orm, context.database);
2038
+ if (await fs.pathExists(exampleDbSchemaSrc)) await processAndCopyFiles("**/*", exampleDbSchemaSrc, serverAppDir, context, false);
2039
+ }
2040
+ const ignorePatterns = [`${context.orm}/**`];
2041
+ if (example === "ai" && context.backend === "next") ignorePatterns.push("next/**");
2042
+ const generalServerFiles = await globby(["**/*.ts", "**/*.hbs"], {
2043
+ cwd: exampleServerSrc,
2044
+ onlyFiles: true,
2045
+ deep: 1,
2046
+ ignore: ignorePatterns
2047
+ });
2048
+ for (const file of generalServerFiles) {
2049
+ const srcPath = path.join(exampleServerSrc, file);
2050
+ const destPath = path.join(serverAppDir, file.replace(".hbs", ""));
2051
+ try {
2052
+ if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
2053
+ else if (!await fs.pathExists(destPath)) await fs.copy(srcPath, destPath, { overwrite: false });
2054
+ } catch (_error) {}
2055
+ }
2056
+ }
2057
+ if (webAppDirExists) {
2058
+ if (hasReactWeb) {
2059
+ const exampleWebSrc = path.join(exampleBaseDir, "web/react");
2060
+ if (await fs.pathExists(exampleWebSrc)) {
2061
+ const reactFramework = context.frontend.find((f) => [
2062
+ "next",
2063
+ "react-router",
2064
+ "tanstack-router",
2065
+ "tanstack-start"
2066
+ ].includes(f));
2067
+ if (reactFramework) {
2068
+ const exampleWebFrameworkSrc = path.join(exampleWebSrc, reactFramework);
2069
+ if (await fs.pathExists(exampleWebFrameworkSrc)) await processAndCopyFiles("**/*", exampleWebFrameworkSrc, webAppDir, context, false);
2070
+ }
2071
+ }
2072
+ } else if (hasNuxtWeb) {
2073
+ const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
2074
+ if (await fs.pathExists(exampleWebNuxtSrc)) await processAndCopyFiles("**/*", exampleWebNuxtSrc, webAppDir, context, false);
2075
+ } else if (hasSvelteWeb) {
2076
+ const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
2077
+ if (await fs.pathExists(exampleWebSvelteSrc)) await processAndCopyFiles("**/*", exampleWebSvelteSrc, webAppDir, context, false);
2078
+ } else if (hasSolidWeb) {
2079
+ const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
2080
+ if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
2081
+ }
2082
+ }
2083
+ if (nativeAppDirExists) {
2084
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2085
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2086
+ if (hasNativeWind || hasUnistyles) {
2087
+ let nativeFramework = "";
2088
+ if (hasNativeWind) nativeFramework = "nativewind";
2089
+ else if (hasUnistyles) nativeFramework = "unistyles";
2090
+ const exampleNativeSrc = path.join(exampleBaseDir, `native/${nativeFramework}`);
2091
+ if (await fs.pathExists(exampleNativeSrc)) await processAndCopyFiles("**/*", exampleNativeSrc, nativeAppDir, context, false);
2092
+ }
2093
+ }
2094
+ }
2095
+ }
2096
+ async function handleExtras(projectDir, context) {
2097
+ const extrasDir = path.join(PKG_ROOT, "templates/extras");
2098
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2099
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2100
+ const hasNative = hasNativeWind || hasUnistyles;
2101
+ if (context.packageManager === "pnpm") {
2102
+ const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
2103
+ const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
2104
+ if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
2105
+ }
2106
+ if (context.packageManager === "pnpm" && (hasNative || context.frontend.includes("nuxt"))) {
2107
+ const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
2108
+ const npmrcDest = path.join(projectDir, ".npmrc");
2109
+ if (await fs.pathExists(npmrcTemplateSrc)) await processTemplate(npmrcTemplateSrc, npmrcDest, context);
2110
+ }
2111
+ if (context.runtime === "workers") {
2112
+ const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers");
2113
+ if (await fs.pathExists(runtimeWorkersDir)) await processAndCopyFiles("**/*", runtimeWorkersDir, projectDir, context, false);
2114
+ }
2115
+ }
2116
+
2117
+ //#endregion
2118
+ //#region src/helpers/project-generation/add-addons.ts
2119
+ function exitWithError(message) {
2120
+ cancel(pc.red(message));
2121
+ process.exit(1);
2122
+ }
2123
+ async function addAddonsToProject(input) {
2124
+ try {
2125
+ const projectDir = input.projectDir || process.cwd();
2126
+ const isBetterTStack = await isBetterTStackProject(projectDir);
2127
+ if (!isBetterTStack) exitWithError("This doesn't appear to be a tvi project. Please run this command from the root of a tvi project.");
2128
+ const detectedConfig = await detectProjectConfig(projectDir);
2129
+ if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid tvi project.");
2130
+ const config = {
2131
+ projectName: detectedConfig.projectName || path.basename(projectDir),
2132
+ projectDir,
2133
+ relativePath: ".",
2134
+ database: detectedConfig.database || "none",
2135
+ orm: detectedConfig.orm || "none",
2136
+ backend: detectedConfig.backend || "none",
2137
+ runtime: detectedConfig.runtime || "none",
2138
+ frontend: detectedConfig.frontend || [],
2139
+ addons: input.addons,
2140
+ examples: detectedConfig.examples || [],
2141
+ auth: detectedConfig.auth || false,
2142
+ git: false,
2143
+ packageManager: input.packageManager || detectedConfig.packageManager || "npm",
2144
+ install: input.install || false,
2145
+ dbSetup: detectedConfig.dbSetup || "none",
2146
+ api: detectedConfig.api || "none"
2147
+ };
2148
+ for (const addon of input.addons) {
2149
+ const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
2150
+ if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
2151
+ }
2152
+ log.info(pc.green(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`));
2153
+ await setupAddonsTemplate(projectDir, config);
2154
+ await setupAddons(config, true);
2155
+ const currentAddons = detectedConfig.addons || [];
2156
+ const mergedAddons = [...new Set([...currentAddons, ...input.addons])];
2157
+ await updateTviConfig(projectDir, { addons: mergedAddons });
2158
+ if (config.install) await installDependencies({
2159
+ projectDir,
2160
+ packageManager: config.packageManager
2161
+ });
2162
+ else log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
2163
+ } catch (error) {
2164
+ const message = error instanceof Error ? error.message : String(error);
2165
+ exitWithError(`Error adding addons: ${message}`);
2166
+ }
2167
+ }
2168
+
2169
+ //#endregion
2170
+ //#region src/helpers/setup/api-setup.ts
2171
+ async function setupApi(config) {
2172
+ const { api, projectName, frontend, backend, packageManager, projectDir } = config;
2173
+ const isConvex = backend === "convex";
2174
+ const webDir = path.join(projectDir, "apps/web");
2175
+ const nativeDir = path.join(projectDir, "apps/native");
2176
+ const webDirExists = await fs.pathExists(webDir);
2177
+ const nativeDirExists = await fs.pathExists(nativeDir);
2178
+ const hasReactWeb = frontend.some((f) => [
2179
+ "tanstack-router",
2180
+ "react-router",
2181
+ "tanstack-start",
2182
+ "next"
2183
+ ].includes(f));
2184
+ const hasNuxtWeb = frontend.includes("nuxt");
2185
+ const hasSvelteWeb = frontend.includes("svelte");
2186
+ const hasSolidWeb = frontend.includes("solid");
2187
+ if (!isConvex && api !== "none") {
2188
+ const serverDir = path.join(projectDir, "apps/server");
2189
+ const serverDirExists = await fs.pathExists(serverDir);
2190
+ if (serverDirExists) {
2191
+ if (api === "orpc") await addPackageDependency({
2192
+ dependencies: ["@orpc/server", "@orpc/client"],
2193
+ projectDir: serverDir
2194
+ });
2195
+ else if (api === "trpc") {
2196
+ await addPackageDependency({
2197
+ dependencies: ["@trpc/server", "@trpc/client"],
2198
+ projectDir: serverDir
2199
+ });
2200
+ if (config.backend === "hono") await addPackageDependency({
2201
+ dependencies: ["@hono/trpc-server"],
2202
+ projectDir: serverDir
2203
+ });
2204
+ else if (config.backend === "elysia") await addPackageDependency({
2205
+ dependencies: ["@elysiajs/trpc"],
2206
+ projectDir: serverDir
2207
+ });
2208
+ }
2209
+ }
2210
+ if (webDirExists) {
2211
+ if (hasReactWeb) {
2212
+ if (api === "orpc") await addPackageDependency({
2213
+ dependencies: [
2214
+ "@orpc/tanstack-query",
2215
+ "@orpc/client",
2216
+ "@orpc/server"
2217
+ ],
2218
+ projectDir: webDir
2219
+ });
2220
+ else if (api === "trpc") await addPackageDependency({
2221
+ dependencies: [
2222
+ "@trpc/tanstack-react-query",
2223
+ "@trpc/client",
2224
+ "@trpc/server"
2225
+ ],
2226
+ projectDir: webDir
2227
+ });
2228
+ } else if (hasNuxtWeb) {
2229
+ if (api === "orpc") await addPackageDependency({
2230
+ dependencies: [
2231
+ "@orpc/tanstack-query",
2232
+ "@orpc/client",
2233
+ "@orpc/server"
2234
+ ],
2235
+ projectDir: webDir
2236
+ });
2237
+ } else if (hasSvelteWeb) {
2238
+ if (api === "orpc") await addPackageDependency({
2239
+ dependencies: [
2240
+ "@orpc/tanstack-query",
2241
+ "@orpc/client",
2242
+ "@orpc/server",
2243
+ "@tanstack/svelte-query"
2244
+ ],
2245
+ projectDir: webDir
2246
+ });
2247
+ } else if (hasSolidWeb) {
2248
+ if (api === "orpc") await addPackageDependency({
2249
+ dependencies: [
2250
+ "@orpc/tanstack-query",
2251
+ "@orpc/client",
2252
+ "@orpc/server",
2253
+ "@tanstack/solid-query"
2254
+ ],
2255
+ projectDir: webDir
2256
+ });
2257
+ }
2258
+ }
2259
+ if (nativeDirExists) {
2260
+ if (api === "trpc") await addPackageDependency({
2261
+ dependencies: [
2262
+ "@trpc/tanstack-react-query",
2263
+ "@trpc/client",
2264
+ "@trpc/server"
2265
+ ],
2266
+ projectDir: nativeDir
2267
+ });
2268
+ else if (api === "orpc") await addPackageDependency({
2269
+ dependencies: [
2270
+ "@orpc/tanstack-query",
2271
+ "@orpc/client",
2272
+ "@orpc/server"
2273
+ ],
2274
+ projectDir: nativeDir
2275
+ });
2276
+ }
2277
+ }
2278
+ const reactBasedFrontends = [
2279
+ "react-router",
2280
+ "tanstack-router",
2281
+ "tanstack-start",
2282
+ "next",
2283
+ "native-nativewind",
2284
+ "native-unistyles"
2285
+ ];
2286
+ const needsSolidQuery = frontend.includes("solid");
2287
+ const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
2288
+ if (needsReactQuery && !isConvex) {
2289
+ const reactQueryDeps = ["@tanstack/react-query"];
2290
+ const reactQueryDevDeps = ["@tanstack/react-query-devtools"];
2291
+ const hasReactWeb$1 = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
2292
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
2293
+ if (hasReactWeb$1 && webDirExists) {
2294
+ const webPkgJsonPath = path.join(webDir, "package.json");
2295
+ if (await fs.pathExists(webPkgJsonPath)) try {
2296
+ await addPackageDependency({
2297
+ dependencies: reactQueryDeps,
2298
+ devDependencies: reactQueryDevDeps,
2299
+ projectDir: webDir
2300
+ });
2301
+ } catch (_error) {}
2302
+ }
2303
+ if (hasNative && nativeDirExists) {
2304
+ const nativePkgJsonPath = path.join(nativeDir, "package.json");
2305
+ if (await fs.pathExists(nativePkgJsonPath)) try {
2306
+ await addPackageDependency({
2307
+ dependencies: reactQueryDeps,
2308
+ projectDir: nativeDir
2309
+ });
2310
+ } catch (_error) {}
2311
+ }
2312
+ }
2313
+ if (needsSolidQuery && !isConvex) {
2314
+ const solidQueryDeps = ["@tanstack/solid-query"];
2315
+ const solidQueryDevDeps = ["@tanstack/solid-query-devtools"];
2316
+ if (webDirExists) {
2317
+ const webPkgJsonPath = path.join(webDir, "package.json");
2318
+ if (await fs.pathExists(webPkgJsonPath)) try {
2319
+ await addPackageDependency({
2320
+ dependencies: solidQueryDeps,
2321
+ devDependencies: solidQueryDevDeps,
2322
+ projectDir: webDir
2323
+ });
2324
+ } catch (_error) {}
2325
+ }
2326
+ }
2327
+ if (isConvex) {
2328
+ if (webDirExists) {
2329
+ const webPkgJsonPath = path.join(webDir, "package.json");
2330
+ if (await fs.pathExists(webPkgJsonPath)) try {
2331
+ const webDepsToAdd = ["convex"];
2332
+ if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
2333
+ if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
2334
+ await addPackageDependency({
2335
+ dependencies: webDepsToAdd,
2336
+ projectDir: webDir
2337
+ });
2338
+ } catch (_error) {}
2339
+ }
2340
+ if (nativeDirExists) {
2341
+ const nativePkgJsonPath = path.join(nativeDir, "package.json");
2342
+ if (await fs.pathExists(nativePkgJsonPath)) try {
2343
+ await addPackageDependency({
2344
+ dependencies: ["convex"],
2345
+ projectDir: nativeDir
2346
+ });
2347
+ } catch (_error) {}
2348
+ }
2349
+ const backendPackageName = `@${projectName}/backend`;
2350
+ const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
2351
+ const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => {
2352
+ try {
2353
+ const pkgJson = await fs.readJson(pkgJsonPath);
2354
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
2355
+ if (pkgJson.dependencies[depName] !== depVersion) {
2356
+ pkgJson.dependencies[depName] = depVersion;
2357
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2358
+ }
2359
+ } catch (_error) {}
2360
+ };
2361
+ if (webDirExists) {
2362
+ const webPkgJsonPath = path.join(webDir, "package.json");
2363
+ if (await fs.pathExists(webPkgJsonPath)) await addWorkspaceDepManually(webPkgJsonPath, backendPackageName, backendWorkspaceVersion);
2364
+ }
2365
+ if (nativeDirExists) {
2366
+ const nativePkgJsonPath = path.join(nativeDir, "package.json");
2367
+ if (await fs.pathExists(nativePkgJsonPath)) await addWorkspaceDepManually(nativePkgJsonPath, backendPackageName, backendWorkspaceVersion);
2368
+ }
2369
+ }
2370
+ }
2371
+
2372
+ //#endregion
2373
+ //#region src/helpers/setup/auth-setup.ts
2374
+ async function setupAuth(config) {
2375
+ const { auth, frontend, backend, projectDir } = config;
2376
+ if (backend === "convex" || !auth) return;
2377
+ const serverDir = path.join(projectDir, "apps/server");
2378
+ const clientDir = path.join(projectDir, "apps/web");
2379
+ const nativeDir = path.join(projectDir, "apps/native");
2380
+ const clientDirExists = await fs.pathExists(clientDir);
2381
+ const nativeDirExists = await fs.pathExists(nativeDir);
2382
+ const serverDirExists = await fs.pathExists(serverDir);
2383
+ try {
2384
+ if (serverDirExists) await addPackageDependency({
2385
+ dependencies: ["better-auth"],
2386
+ projectDir: serverDir
2387
+ });
2388
+ const hasWebFrontend = frontend.some((f) => [
2389
+ "react-router",
2390
+ "tanstack-router",
2391
+ "tanstack-start",
2392
+ "next",
2393
+ "nuxt",
2394
+ "svelte",
2395
+ "solid"
2396
+ ].includes(f));
2397
+ if (hasWebFrontend && clientDirExists) await addPackageDependency({
2398
+ dependencies: ["better-auth"],
2399
+ projectDir: clientDir
2400
+ });
2401
+ if ((frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) && nativeDirExists) {
2402
+ await addPackageDependency({
2403
+ dependencies: ["better-auth", "@better-auth/expo"],
2404
+ projectDir: nativeDir
2405
+ });
2406
+ if (serverDirExists) await addPackageDependency({
2407
+ dependencies: ["@better-auth/expo"],
2408
+ projectDir: serverDir
2409
+ });
2410
+ }
2411
+ } catch (error) {
2412
+ consola.error(pc.red("Failed to configure authentication dependencies"));
2413
+ if (error instanceof Error) consola.error(pc.red(error.message));
2414
+ }
2415
+ }
2416
+ function generateAuthSecret(length = 32) {
2417
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2418
+ let result = "";
2419
+ const charactersLength = characters.length;
2420
+ for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength));
2421
+ return result;
2422
+ }
2423
+
2424
+ //#endregion
2425
+ //#region src/helpers/setup/backend-setup.ts
2426
+ async function setupBackendDependencies(config) {
2427
+ const { backend, runtime, api, projectDir } = config;
2428
+ if (backend === "convex") return;
2429
+ const framework = backend;
2430
+ const serverDir = path.join(projectDir, "apps/server");
2431
+ const dependencies = [];
2432
+ const devDependencies = [];
2433
+ if (framework === "hono") {
2434
+ dependencies.push("hono");
2435
+ if (api === "trpc") dependencies.push("@hono/trpc-server");
2436
+ if (runtime === "node") {
2437
+ dependencies.push("@hono/node-server");
2438
+ devDependencies.push("tsx", "@types/node");
2439
+ }
2440
+ } else if (framework === "elysia") {
2441
+ dependencies.push("elysia", "@elysiajs/cors");
2442
+ if (api === "trpc") dependencies.push("@elysiajs/trpc");
2443
+ if (runtime === "node") {
2444
+ dependencies.push("@elysiajs/node");
2445
+ devDependencies.push("tsx", "@types/node");
2446
+ }
2447
+ } else if (framework === "express") {
2448
+ dependencies.push("express", "cors");
2449
+ devDependencies.push("@types/express", "@types/cors");
2450
+ if (runtime === "node") devDependencies.push("tsx", "@types/node");
2451
+ } else if (framework === "fastify") {
2452
+ dependencies.push("fastify", "@fastify/cors");
2453
+ if (runtime === "node") devDependencies.push("tsx", "@types/node");
2454
+ }
2455
+ if (runtime === "bun") devDependencies.push("@types/bun");
2456
+ if (dependencies.length > 0 || devDependencies.length > 0) await addPackageDependency({
2457
+ dependencies,
2458
+ devDependencies,
2459
+ projectDir: serverDir
2460
+ });
2461
+ }
2462
+
2463
+ //#endregion
2464
+ //#region src/helpers/project-generation/env-setup.ts
2465
+ async function addEnvVariablesToFile(filePath, variables) {
2466
+ await fs.ensureDir(path.dirname(filePath));
2467
+ let envContent = "";
2468
+ if (await fs.pathExists(filePath)) envContent = await fs.readFile(filePath, "utf8");
2469
+ let modified = false;
2470
+ let contentToAdd = "";
2471
+ const exampleVariables = [];
2472
+ for (const { key, value, condition } of variables) if (condition) {
2473
+ const regex = new RegExp(`^${key}=.*$`, "m");
2474
+ const valueToWrite = value ?? "";
2475
+ exampleVariables.push(`${key}=`);
2476
+ if (regex.test(envContent)) {
2477
+ const existingMatch = envContent.match(regex);
2478
+ if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
2479
+ envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
2480
+ modified = true;
2481
+ }
2482
+ } else {
2483
+ contentToAdd += `${key}=${valueToWrite}\n`;
2484
+ modified = true;
2485
+ }
2486
+ }
2487
+ if (contentToAdd) {
2488
+ if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n";
2489
+ envContent += contentToAdd;
2490
+ }
2491
+ if (modified) await fs.writeFile(filePath, envContent.trimEnd());
2492
+ const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
2493
+ let exampleEnvContent = "";
2494
+ if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
2495
+ let exampleModified = false;
2496
+ let exampleContentToAdd = "";
2497
+ for (const exampleVar of exampleVariables) {
2498
+ const key = exampleVar.split("=")[0];
2499
+ const regex = new RegExp(`^${key}=.*$`, "m");
2500
+ if (!regex.test(exampleEnvContent)) {
2501
+ exampleContentToAdd += `${exampleVar}\n`;
2502
+ exampleModified = true;
2503
+ }
2504
+ }
2505
+ if (exampleContentToAdd) {
2506
+ if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) exampleEnvContent += "\n";
2507
+ exampleEnvContent += exampleContentToAdd;
2508
+ }
2509
+ if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
2510
+ }
2511
+ async function setupEnvironmentVariables(config) {
2512
+ const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
2513
+ const hasReactRouter = frontend.includes("react-router");
2514
+ const hasTanStackRouter = frontend.includes("tanstack-router");
2515
+ const hasTanStackStart = frontend.includes("tanstack-start");
2516
+ const hasNextJs = frontend.includes("next");
2517
+ const hasNuxt = frontend.includes("nuxt");
2518
+ const hasSvelte = frontend.includes("svelte");
2519
+ const hasSolid = frontend.includes("solid");
2520
+ const hasWebFrontend = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || hasSolid || hasSvelte;
2521
+ if (hasWebFrontend) {
2522
+ const clientDir = path.join(projectDir, "apps/web");
2523
+ if (await fs.pathExists(clientDir)) {
2524
+ let envVarName = "VITE_SERVER_URL";
2525
+ let serverUrl = "http://localhost:3000";
2526
+ if (hasNextJs) envVarName = "NEXT_PUBLIC_SERVER_URL";
2527
+ else if (hasNuxt) envVarName = "NUXT_PUBLIC_SERVER_URL";
2528
+ else if (hasSvelte) envVarName = "PUBLIC_SERVER_URL";
2529
+ if (backend === "convex") {
2530
+ if (hasNextJs) envVarName = "NEXT_PUBLIC_CONVEX_URL";
2531
+ else if (hasNuxt) envVarName = "NUXT_PUBLIC_CONVEX_URL";
2532
+ else if (hasSvelte) envVarName = "PUBLIC_CONVEX_URL";
2533
+ else envVarName = "VITE_CONVEX_URL";
2534
+ serverUrl = "https://<YOUR_CONVEX_URL>";
2535
+ }
2536
+ const clientVars = [{
2537
+ key: envVarName,
2538
+ value: serverUrl,
2539
+ condition: true
2540
+ }];
2541
+ await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
2542
+ }
2543
+ }
2544
+ if (frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) {
2545
+ const nativeDir = path.join(projectDir, "apps/native");
2546
+ if (await fs.pathExists(nativeDir)) {
2547
+ let envVarName = "EXPO_PUBLIC_SERVER_URL";
2548
+ let serverUrl = "http://localhost:3000";
2549
+ if (backend === "convex") {
2550
+ envVarName = "EXPO_PUBLIC_CONVEX_URL";
2551
+ serverUrl = "https://<YOUR_CONVEX_URL>";
2552
+ }
2553
+ const nativeVars = [{
2554
+ key: envVarName,
2555
+ value: serverUrl,
2556
+ condition: true
2557
+ }];
2558
+ await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
2559
+ }
2560
+ }
2561
+ if (backend === "convex") return;
2562
+ const serverDir = path.join(projectDir, "apps/server");
2563
+ if (!await fs.pathExists(serverDir)) return;
2564
+ const envPath = path.join(serverDir, ".env");
2565
+ let corsOrigin = "http://localhost:3001";
2566
+ if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173";
2567
+ let databaseUrl = null;
2568
+ const specializedSetup = dbSetup === "turso" || dbSetup === "prisma-postgres" || dbSetup === "mongodb-atlas" || dbSetup === "neon" || dbSetup === "supabase" || dbSetup === "d1";
2569
+ if (database !== "none" && !specializedSetup) switch (database) {
2570
+ case "postgres":
2571
+ databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
2572
+ break;
2573
+ case "mysql":
2574
+ databaseUrl = "mysql://root:password@localhost:3306/mydb";
2575
+ break;
2576
+ case "mongodb":
2577
+ databaseUrl = "mongodb://localhost:27017/mydatabase";
2578
+ break;
2579
+ case "sqlite":
2580
+ if (config.runtime === "workers") databaseUrl = "http://127.0.0.1:8080";
2581
+ else databaseUrl = "file:./local.db";
2582
+ break;
2583
+ }
2584
+ const serverVars = [
2585
+ {
2586
+ key: "CORS_ORIGIN",
2587
+ value: corsOrigin,
2588
+ condition: true
2589
+ },
2590
+ {
2591
+ key: "BETTER_AUTH_SECRET",
2592
+ value: generateAuthSecret(),
2593
+ condition: !!auth
2594
+ },
2595
+ {
2596
+ key: "BETTER_AUTH_URL",
2597
+ value: "http://localhost:3000",
2598
+ condition: !!auth
2599
+ },
2600
+ {
2601
+ key: "DATABASE_URL",
2602
+ value: databaseUrl,
2603
+ condition: database !== "none" && !specializedSetup
2604
+ },
2605
+ {
2606
+ key: "GOOGLE_GENERATIVE_AI_API_KEY",
2607
+ value: "",
2608
+ condition: examples?.includes("ai") || false
2609
+ }
2610
+ ];
2611
+ await addEnvVariablesToFile(envPath, serverVars);
2612
+ if (config.runtime === "workers") {
2613
+ const devVarsPath = path.join(serverDir, ".dev.vars");
2614
+ try {
2615
+ await fs.copy(envPath, devVarsPath);
2616
+ } catch (_err) {}
2617
+ }
2618
+ }
2619
+
2620
+ //#endregion
2621
+ //#region src/helpers/database-providers/d1-setup.ts
2622
+ async function setupCloudflareD1(config) {
2623
+ const { projectDir } = config;
2624
+ const envPath = path.join(projectDir, "apps/server", ".env");
2625
+ const variables = [
2626
+ {
2627
+ key: "CLOUDFLARE_ACCOUNT_ID",
2628
+ value: "",
2629
+ condition: true
2630
+ },
2631
+ {
2632
+ key: "CLOUDFLARE_DATABASE_ID",
2633
+ value: "",
2634
+ condition: true
2635
+ },
2636
+ {
2637
+ key: "CLOUDFLARE_D1_TOKEN",
2638
+ value: "",
2639
+ condition: true
2640
+ }
2641
+ ];
2642
+ try {
2643
+ await addEnvVariablesToFile(envPath, variables);
2644
+ } catch (_err) {}
2645
+ }
2646
+
2647
+ //#endregion
2648
+ //#region src/utils/command-exists.ts
2649
+ async function commandExists(command) {
2650
+ try {
2651
+ const isWindows = process.platform === "win32";
2652
+ if (isWindows) {
2653
+ const result$1 = await execa("where", [command]);
2654
+ return result$1.exitCode === 0;
2655
+ }
2656
+ const result = await execa("which", [command]);
2657
+ return result.exitCode === 0;
2658
+ } catch {
2659
+ return false;
2660
+ }
2661
+ }
2662
+
2663
+ //#endregion
2664
+ //#region src/helpers/database-providers/mongodb-atlas-setup.ts
2665
+ async function checkAtlasCLI() {
2666
+ const s = spinner();
2667
+ s.start("Checking for MongoDB Atlas CLI...");
2668
+ try {
2669
+ const exists = await commandExists("atlas");
2670
+ s.stop(exists ? "MongoDB Atlas CLI found" : pc.yellow("MongoDB Atlas CLI not found"));
2671
+ return exists;
2672
+ } catch (_error) {
2673
+ s.stop(pc.red("Error checking MongoDB Atlas CLI"));
2674
+ return false;
2675
+ }
2676
+ }
2677
+ async function initMongoDBAtlas(serverDir) {
2678
+ try {
2679
+ const hasAtlas = await checkAtlasCLI();
2680
+ if (!hasAtlas) {
2681
+ consola.error(pc.red("MongoDB Atlas CLI not found."));
2682
+ log.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/"));
2683
+ return null;
2684
+ }
2685
+ log.info(pc.blue("Running MongoDB Atlas setup..."));
2686
+ await execa("atlas", ["deployments", "setup"], {
2687
+ cwd: serverDir,
2688
+ stdio: "inherit"
2689
+ });
2690
+ log.info(pc.green("MongoDB Atlas deployment ready"));
2691
+ const connectionString = await text({
2692
+ message: "Enter your MongoDB connection string:",
2693
+ placeholder: "mongodb+srv://username:password@cluster.mongodb.net/database",
2694
+ validate(value) {
2695
+ if (!value) return "Please enter a connection string";
2696
+ if (!value.startsWith("mongodb")) return "URL should start with mongodb:// or mongodb+srv://";
2697
+ }
2698
+ });
2699
+ if (isCancel(connectionString)) {
2700
+ cancel("MongoDB setup cancelled");
2701
+ return null;
2702
+ }
2703
+ return { connectionString };
2704
+ } catch (error) {
2705
+ if (error instanceof Error) consola.error(pc.red(error.message));
2706
+ return null;
2707
+ }
2708
+ }
2709
+ async function writeEnvFile$3(projectDir, config) {
2710
+ try {
2711
+ const envPath = path.join(projectDir, "apps/server", ".env");
2712
+ const variables = [{
2713
+ key: "DATABASE_URL",
2714
+ value: config?.connectionString ?? "mongodb://localhost:27017/mydb",
2715
+ condition: true
2716
+ }];
2717
+ await addEnvVariablesToFile(envPath, variables);
2718
+ } catch (_error) {
2719
+ consola.error("Failed to update environment configuration");
2720
+ }
2721
+ }
2722
+ function displayManualSetupInstructions$3() {
2723
+ log.info(`
2724
+ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
2725
+
2726
+ 1. Install Atlas CLI:
2727
+ ${pc.blue("https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/")}
2728
+
2729
+ 2. Run the following command and follow the prompts:
2730
+ ${pc.blue("atlas deployments setup")}
2731
+
2732
+ 3. Get your connection string from the Atlas dashboard:
2733
+ Format: ${pc.dim("mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME")}
2734
+
2735
+ 4. Add the connection string to your .env file:
2736
+ ${pc.dim("DATABASE_URL=\"your_connection_string\"")}
2737
+ `);
2738
+ }
2739
+ async function setupMongoDBAtlas(config) {
2740
+ const { projectDir } = config;
2741
+ const mainSpinner = spinner();
2742
+ mainSpinner.start("Setting up MongoDB Atlas...");
2743
+ const serverDir = path.join(projectDir, "apps/server");
2744
+ try {
2745
+ await fs.ensureDir(serverDir);
2746
+ mainSpinner.stop("MongoDB Atlas setup ready");
2747
+ const config$1 = await initMongoDBAtlas(serverDir);
2748
+ if (config$1) {
2749
+ await writeEnvFile$3(projectDir, config$1);
2750
+ log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
2751
+ } else {
2752
+ log.warn(pc.yellow("Falling back to local MongoDB configuration"));
2753
+ await writeEnvFile$3(projectDir);
2754
+ displayManualSetupInstructions$3();
2755
+ }
2756
+ } catch (error) {
2757
+ mainSpinner.stop(pc.red("MongoDB Atlas setup failed"));
2758
+ consola.error(pc.red(`Error during MongoDB Atlas setup: ${error instanceof Error ? error.message : String(error)}`));
2759
+ try {
2760
+ await writeEnvFile$3(projectDir);
2761
+ displayManualSetupInstructions$3();
2762
+ } catch {}
2763
+ }
2764
+ }
2765
+
2766
+ //#endregion
2767
+ //#region src/helpers/database-providers/neon-setup.ts
2768
+ const NEON_REGIONS = [
2769
+ {
2770
+ label: "AWS US East (N. Virginia)",
2771
+ value: "aws-us-east-1"
2772
+ },
2773
+ {
2774
+ label: "AWS US East (Ohio)",
2775
+ value: "aws-us-east-2"
2776
+ },
2777
+ {
2778
+ label: "AWS US West (Oregon)",
2779
+ value: "aws-us-west-2"
2780
+ },
2781
+ {
2782
+ label: "AWS Europe (Frankfurt)",
2783
+ value: "aws-eu-central-1"
2784
+ },
2785
+ {
2786
+ label: "AWS Asia Pacific (Singapore)",
2787
+ value: "aws-ap-southeast-1"
2788
+ },
2789
+ {
2790
+ label: "AWS Asia Pacific (Sydney)",
2791
+ value: "aws-ap-southeast-2"
2792
+ },
2793
+ {
2794
+ label: "Azure East US 2 region (Virginia)",
2795
+ value: "azure-eastus2"
2796
+ }
2797
+ ];
2798
+ async function executeNeonCommand(packageManager, commandArgsString, spinnerText) {
2799
+ const s = spinner();
2800
+ try {
2801
+ const fullCommand = getPackageExecutionCommand(packageManager, commandArgsString);
2802
+ if (spinnerText) s.start(spinnerText);
2803
+ const result = await execa(fullCommand, { shell: true });
2804
+ if (spinnerText) s.stop(pc.green(spinnerText.replace("...", "").replace("ing ", "ed ").trim()));
2805
+ return result;
2806
+ } catch (error) {
2807
+ if (s) s.stop(pc.red(`Failed: ${spinnerText || "Command execution"}`));
2808
+ throw error;
2809
+ }
2810
+ }
2811
+ async function createNeonProject(projectName, regionId, packageManager) {
2812
+ try {
2813
+ const commandArgsString = `neonctl projects create --name ${projectName} --region-id ${regionId} --output json`;
2814
+ const { stdout } = await executeNeonCommand(packageManager, commandArgsString, `Creating Neon project "${projectName}"...`);
2815
+ const response = JSON.parse(stdout);
2816
+ if (response.project && response.connection_uris && response.connection_uris.length > 0) {
2817
+ const projectId = response.project.id;
2818
+ const connectionUri = response.connection_uris[0].connection_uri;
2819
+ const params = response.connection_uris[0].connection_parameters;
2820
+ return {
2821
+ connectionString: connectionUri,
2822
+ projectId,
2823
+ dbName: params.database,
2824
+ roleName: params.role
2825
+ };
2826
+ }
2827
+ consola$1.error(pc.red("Failed to extract connection information from response"));
2828
+ return null;
2829
+ } catch (_error) {
2830
+ consola$1.error(pc.red("Failed to create Neon project"));
2831
+ }
2832
+ }
2833
+ async function writeEnvFile$2(projectDir, config) {
2834
+ const envPath = path.join(projectDir, "apps/server", ".env");
2835
+ const variables = [{
2836
+ key: "DATABASE_URL",
2837
+ value: config?.connectionString ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
2838
+ condition: true
2839
+ }];
2840
+ await addEnvVariablesToFile(envPath, variables);
2841
+ return true;
2842
+ }
2843
+ async function setupWithNeonDb(projectDir, packageManager) {
2844
+ try {
2845
+ const s = spinner();
2846
+ s.start("Creating Neon database using neondb...");
2847
+ const serverDir = path.join(projectDir, "apps/server");
2848
+ await fs.ensureDir(serverDir);
2849
+ const packageCmd = getPackageExecutionCommand(packageManager, "neondb --yes");
2850
+ await execa(packageCmd, {
2851
+ shell: true,
2852
+ cwd: serverDir
2853
+ });
2854
+ s.stop(pc.green("Neon database created successfully!"));
2855
+ return true;
2856
+ } catch (error) {
2857
+ consola$1.error(pc.red("Failed to create database with neondb"));
2858
+ throw error;
2859
+ }
2860
+ }
2861
+ function displayManualSetupInstructions$2() {
2862
+ log.info(`Manual Neon PostgreSQL Setup Instructions:
2863
+
2864
+ 1. Visit https://neon.tech and create an account
2865
+ 2. Create a new project from the dashboard
2866
+ 3. Get your connection string
2867
+ 4. Add the database URL to the .env file in apps/server/.env
2868
+
2869
+ DATABASE_URL="your_connection_string"`);
2870
+ }
2871
+ async function setupNeonPostgres(config) {
2872
+ const { packageManager, projectDir } = config;
2873
+ try {
2874
+ const setupMethod = await select({
2875
+ message: "Choose your Neon setup method:",
2876
+ options: [{
2877
+ label: "Quick setup with neondb",
2878
+ value: "neondb",
2879
+ hint: "fastest, no auth required"
2880
+ }, {
2881
+ label: "Custom setup with neonctl",
2882
+ value: "neonctl",
2883
+ hint: "More control - choose project name and region"
2884
+ }],
2885
+ initialValue: "neondb"
2886
+ });
2887
+ if (isCancel(setupMethod)) {
2888
+ cancel(pc.red("Operation cancelled"));
2889
+ process.exit(0);
2890
+ }
2891
+ if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager);
2892
+ else {
2893
+ const suggestedProjectName = path.basename(projectDir);
2894
+ const projectName = await text({
2895
+ message: "Enter a name for your Neon project:",
2896
+ defaultValue: suggestedProjectName,
2897
+ initialValue: suggestedProjectName
2898
+ });
2899
+ const regionId = await select({
2900
+ message: "Select a region for your Neon project:",
2901
+ options: NEON_REGIONS,
2902
+ initialValue: NEON_REGIONS[0].value
2903
+ });
2904
+ if (isCancel(projectName) || isCancel(regionId)) {
2905
+ cancel(pc.red("Operation cancelled"));
2906
+ process.exit(0);
2907
+ }
2908
+ const neonConfig = await createNeonProject(projectName, regionId, packageManager);
2909
+ if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
2910
+ const finalSpinner = spinner();
2911
+ finalSpinner.start("Configuring database connection");
2912
+ await fs.ensureDir(path.join(projectDir, "apps/server"));
2913
+ await writeEnvFile$2(projectDir, neonConfig);
2914
+ finalSpinner.stop("Neon database configured!");
2915
+ }
2916
+ } catch (error) {
2917
+ if (error instanceof Error) consola$1.error(pc.red(error.message));
2918
+ await writeEnvFile$2(projectDir);
2919
+ displayManualSetupInstructions$2();
2920
+ }
2921
+ }
2922
+
2923
+ //#endregion
2924
+ //#region src/helpers/database-providers/prisma-postgres-setup.ts
2925
+ async function initPrismaDatabase(serverDir, packageManager) {
2926
+ const s = spinner();
2927
+ try {
2928
+ s.start("Initializing Prisma PostgreSQL...");
2929
+ const prismaDir = path.join(serverDir, "prisma");
2930
+ await fs.ensureDir(prismaDir);
2931
+ s.stop("Prisma PostgreSQL initialized. Follow the prompts below:");
2932
+ const prismaInitCommand = getPackageExecutionCommand(packageManager, "prisma init --db");
2933
+ await execa(prismaInitCommand, {
2934
+ cwd: serverDir,
2935
+ stdio: "inherit",
2936
+ shell: true
2937
+ });
2938
+ log.info(pc.yellow("Please copy the Prisma Postgres URL from the output above.\nIt looks like: prisma+postgres://accelerate.prisma-data.net/?api_key=..."));
2939
+ const databaseUrl = await password({
2940
+ message: "Paste your Prisma Postgres database URL:",
2941
+ validate(value) {
2942
+ if (!value) return "Please enter a database URL";
2943
+ if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://";
2944
+ }
2945
+ });
2946
+ if (isCancel(databaseUrl)) {
2947
+ cancel("Database setup cancelled");
2948
+ return null;
2949
+ }
2950
+ return { databaseUrl };
2951
+ } catch (error) {
2952
+ s.stop(pc.red("Prisma PostgreSQL initialization failed"));
2953
+ if (error instanceof Error) consola$1.error(error.message);
2954
+ return null;
2955
+ }
2956
+ }
2957
+ async function writeEnvFile$1(projectDir, config) {
2958
+ try {
2959
+ const envPath = path.join(projectDir, "apps/server", ".env");
2960
+ const variables = [{
2961
+ key: "DATABASE_URL",
2962
+ value: config?.databaseUrl ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
2963
+ condition: true
2964
+ }];
2965
+ await addEnvVariablesToFile(envPath, variables);
2966
+ } catch (_error) {
2967
+ consola$1.error("Failed to update environment configuration");
2968
+ }
2969
+ }
2970
+ function displayManualSetupInstructions$1() {
2971
+ log.info(`Manual Prisma PostgreSQL Setup Instructions:
2972
+
2973
+ 1. Visit https://console.prisma.io and create an account
2974
+ 2. Create a new PostgreSQL database from the dashboard
2975
+ 3. Get your database URL
2976
+ 4. Add the database URL to the .env file in apps/server/.env
2977
+
2978
+ DATABASE_URL="your_database_url"`);
2979
+ }
2980
+ async function addPrismaAccelerateExtension(serverDir) {
2981
+ try {
2982
+ await addPackageDependency({
2983
+ dependencies: ["@prisma/extension-accelerate"],
2984
+ projectDir: serverDir
2985
+ });
2986
+ const prismaIndexPath = path.join(serverDir, "prisma/index.ts");
2987
+ const prismaIndexContent = `
2988
+ import { PrismaClient } from "./generated/client";
2989
+ import { withAccelerate } from "@prisma/extension-accelerate";
2990
+
2991
+ const prisma = new PrismaClient().$extends(withAccelerate());
2992
+
2993
+ export default prisma;
2994
+ `;
2995
+ await fs.writeFile(prismaIndexPath, prismaIndexContent.trim());
2996
+ const dbFilePath = path.join(serverDir, "src/db/index.ts");
2997
+ if (await fs.pathExists(dbFilePath)) {
2998
+ let dbFileContent = await fs.readFile(dbFilePath, "utf8");
2999
+ if (!dbFileContent.includes("@prisma/extension-accelerate")) {
3000
+ dbFileContent = `import { withAccelerate } from "@prisma/extension-accelerate";\n${dbFileContent}`;
3001
+ dbFileContent = dbFileContent.replace("export const db = new PrismaClient();", "export const db = new PrismaClient().$extends(withAccelerate());");
3002
+ await fs.writeFile(dbFilePath, dbFileContent);
3003
+ }
3004
+ }
3005
+ return true;
3006
+ } catch (_error) {
3007
+ log.warn(pc.yellow("Could not add Prisma Accelerate extension automatically"));
3008
+ return false;
3009
+ }
3010
+ }
3011
+ async function setupPrismaPostgres(config) {
3012
+ const { packageManager, projectDir } = config;
3013
+ const serverDir = path.join(projectDir, "apps/server");
3014
+ const s = spinner();
3015
+ s.start("Setting up Prisma PostgreSQL...");
3016
+ try {
3017
+ await fs.ensureDir(serverDir);
3018
+ s.stop("Prisma PostgreSQL setup ready");
3019
+ const config$1 = await initPrismaDatabase(serverDir, packageManager);
3020
+ if (config$1) {
3021
+ await writeEnvFile$1(projectDir, config$1);
3022
+ await addPrismaAccelerateExtension(serverDir);
3023
+ log.success(pc.green("Prisma PostgreSQL database configured successfully!"));
3024
+ log.info(pc.cyan("NOTE: Make sure to uncomment `import \"dotenv/config\";` in `apps/server/src/prisma.config.ts` to load environment variables."));
3025
+ } else {
3026
+ const fallbackSpinner = spinner();
3027
+ fallbackSpinner.start("Setting up fallback configuration...");
3028
+ await writeEnvFile$1(projectDir);
3029
+ fallbackSpinner.stop("Fallback configuration ready");
3030
+ displayManualSetupInstructions$1();
3031
+ }
3032
+ } catch (error) {
3033
+ s.stop(pc.red("Prisma PostgreSQL setup failed"));
3034
+ consola$1.error(pc.red(`Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`));
3035
+ try {
3036
+ await writeEnvFile$1(projectDir);
3037
+ displayManualSetupInstructions$1();
3038
+ } catch {}
3039
+ log.info("Setup completed with manual configuration required.");
3040
+ }
3041
+ }
3042
+
3043
+ //#endregion
3044
+ //#region src/helpers/database-providers/supabase-setup.ts
3045
+ async function writeSupabaseEnvFile(projectDir, databaseUrl) {
3046
+ try {
3047
+ const envPath = path.join(projectDir, "apps/server", ".env");
3048
+ const dbUrlToUse = databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
3049
+ const variables = [{
3050
+ key: "DATABASE_URL",
3051
+ value: dbUrlToUse,
3052
+ condition: true
3053
+ }, {
3054
+ key: "DIRECT_URL",
3055
+ value: dbUrlToUse,
3056
+ condition: true
3057
+ }];
3058
+ await addEnvVariablesToFile(envPath, variables);
3059
+ return true;
3060
+ } catch (error) {
3061
+ consola$1.error(pc.red("Failed to update .env file for Supabase."));
3062
+ if (error instanceof Error) consola$1.error(error.message);
3063
+ return false;
3064
+ }
3065
+ }
3066
+ function extractDbUrl(output) {
3067
+ const dbUrlMatch = output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/);
3068
+ const url = dbUrlMatch?.[1];
3069
+ if (url) return url;
3070
+ return null;
3071
+ }
3072
+ async function initializeSupabase(serverDir, packageManager) {
3073
+ log.info("Initializing Supabase project...");
3074
+ try {
3075
+ const supabaseInitCommand = getPackageExecutionCommand(packageManager, "supabase init");
3076
+ await execa(supabaseInitCommand, {
3077
+ cwd: serverDir,
3078
+ stdio: "inherit",
3079
+ shell: true
3080
+ });
3081
+ log.success("Supabase project initialized");
3082
+ return true;
3083
+ } catch (error) {
3084
+ consola$1.error(pc.red("Failed to initialize Supabase project."));
3085
+ if (error instanceof Error) consola$1.error(error.message);
3086
+ else consola$1.error(String(error));
3087
+ if (error instanceof Error && error.message.includes("ENOENT")) {
3088
+ log.error(pc.red("Supabase CLI not found. Please install it globally or ensure it's in your PATH."));
3089
+ log.info("You can install it using: npm install -g supabase");
3090
+ }
3091
+ return false;
3092
+ }
3093
+ }
3094
+ async function startSupabase(serverDir, packageManager) {
3095
+ log.info("Starting Supabase services (this may take a moment)...");
3096
+ const supabaseStartCommand = getPackageExecutionCommand(packageManager, "supabase start");
3097
+ try {
3098
+ const subprocess = execa(supabaseStartCommand, {
3099
+ cwd: serverDir,
3100
+ shell: true
3101
+ });
3102
+ let stdoutData = "";
3103
+ if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
3104
+ const text$1 = data.toString();
3105
+ process.stdout.write(text$1);
3106
+ stdoutData += text$1;
3107
+ });
3108
+ if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
3109
+ await subprocess;
3110
+ await new Promise((resolve) => setTimeout(resolve, 100));
3111
+ return stdoutData;
3112
+ } catch (error) {
3113
+ consola$1.error(pc.red("Failed to start Supabase services."));
3114
+ const execaError = error;
3115
+ if (execaError?.message) {
3116
+ consola$1.error(`Error details: ${execaError.message}`);
3117
+ if (execaError.message.includes("Docker is not running")) log.error(pc.red("Docker is not running. Please start Docker and try again."));
3118
+ } else consola$1.error(String(error));
3119
+ return null;
3120
+ }
3121
+ }
3122
+ function displayManualSupabaseInstructions(output) {
3123
+ log.info(`"Manual Supabase Setup Instructions:"
3124
+ 1. Ensure Docker is installed and running.
3125
+ 2. Install the Supabase CLI (e.g., \`npm install -g supabase\`).
3126
+ 3. Run \`supabase init\` in your project's \`apps/server\` directory.
3127
+ 4. Run \`supabase start\` in your project's \`apps/server\` directory.
3128
+ 5. Copy the 'DB URL' from the output.${output ? `
3129
+ ${pc.bold("Relevant output from `supabase start`:")}
3130
+ ${pc.dim(output)}` : ""}
3131
+ 6. Add the DB URL to the .env file in \`apps/server/.env\` as \`DATABASE_URL\`:
3132
+ ${pc.gray("DATABASE_URL=\"your_supabase_db_url\"")}`);
3133
+ }
3134
+ async function setupSupabase(config) {
3135
+ const { projectDir, packageManager } = config;
3136
+ const serverDir = path.join(projectDir, "apps", "server");
3137
+ try {
3138
+ await fs.ensureDir(serverDir);
3139
+ const initialized = await initializeSupabase(serverDir, packageManager);
3140
+ if (!initialized) {
3141
+ displayManualSupabaseInstructions();
3142
+ return;
3143
+ }
3144
+ const supabaseOutput = await startSupabase(serverDir, packageManager);
3145
+ if (!supabaseOutput) {
3146
+ displayManualSupabaseInstructions();
3147
+ return;
3148
+ }
3149
+ const dbUrl = extractDbUrl(supabaseOutput);
3150
+ if (dbUrl) {
3151
+ const envUpdated = await writeSupabaseEnvFile(projectDir, dbUrl);
3152
+ if (envUpdated) log.success(pc.green("Supabase local development setup ready!"));
3153
+ else {
3154
+ log.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
3155
+ displayManualSupabaseInstructions(supabaseOutput);
3156
+ }
3157
+ } else {
3158
+ log.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
3159
+ displayManualSupabaseInstructions(supabaseOutput);
3160
+ }
3161
+ } catch (error) {
3162
+ if (error instanceof Error) consola$1.error(pc.red(`Error during Supabase setup: ${error.message}`));
3163
+ else consola$1.error(pc.red(`An unknown error occurred during Supabase setup: ${String(error)}`));
3164
+ displayManualSupabaseInstructions();
3165
+ }
3166
+ }
3167
+
3168
+ //#endregion
3169
+ //#region src/helpers/database-providers/turso-setup.ts
3170
+ async function isTursoInstalled() {
3171
+ return commandExists("turso");
3172
+ }
3173
+ async function isTursoLoggedIn() {
3174
+ try {
3175
+ const output = await $`turso auth whoami`;
3176
+ return !output.stdout.includes("You are not logged in");
3177
+ } catch {
3178
+ return false;
3179
+ }
3180
+ }
3181
+ async function loginToTurso() {
3182
+ const s = spinner();
3183
+ try {
3184
+ s.start("Logging in to Turso...");
3185
+ await $`turso auth login`;
3186
+ s.stop("Logged into Turso");
3187
+ return true;
3188
+ } catch (_error) {
3189
+ s.stop(pc.red("Failed to log in to Turso"));
3190
+ }
3191
+ }
3192
+ async function installTursoCLI(isMac) {
3193
+ const s = spinner();
3194
+ try {
3195
+ s.start("Installing Turso CLI...");
3196
+ if (isMac) await $`brew install tursodatabase/tap/turso`;
3197
+ else {
3198
+ const { stdout: installScript } = await $`curl -sSfL https://get.tur.so/install.sh`;
3199
+ await $`bash -c '${installScript}'`;
3200
+ }
3201
+ s.stop("Turso CLI installed");
3202
+ return true;
3203
+ } catch (error) {
3204
+ if (error instanceof Error && error.message.includes("User force closed")) {
3205
+ s.stop("Turso CLI installation cancelled");
3206
+ log.warn(pc.yellow("Turso CLI installation cancelled by user"));
3207
+ throw new Error("Installation cancelled");
3208
+ }
3209
+ s.stop(pc.red("Failed to install Turso CLI"));
3210
+ }
3211
+ }
3212
+ async function getTursoGroups() {
3213
+ const s = spinner();
3214
+ try {
3215
+ s.start("Fetching Turso groups...");
3216
+ const { stdout } = await $`turso group list`;
3217
+ const lines = stdout.trim().split("\n");
3218
+ if (lines.length <= 1) {
3219
+ s.stop("No Turso groups found");
3220
+ return [];
3221
+ }
3222
+ const groups = lines.slice(1).map((line) => {
3223
+ const [name, locations, version, status] = line.trim().split(/\s{2,}/);
3224
+ return {
3225
+ name,
3226
+ locations,
3227
+ version,
3228
+ status
3229
+ };
3230
+ });
3231
+ s.stop(`Found ${groups.length} Turso groups`);
3232
+ return groups;
3233
+ } catch (error) {
3234
+ s.stop(pc.red("Error fetching Turso groups"));
3235
+ console.error("Error fetching Turso groups:", error);
3236
+ return [];
3237
+ }
3238
+ }
3239
+ async function selectTursoGroup() {
3240
+ const groups = await getTursoGroups();
3241
+ if (groups.length === 0) return null;
3242
+ if (groups.length === 1) {
3243
+ log.info(`Using the only available group: ${pc.blue(groups[0].name)}`);
3244
+ return groups[0].name;
3245
+ }
3246
+ const groupOptions = groups.map((group$1) => ({
3247
+ value: group$1.name,
3248
+ label: `${group$1.name} (${group$1.locations})`
3249
+ }));
3250
+ const selectedGroup = await select({
3251
+ message: "Select a Turso database group:",
3252
+ options: groupOptions
3253
+ });
3254
+ if (isCancel(selectedGroup)) {
3255
+ cancel(pc.red("Operation cancelled"));
3256
+ process.exit(0);
3257
+ }
3258
+ return selectedGroup;
3259
+ }
3260
+ async function createTursoDatabase(dbName, groupName) {
3261
+ const s = spinner();
3262
+ try {
3263
+ s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`);
3264
+ if (groupName) await $`turso db create ${dbName} --group ${groupName}`;
3265
+ else await $`turso db create ${dbName}`;
3266
+ s.stop(`Turso database "${dbName}" created`);
3267
+ } catch (error) {
3268
+ s.stop(pc.red(`Failed to create database "${dbName}"`));
3269
+ if (error instanceof Error && error.message.includes("already exists")) throw new Error("DATABASE_EXISTS");
3270
+ }
3271
+ s.start("Retrieving database connection details...");
3272
+ try {
3273
+ const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
3274
+ const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
3275
+ s.stop("Database connection details retrieved");
3276
+ return {
3277
+ dbUrl: dbUrl.trim(),
3278
+ authToken: authToken.trim()
3279
+ };
3280
+ } catch (_error) {
3281
+ s.stop(pc.red("Failed to retrieve database connection details"));
3282
+ }
3283
+ }
3284
+ async function writeEnvFile(projectDir, config) {
3285
+ const envPath = path.join(projectDir, "apps/server", ".env");
3286
+ const variables = [{
3287
+ key: "DATABASE_URL",
3288
+ value: config?.dbUrl ?? "",
3289
+ condition: true
3290
+ }, {
3291
+ key: "DATABASE_AUTH_TOKEN",
3292
+ value: config?.authToken ?? "",
3293
+ condition: true
3294
+ }];
3295
+ await addEnvVariablesToFile(envPath, variables);
3296
+ }
3297
+ function displayManualSetupInstructions() {
3298
+ log.info(`Manual Turso Setup Instructions:
3299
+
3300
+ 1. Visit https://turso.tech and create an account
3301
+ 2. Create a new database from the dashboard
3302
+ 3. Get your database URL and authentication token
3303
+ 4. Add these credentials to the .env file in apps/server/.env
3304
+
3305
+ DATABASE_URL=your_database_url
3306
+ DATABASE_AUTH_TOKEN=your_auth_token`);
3307
+ }
3308
+ async function setupTurso(config) {
3309
+ const { orm, projectDir } = config;
3310
+ const _isDrizzle = orm === "drizzle";
3311
+ const setupSpinner = spinner();
3312
+ setupSpinner.start("Checking Turso CLI availability...");
3313
+ try {
3314
+ const platform = os.platform();
3315
+ const isMac = platform === "darwin";
3316
+ const _isLinux = platform === "linux";
3317
+ const isWindows = platform === "win32";
3318
+ if (isWindows) {
3319
+ setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
3320
+ log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
3321
+ await writeEnvFile(projectDir);
3322
+ displayManualSetupInstructions();
3323
+ return;
3324
+ }
3325
+ setupSpinner.stop("Turso CLI availability checked");
3326
+ const isCliInstalled = await isTursoInstalled();
3327
+ if (!isCliInstalled) {
3328
+ const shouldInstall = await confirm({
3329
+ message: "Would you like to install Turso CLI?",
3330
+ initialValue: true
3331
+ });
3332
+ if (isCancel(shouldInstall)) {
3333
+ cancel(pc.red("Operation cancelled"));
3334
+ process.exit(0);
3335
+ }
3336
+ if (!shouldInstall) {
3337
+ await writeEnvFile(projectDir);
3338
+ displayManualSetupInstructions();
3339
+ return;
3340
+ }
3341
+ await installTursoCLI(isMac);
3342
+ }
3343
+ const isLoggedIn = await isTursoLoggedIn();
3344
+ if (!isLoggedIn) await loginToTurso();
3345
+ const selectedGroup = await selectTursoGroup();
3346
+ let success = false;
3347
+ let dbName = "";
3348
+ let suggestedName = path.basename(projectDir);
3349
+ while (!success) {
3350
+ const dbNameResponse = await text({
3351
+ message: "Enter a name for your database:",
3352
+ defaultValue: suggestedName,
3353
+ initialValue: suggestedName,
3354
+ placeholder: suggestedName
3355
+ });
3356
+ if (isCancel(dbNameResponse)) {
3357
+ cancel(pc.red("Operation cancelled"));
3358
+ process.exit(0);
3359
+ }
3360
+ dbName = dbNameResponse;
3361
+ try {
3362
+ const config$1 = await createTursoDatabase(dbName, selectedGroup);
3363
+ await writeEnvFile(projectDir, config$1);
3364
+ success = true;
3365
+ } catch (error) {
3366
+ if (error instanceof Error && error.message === "DATABASE_EXISTS") {
3367
+ log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
3368
+ suggestedName = `${dbName}-${Math.floor(Math.random() * 1e3)}`;
3369
+ } else throw error;
3370
+ }
3371
+ }
3372
+ log.success("Turso database setup completed successfully!");
3373
+ } catch (error) {
3374
+ setupSpinner.stop(pc.red("Turso CLI availability check failed"));
3375
+ consola.error(pc.red(`Error during Turso setup: ${error instanceof Error ? error.message : String(error)}`));
3376
+ await writeEnvFile(projectDir);
3377
+ displayManualSetupInstructions();
3378
+ log.success("Setup completed with manual configuration required.");
3379
+ }
3380
+ }
3381
+
3382
+ //#endregion
3383
+ //#region src/helpers/setup/db-setup.ts
3384
+ async function setupDatabase(config) {
3385
+ const { database, orm, dbSetup, backend, projectDir } = config;
3386
+ if (backend === "convex" || database === "none") {
3387
+ if (backend !== "convex") {
3388
+ const serverDir$1 = path.join(projectDir, "apps/server");
3389
+ const serverDbDir = path.join(serverDir$1, "src/db");
3390
+ if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
3391
+ }
3392
+ return;
3393
+ }
3394
+ const s = spinner();
3395
+ const serverDir = path.join(projectDir, "apps/server");
3396
+ if (!await fs.pathExists(serverDir)) return;
3397
+ try {
3398
+ if (orm === "prisma") await addPackageDependency({
3399
+ dependencies: ["@prisma/client"],
3400
+ devDependencies: ["prisma"],
3401
+ projectDir: serverDir
3402
+ });
3403
+ else if (orm === "drizzle") {
3404
+ if (database === "sqlite") await addPackageDependency({
3405
+ dependencies: ["drizzle-orm", "@libsql/client"],
3406
+ devDependencies: ["drizzle-kit"],
3407
+ projectDir: serverDir
3408
+ });
3409
+ else if (database === "postgres") await addPackageDependency({
3410
+ dependencies: ["drizzle-orm", "pg"],
3411
+ devDependencies: ["drizzle-kit", "@types/pg"],
3412
+ projectDir: serverDir
3413
+ });
3414
+ else if (database === "mysql") await addPackageDependency({
3415
+ dependencies: ["drizzle-orm", "mysql2"],
3416
+ devDependencies: ["drizzle-kit"],
3417
+ projectDir: serverDir
3418
+ });
3419
+ } else if (orm === "mongoose") await addPackageDependency({
3420
+ dependencies: ["mongoose"],
3421
+ devDependencies: [],
3422
+ projectDir: serverDir
3423
+ });
3424
+ if (database === "sqlite" && dbSetup === "turso") await setupTurso(config);
3425
+ else if (database === "sqlite" && dbSetup === "d1") await setupCloudflareD1(config);
3426
+ else if (database === "postgres") {
3427
+ if (orm === "prisma" && dbSetup === "prisma-postgres") await setupPrismaPostgres(config);
3428
+ else if (dbSetup === "neon") await setupNeonPostgres(config);
3429
+ else if (dbSetup === "supabase") await setupSupabase(config);
3430
+ } else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config);
3431
+ } catch (error) {
3432
+ s.stop(pc.red("Failed to set up database"));
3433
+ if (error instanceof Error) consola.error(pc.red(error.message));
3434
+ }
3435
+ }
3436
+
3437
+ //#endregion
3438
+ //#region src/helpers/setup/examples-setup.ts
3439
+ async function setupExamples(config) {
3440
+ const { examples, frontend, backend, projectDir } = config;
3441
+ if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
3442
+ if (examples.includes("ai")) {
3443
+ const clientDir = path.join(projectDir, "apps/web");
3444
+ const serverDir = path.join(projectDir, "apps/server");
3445
+ const clientDirExists = await fs.pathExists(clientDir);
3446
+ const serverDirExists = await fs.pathExists(serverDir);
3447
+ const hasNuxt = frontend.includes("nuxt");
3448
+ const hasSvelte = frontend.includes("svelte");
3449
+ const hasReact = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start") || frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3450
+ if (clientDirExists) {
3451
+ const dependencies = ["ai"];
3452
+ if (hasNuxt) dependencies.push("@ai-sdk/vue");
3453
+ else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
3454
+ else if (hasReact) dependencies.push("@ai-sdk/react");
3455
+ await addPackageDependency({
3456
+ dependencies,
3457
+ projectDir: clientDir
3458
+ });
3459
+ }
3460
+ if (serverDirExists && backend !== "none") await addPackageDependency({
3461
+ dependencies: ["ai", "@ai-sdk/google"],
3462
+ projectDir: serverDir
3463
+ });
3464
+ }
3465
+ }
3466
+
3467
+ //#endregion
3468
+ //#region src/helpers/setup/runtime-setup.ts
3469
+ async function setupRuntime(config) {
3470
+ const { runtime, backend, projectDir } = config;
3471
+ if (backend === "convex" || backend === "next" || runtime === "none") return;
3472
+ const serverDir = path.join(projectDir, "apps/server");
3473
+ if (!await fs.pathExists(serverDir)) return;
3474
+ if (runtime === "bun") await setupBunRuntime(serverDir, backend);
3475
+ else if (runtime === "node") await setupNodeRuntime(serverDir, backend);
3476
+ else if (runtime === "workers") await setupWorkersRuntime(serverDir);
3477
+ }
3478
+ async function generateCloudflareWorkerTypes(config) {
3479
+ if (config.runtime !== "workers") return;
3480
+ const serverDir = path.join(config.projectDir, "apps/server");
3481
+ if (!await fs.pathExists(serverDir)) return;
3482
+ const s = spinner();
3483
+ try {
3484
+ s.start("Generating Cloudflare Workers types...");
3485
+ const runCmd = config.packageManager === "npm" ? "npm" : config.packageManager;
3486
+ await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
3487
+ s.stop("Cloudflare Workers types generated successfully!");
3488
+ } catch {
3489
+ s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
3490
+ const managerCmd = config.packageManager === "npm" ? "npm run" : `${config.packageManager} run`;
3491
+ console.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
3492
+ }
3493
+ }
3494
+ async function setupBunRuntime(serverDir, _backend) {
3495
+ const packageJsonPath = path.join(serverDir, "package.json");
3496
+ if (!await fs.pathExists(packageJsonPath)) return;
3497
+ const packageJson = await fs.readJson(packageJsonPath);
3498
+ packageJson.scripts = {
3499
+ ...packageJson.scripts,
3500
+ dev: "bun run --hot src/index.ts",
3501
+ start: "bun run dist/src/index.js"
3502
+ };
3503
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3504
+ await addPackageDependency({
3505
+ devDependencies: ["@types/bun"],
3506
+ projectDir: serverDir
3507
+ });
3508
+ }
3509
+ async function setupNodeRuntime(serverDir, backend) {
3510
+ const packageJsonPath = path.join(serverDir, "package.json");
3511
+ if (!await fs.pathExists(packageJsonPath)) return;
3512
+ const packageJson = await fs.readJson(packageJsonPath);
3513
+ packageJson.scripts = {
3514
+ ...packageJson.scripts,
3515
+ dev: "tsx watch src/index.ts",
3516
+ start: "node dist/src/index.js"
3517
+ };
3518
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3519
+ await addPackageDependency({
3520
+ devDependencies: ["tsx", "@types/node"],
3521
+ projectDir: serverDir
3522
+ });
3523
+ if (backend === "hono") await addPackageDependency({
3524
+ dependencies: ["@hono/node-server"],
3525
+ projectDir: serverDir
3526
+ });
3527
+ else if (backend === "elysia") await addPackageDependency({
3528
+ dependencies: ["@elysiajs/node"],
3529
+ projectDir: serverDir
3530
+ });
3531
+ }
3532
+ async function setupWorkersRuntime(serverDir) {
3533
+ const packageJsonPath = path.join(serverDir, "package.json");
3534
+ if (!await fs.pathExists(packageJsonPath)) return;
3535
+ const packageJson = await fs.readJson(packageJsonPath);
3536
+ packageJson.scripts = {
3537
+ ...packageJson.scripts,
3538
+ dev: "wrangler dev --port=3000",
3539
+ start: "wrangler dev",
3540
+ deploy: "wrangler deploy",
3541
+ build: "wrangler deploy --dry-run",
3542
+ "cf-typegen": "wrangler types --env-interface CloudflareBindings"
3543
+ };
3544
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3545
+ await addPackageDependency({
3546
+ devDependencies: ["wrangler"],
3547
+ projectDir: serverDir
3548
+ });
3549
+ }
3550
+
3551
+ //#endregion
3552
+ //#region src/helpers/project-generation/create-readme.ts
3553
+ async function createReadme(projectDir, options) {
3554
+ const readmePath = path.join(projectDir, "README.md");
3555
+ const content = generateReadmeContent(options);
3556
+ try {
3557
+ await fs.writeFile(readmePath, content);
3558
+ } catch (error) {
3559
+ consola.error("Failed to create README.md file:", error);
3560
+ }
3561
+ }
3562
+ function generateReadmeContent(options) {
3563
+ const { projectName, packageManager, database, auth, addons = [], orm = "drizzle", runtime = "bun", frontend = ["tanstack-router"], backend = "hono", api = "trpc" } = options;
3564
+ const isConvex = backend === "convex";
3565
+ const hasReactRouter = frontend.includes("react-router");
3566
+ const hasTanstackRouter = frontend.includes("tanstack-router");
3567
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3568
+ const hasNext = frontend.includes("next");
3569
+ const hasTanstackStart = frontend.includes("tanstack-start");
3570
+ const hasSvelte = frontend.includes("svelte");
3571
+ const hasSolid = frontend.includes("solid");
3572
+ const hasNuxt = frontend.includes("nuxt");
3573
+ const packageManagerRunCmd = packageManager === "npm" ? "npm run" : packageManager;
3574
+ let webPort = "3001";
3575
+ if (hasReactRouter || hasSvelte) webPort = "5173";
3576
+ return `# ${projectName}
3577
+
3578
+ This project was created with [tvi](https://github.com//shaneholloman/tvi), a modern TypeScript stack that combines ${hasTanstackRouter ? "React, TanStack Router" : hasReactRouter ? "React, React Router" : hasNext ? "Next.js" : hasTanstackStart ? "React, TanStack Start" : hasSvelte ? "SvelteKit" : hasNuxt ? "Nuxt" : hasSolid ? "SolidJS" : ""}, ${backend[0].toUpperCase() + backend.slice(1)}${isConvex ? "" : `, ${api.toUpperCase()}`}, and more.
3579
+
3580
+ ## Features
3581
+
3582
+ ${generateFeaturesList(database, auth, addons, orm, runtime, frontend, backend, api)}
3583
+
3584
+ ## Getting Started
3585
+
3586
+ First, install the dependencies:
3587
+
3588
+ \`\`\`bash
3589
+ ${packageManager} install
3590
+ \`\`\`
3591
+ ${isConvex ? `
3592
+ ## Convex Setup
3593
+
3594
+ This project uses Convex as a backend. You'll need to set up Convex before running the app:
3595
+
3596
+ \`\`\`bash
3597
+ ${packageManagerRunCmd} dev:setup
3598
+ \`\`\`
3599
+
3600
+ Follow the prompts to create a new Convex project and connect it to your application.` : generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)}
3601
+
3602
+ Then, run the development server:
3603
+
3604
+ \`\`\`bash
3605
+ ${packageManagerRunCmd} dev
3606
+ \`\`\`
3607
+
3608
+ ${hasTanstackRouter || hasReactRouter || hasNext || hasTanstackStart || hasSvelte || hasNuxt || hasSolid ? `Open [http://localhost:${webPort}](http://localhost:${webPort}) in your browser to see the web application.` : ""}
3609
+ ${hasNative ? "Use the Expo Go app to run the mobile application.\n" : ""}
3610
+ ${isConvex ? "Your app will connect to the Convex cloud backend automatically." : "The API is running at [http://localhost:3000](http://localhost:3000)."}
3611
+
3612
+ ${addons.includes("pwa") && hasReactRouter ? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n" : ""}
3613
+
3614
+ ## Project Structure
3615
+
3616
+ \`\`\`
3617
+ ${projectName}/
3618
+ ├── apps/
3619
+ ${hasTanstackRouter || hasReactRouter || hasNext || hasTanstackStart || hasSvelte || hasNuxt || hasSolid ? `│ ├── web/ # Frontend application (${hasTanstackRouter ? "React + TanStack Router" : hasReactRouter ? "React + React Router" : hasNext ? "Next.js" : hasTanstackStart ? "React + TanStack Start" : hasSvelte ? "SvelteKit" : hasNuxt ? "Nuxt" : hasSolid ? "SolidJS" : ""})\n` : ""}${hasNative ? "│ ├── native/ # Mobile application (React Native, Expo)\n" : ""}${addons.includes("starlight") ? "│ ├── docs/ # Documentation site (Astro Starlight)\n" : ""}${isConvex ? "├── packages/\n│ └── backend/ # Convex backend functions and schema\n" : `│ └── server/ # Backend API (${backend[0].toUpperCase() + backend.slice(1)}, ${api.toUpperCase()})`}
3620
+ \`\`\`
3621
+
3622
+ ## Available Scripts
3623
+
3624
+ ${generateScriptsList(packageManagerRunCmd, database, orm, auth, hasNative, addons, backend)}
3625
+ `;
3626
+ }
3627
+ function generateFeaturesList(database, auth, addons, orm, runtime, frontend, backend, api) {
3628
+ const isConvex = backend === "convex";
3629
+ const hasTanstackRouter = frontend.includes("tanstack-router");
3630
+ const hasReactRouter = frontend.includes("react-router");
3631
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3632
+ const hasNext = frontend.includes("next");
3633
+ const hasTanstackStart = frontend.includes("tanstack-start");
3634
+ const hasSvelte = frontend.includes("svelte");
3635
+ const hasNuxt = frontend.includes("nuxt");
3636
+ const hasSolid = frontend.includes("solid");
3637
+ const addonsList = ["- **TypeScript** - For type safety and improved developer experience"];
3638
+ if (hasTanstackRouter) addonsList.push("- **TanStack Router** - File-based routing with full type safety");
3639
+ else if (hasReactRouter) addonsList.push("- **React Router** - Declarative routing for React");
3640
+ else if (hasNext) addonsList.push("- **Next.js** - Full-stack React framework");
3641
+ else if (hasTanstackStart) addonsList.push("- **TanStack Start** - SSR framework with TanStack Router");
3642
+ else if (hasSvelte) addonsList.push("- **SvelteKit** - Web framework for building Svelte apps");
3643
+ else if (hasNuxt) addonsList.push("- **Nuxt** - The Intuitive Vue Framework");
3644
+ else if (hasSolid) addonsList.push("- **SolidJS** - Simple and performant reactivity");
3645
+ if (hasNative) {
3646
+ addonsList.push("- **React Native** - Build mobile apps using React");
3647
+ addonsList.push("- **Expo** - Tools for React Native development");
3648
+ }
3649
+ addonsList.push("- **TailwindCSS** - Utility-first CSS for rapid UI development", "- **shadcn/ui** - Reusable UI components");
3650
+ if (isConvex) addonsList.push("- **Convex** - Reactive backend-as-a-service platform");
3651
+ else {
3652
+ if (backend === "hono") addonsList.push("- **Hono** - Lightweight, performant server framework");
3653
+ else if (backend === "express") addonsList.push("- **Express** - Fast, unopinionated web framework");
3654
+ else if (backend === "fastify") addonsList.push("- **Fastify** - Fast, low-overhead web framework");
3655
+ else if (backend === "elysia") addonsList.push("- **Elysia** - Type-safe, high-performance framework");
3656
+ else if (backend === "next") addonsList.push("- **Next.js** - Full-stack React framework");
3657
+ if (api === "trpc") addonsList.push("- **tRPC** - End-to-end type-safe APIs");
3658
+ else if (api === "orpc") addonsList.push("- **oRPC** - End-to-end type-safe APIs with OpenAPI integration");
3659
+ addonsList.push(`- **${runtime === "bun" ? "Bun" : "Node.js"}** - Runtime environment`);
3660
+ }
3661
+ if (database !== "none" && !isConvex) addonsList.push(`- **${orm === "drizzle" ? "Drizzle" : orm === "prisma" ? "Prisma" : "Mongoose"}** - TypeScript-first ORM`, `- **${database === "sqlite" ? "SQLite/Turso" : database === "postgres" ? "PostgreSQL" : database === "mysql" ? "MySQL" : "MongoDB"}** - Database engine`);
3662
+ if (auth && !isConvex) addonsList.push("- **Authentication** - Email & password authentication with Better Auth");
3663
+ for (const addon of addons) if (addon === "pwa") addonsList.push("- **PWA** - Progressive Web App support");
3664
+ else if (addon === "tauri") addonsList.push("- **Tauri** - Build native desktop applications");
3665
+ else if (addon === "biome") addonsList.push("- **Biome** - Linting and formatting");
3666
+ else if (addon === "husky") addonsList.push("- **Husky** - Git hooks for code quality");
3667
+ else if (addon === "starlight") addonsList.push("- **Starlight** - Documentation site with Astro");
3668
+ else if (addon === "turborepo") addonsList.push("- **Turborepo** - Optimized monorepo build system");
3669
+ return addonsList.join("\n");
3670
+ }
3671
+ function generateDatabaseSetup(database, auth, packageManagerRunCmd, orm) {
3672
+ if (database === "none") return "";
3673
+ let setup = "## Database Setup\n\n";
3674
+ if (database === "sqlite") setup += `This project uses SQLite${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
3675
+
3676
+ 1. Start the local SQLite database:
3677
+ \`\`\`bash
3678
+ cd apps/server && ${packageManagerRunCmd} db:local
3679
+ \`\`\`
3680
+
3681
+ 2. Update your \`.env\` file in the \`apps/server\` directory with the appropriate connection details if needed.
3682
+ `;
3683
+ else if (database === "postgres") setup += `This project uses PostgreSQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
3684
+
3685
+ 1. Make sure you have a PostgreSQL database set up.
3686
+ 2. Update your \`apps/server/.env\` file with your PostgreSQL connection details.
3687
+ `;
3688
+ else if (database === "mysql") setup += `This project uses MySQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
3689
+
3690
+ 1. Make sure you have a MySQL database set up.
3691
+ 2. Update your \`apps/server/.env\` file with your MySQL connection details.
3692
+ `;
3693
+ else if (database === "mongodb") setup += `This project uses MongoDB ${orm === "mongoose" ? "with Mongoose" : "with Prisma ORM"}.
3694
+
3695
+ 1. Make sure you have MongoDB set up.
3696
+ 2. Update your \`apps/server/.env\` file with your MongoDB connection URI.
3697
+ `;
3698
+ setup += `
3699
+ ${auth ? "3" : "3"}. ${orm === "prisma" ? `Generate the Prisma client and push the schema:
3700
+ \`\`\`bash
3701
+ ${packageManagerRunCmd} db:push
3702
+ \`\`\`` : orm === "drizzle" ? `Apply the schema to your database:
3703
+ \`\`\`bash
3704
+ ${packageManagerRunCmd} db:push
3705
+ \`\`\`` : `Apply the schema to your database:
3706
+ \`\`\`bash
3707
+ ${packageManagerRunCmd} db:push
3708
+ \`\`\``}
3709
+ `;
3710
+ return setup;
3711
+ }
3712
+ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNative, addons, backend) {
3713
+ const isConvex = backend === "convex";
3714
+ let scripts = `- \`${packageManagerRunCmd} dev\`: Start all applications in development mode
3715
+ - \`${packageManagerRunCmd} build\`: Build all applications
3716
+ - \`${packageManagerRunCmd} dev:web\`: Start only the web application`;
3717
+ if (isConvex) scripts += `
3718
+ - \`${packageManagerRunCmd} dev:setup\`: Setup and configure your Convex project`;
3719
+ else scripts += `
3720
+ - \`${packageManagerRunCmd} dev:server\`: Start only the server`;
3721
+ scripts += `
3722
+ - \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all apps`;
3723
+ if (hasNative) scripts += `
3724
+ - \`${packageManagerRunCmd} dev:native\`: Start the React Native/Expo development server`;
3725
+ if (database !== "none" && !isConvex) {
3726
+ scripts += `
3727
+ - \`${packageManagerRunCmd} db:push\`: Push schema changes to database
3728
+ - \`${packageManagerRunCmd} db:studio\`: Open database studio UI`;
3729
+ if (database === "sqlite" && orm === "drizzle") scripts += `
3730
+ - \`cd apps/server && ${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
3731
+ }
3732
+ if (addons.includes("biome")) scripts += `
3733
+ - \`${packageManagerRunCmd} check\`: Run Biome formatting and linting`;
3734
+ if (addons.includes("pwa")) scripts += `
3735
+ - \`cd apps/web && ${packageManagerRunCmd} generate-pwa-assets\`: Generate PWA assets`;
3736
+ if (addons.includes("tauri")) scripts += `
3737
+ - \`cd apps/web && ${packageManagerRunCmd} desktop:dev\`: Start Tauri desktop app in development
3738
+ - \`cd apps/web && ${packageManagerRunCmd} desktop:build\`: Build Tauri desktop app`;
3739
+ if (addons.includes("starlight")) scripts += `
3740
+ - \`cd apps/docs && ${packageManagerRunCmd} dev\`: Start documentation site
3741
+ - \`cd apps/docs && ${packageManagerRunCmd} build\`: Build documentation site`;
3742
+ return scripts;
3743
+ }
3744
+
3745
+ //#endregion
3746
+ //#region src/helpers/project-generation/post-installation.ts
3747
+ function displayPostInstallInstructions(config) {
3748
+ const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup } = config;
3749
+ const isConvex = backend === "convex";
3750
+ const runCmd = packageManager === "npm" ? "npm run" : packageManager;
3751
+ const cdCmd = `cd ${relativePath}`;
3752
+ const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
3753
+ const databaseInstructions = !isConvex && database !== "none" ? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
3754
+ const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
3755
+ const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
3756
+ const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
3757
+ const pwaInstructions = addons?.includes("pwa") && (frontend?.includes("react-router") || frontend?.includes("tanstack-router")) ? getPwaInstructions() : "";
3758
+ const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
3759
+ const hasWeb = frontend?.some((f) => [
3760
+ "tanstack-router",
3761
+ "react-router",
3762
+ "next",
3763
+ "tanstack-start",
3764
+ "nuxt",
3765
+ "svelte",
3766
+ "solid"
3767
+ ].includes(f));
3768
+ const hasNative = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles");
3769
+ const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
3770
+ const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
3771
+ const hasReactRouter = frontend?.includes("react-router");
3772
+ const hasSvelte = frontend?.includes("svelte");
3773
+ const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
3774
+ const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
3775
+ let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
3776
+ let stepCounter = 2;
3777
+ if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
3778
+ if (isConvex) {
3779
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup ${pc.dim("(this will guide you through Convex project setup)")}\n`;
3780
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
3781
+ } else {
3782
+ if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
3783
+ if (runtime === "workers") {
3784
+ if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first (see Database commands below)\n`;
3785
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
3786
+ output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} run cf-typegen\n\n`;
3787
+ } else output += "\n";
3788
+ }
3789
+ output += `${pc.bold("Your project will be available at:")}\n`;
3790
+ if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
3791
+ else if (!hasNative && !addons?.includes("starlight")) output += `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`;
3792
+ if (!isConvex) output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
3793
+ if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
3794
+ if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
3795
+ if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
3796
+ if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
3797
+ if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
3798
+ if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
3799
+ if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
3800
+ if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
3801
+ if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
3802
+ output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}\n\n`;
3803
+ output += `${pc.bold("Like tvi?")} Please consider giving us a star on GitHub:\n`;
3804
+ output += pc.cyan("https://github.com//shaneholloman/tvi");
3805
+ consola$1.box(output);
3806
+ }
3807
+ function getNativeInstructions(isConvex) {
3808
+ const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
3809
+ const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : "http://<YOUR_LOCAL_IP>:3000";
3810
+ const envFileName = ".env";
3811
+ const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
3812
+ let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`;
3813
+ if (isConvex) instructions += `\n${pc.yellow("IMPORTANT:")} When using local development with Convex and native apps, ensure you use your local IP address \ninstead of localhost or 127.0.0.1 for proper connectivity.\n`;
3814
+ return instructions;
3815
+ }
3816
+ function getLintingInstructions(runCmd) {
3817
+ return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
3818
+ }
3819
+ function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) {
3820
+ const instructions = [];
3821
+ if (runtime === "workers" && dbSetup === "d1") {
3822
+ const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
3823
+ instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
3824
+ instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
3825
+ instructions.push(`${pc.cyan("3.")} Update apps/server/wrangler.jsonc with database_id and database_name`);
3826
+ instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white("cd apps/server && bun db:generate")}`);
3827
+ instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
3828
+ instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
3829
+ instructions.push("");
3830
+ }
3831
+ if (orm === "prisma") {
3832
+ if (database === "sqlite") instructions.push(`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`, `Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso`);
3833
+ if (runtime === "bun") instructions.push(`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`);
3834
+ instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
3835
+ instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
3836
+ } else if (orm === "drizzle") {
3837
+ instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
3838
+ instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
3839
+ if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
3840
+ } else if (orm === "none") instructions.push(`${pc.yellow("NOTE:")} Manual database schema setup required.`);
3841
+ return instructions.length ? `${pc.bold("Database commands:")}\n${instructions.join("\n")}` : "";
3842
+ }
3843
+ function getTauriInstructions(runCmd) {
3844
+ return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\nSee: https://v2.tauri.app/start/prerequisites/`;
3845
+ }
3846
+ function getPwaInstructions() {
3847
+ return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
3848
+ }
3849
+ function getStarlightInstructions(runCmd) {
3850
+ return `\n${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}`;
3851
+ }
3852
+ function getNoOrmWarning() {
3853
+ return `\n${pc.yellow("WARNING:")} Database selected without an ORM. Features requiring database access (e.g., examples, auth) need manual setup.`;
3854
+ }
3855
+ function getBunWebNativeWarning() {
3856
+ return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
3857
+ }
3858
+
3859
+ //#endregion
3860
+ //#region src/helpers/project-generation/project-config.ts
3861
+ async function updatePackageConfigurations(projectDir, options) {
3862
+ await updateRootPackageJson(projectDir, options);
3863
+ if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
3864
+ else await updateConvexPackageJson(projectDir, options);
3865
+ }
3866
+ async function updateRootPackageJson(projectDir, options) {
3867
+ const rootPackageJsonPath = path.join(projectDir, "package.json");
3868
+ if (!await fs.pathExists(rootPackageJsonPath)) return;
3869
+ const packageJson = await fs.readJson(rootPackageJsonPath);
3870
+ packageJson.name = options.projectName;
3871
+ if (!packageJson.scripts) packageJson.scripts = {};
3872
+ const scripts = packageJson.scripts;
3873
+ const backendPackageName = options.backend === "convex" ? `@${options.projectName}/backend` : "server";
3874
+ let serverDevScript = "";
3875
+ if (options.addons.includes("turborepo")) serverDevScript = `turbo -F ${backendPackageName} dev`;
3876
+ else if (options.packageManager === "bun") serverDevScript = `bun run --filter ${backendPackageName} dev`;
3877
+ else if (options.packageManager === "pnpm") serverDevScript = `pnpm --filter ${backendPackageName} dev`;
3878
+ else if (options.packageManager === "npm") serverDevScript = `npm run dev --workspace ${backendPackageName}`;
3879
+ let devScript = "";
3880
+ if (options.packageManager === "pnpm") devScript = "pnpm -r dev";
3881
+ else if (options.packageManager === "npm") devScript = "npm run dev --workspaces";
3882
+ else if (options.packageManager === "bun") devScript = "bun run --filter '*' dev";
3883
+ const needsDbScripts = options.backend !== "convex" && options.database !== "none" && options.orm !== "none" && options.orm !== "mongoose";
3884
+ if (options.addons.includes("turborepo")) {
3885
+ scripts.dev = "turbo dev";
3886
+ scripts.build = "turbo build";
3887
+ scripts["check-types"] = "turbo check-types";
3888
+ scripts["dev:native"] = "turbo -F native dev";
3889
+ scripts["dev:web"] = "turbo -F web dev";
3890
+ scripts["dev:server"] = serverDevScript;
3891
+ if (options.backend === "convex") scripts["dev:setup"] = `turbo -F ${backendPackageName} setup`;
3892
+ if (needsDbScripts) {
3893
+ scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
3894
+ scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`;
3895
+ if (options.orm === "prisma") {
3896
+ scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
3897
+ scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
3898
+ } else if (options.orm === "drizzle") {
3899
+ scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
3900
+ scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
3901
+ }
3902
+ }
3903
+ } else if (options.packageManager === "pnpm") {
3904
+ scripts.dev = devScript;
3905
+ scripts.build = "pnpm -r build";
3906
+ scripts["check-types"] = "pnpm -r check-types";
3907
+ scripts["dev:native"] = "pnpm --filter native dev";
3908
+ scripts["dev:web"] = "pnpm --filter web dev";
3909
+ scripts["dev:server"] = serverDevScript;
3910
+ if (options.backend === "convex") scripts["dev:setup"] = `pnpm --filter ${backendPackageName} setup`;
3911
+ if (needsDbScripts) {
3912
+ scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
3913
+ scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`;
3914
+ if (options.orm === "prisma") {
3915
+ scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
3916
+ scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
3917
+ } else if (options.orm === "drizzle") {
3918
+ scripts["db:generate"] = `pnpm --filter ${backendPackageName} db:generate`;
3919
+ scripts["db:migrate"] = `pnpm --filter ${backendPackageName} db:migrate`;
3920
+ }
3921
+ }
3922
+ } else if (options.packageManager === "npm") {
3923
+ scripts.dev = devScript;
3924
+ scripts.build = "npm run build --workspaces";
3925
+ scripts["check-types"] = "npm run check-types --workspaces";
3926
+ scripts["dev:native"] = "npm run dev --workspace native";
3927
+ scripts["dev:web"] = "npm run dev --workspace web";
3928
+ scripts["dev:server"] = serverDevScript;
3929
+ if (options.backend === "convex") scripts["dev:setup"] = `npm run setup --workspace ${backendPackageName}`;
3930
+ if (needsDbScripts) {
3931
+ scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
3932
+ scripts["db:studio"] = `npm run db:studio --workspace ${backendPackageName}`;
3933
+ if (options.orm === "prisma") {
3934
+ scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
3935
+ scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
3936
+ } else if (options.orm === "drizzle") {
3937
+ scripts["db:generate"] = `npm run db:generate --workspace ${backendPackageName}`;
3938
+ scripts["db:migrate"] = `npm run db:migrate --workspace ${backendPackageName}`;
3939
+ }
3940
+ }
3941
+ } else if (options.packageManager === "bun") {
3942
+ scripts.dev = devScript;
3943
+ scripts.build = "bun run --filter '*' build";
3944
+ scripts["check-types"] = "bun run --filter '*' check-types";
3945
+ scripts["dev:native"] = "bun run --filter native dev";
3946
+ scripts["dev:web"] = "bun run --filter web dev";
3947
+ scripts["dev:server"] = serverDevScript;
3948
+ if (options.backend === "convex") scripts["dev:setup"] = `bun run --filter ${backendPackageName} setup`;
3949
+ if (needsDbScripts) {
3950
+ scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
3951
+ scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`;
3952
+ if (options.orm === "prisma") {
3953
+ scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
3954
+ scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
3955
+ } else if (options.orm === "drizzle") {
3956
+ scripts["db:generate"] = `bun run --filter ${backendPackageName} db:generate`;
3957
+ scripts["db:migrate"] = `bun run --filter ${backendPackageName} db:migrate`;
3958
+ }
3959
+ }
3960
+ }
3961
+ if (options.addons.includes("biome")) scripts.check = "biome check --write .";
3962
+ if (options.addons.includes("husky")) {
3963
+ scripts.prepare = "husky";
3964
+ packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
3965
+ }
3966
+ try {
3967
+ const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir });
3968
+ packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
3969
+ } catch (_e) {
3970
+ log.warn(`Could not determine ${options.packageManager} version.`);
3971
+ }
3972
+ if (!packageJson.workspaces) packageJson.workspaces = [];
3973
+ const workspaces = packageJson.workspaces;
3974
+ if (options.backend === "convex") {
3975
+ if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
3976
+ const needsAppsDir = options.frontend.length > 0 || options.addons.includes("starlight");
3977
+ if (needsAppsDir && !workspaces.includes("apps/*")) workspaces.push("apps/*");
3978
+ } else {
3979
+ if (!workspaces.includes("apps/*")) workspaces.push("apps/*");
3980
+ if (!workspaces.includes("packages/*")) workspaces.push("packages/*");
3981
+ }
3982
+ await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
3983
+ }
3984
+ async function updateServerPackageJson(projectDir, options) {
3985
+ const serverPackageJsonPath = path.join(projectDir, "apps/server/package.json");
3986
+ if (!await fs.pathExists(serverPackageJsonPath)) return;
3987
+ const serverPackageJson = await fs.readJson(serverPackageJsonPath);
3988
+ if (!serverPackageJson.scripts) serverPackageJson.scripts = {};
3989
+ const scripts = serverPackageJson.scripts;
3990
+ if (options.database !== "none") {
3991
+ if (options.database === "sqlite" && options.orm === "drizzle") scripts["db:local"] = "turso dev --db-file local.db";
3992
+ if (options.orm === "prisma") {
3993
+ scripts["db:push"] = "prisma db push --schema ./prisma/schema";
3994
+ scripts["db:studio"] = "prisma studio";
3995
+ scripts["db:generate"] = "prisma generate --schema ./prisma/schema";
3996
+ scripts["db:migrate"] = "prisma migrate dev";
3997
+ } else if (options.orm === "drizzle") {
3998
+ scripts["db:push"] = "drizzle-kit push";
3999
+ scripts["db:studio"] = "drizzle-kit studio";
4000
+ scripts["db:generate"] = "drizzle-kit generate";
4001
+ scripts["db:migrate"] = "drizzle-kit migrate";
4002
+ }
4003
+ }
4004
+ await fs.writeJson(serverPackageJsonPath, serverPackageJson, { spaces: 2 });
4005
+ }
4006
+ async function updateConvexPackageJson(projectDir, options) {
4007
+ const convexPackageJsonPath = path.join(projectDir, "packages/backend/package.json");
4008
+ if (!await fs.pathExists(convexPackageJsonPath)) return;
4009
+ const convexPackageJson = await fs.readJson(convexPackageJsonPath);
4010
+ convexPackageJson.name = `@${options.projectName}/backend`;
4011
+ if (!convexPackageJson.scripts) convexPackageJson.scripts = {};
4012
+ await fs.writeJson(convexPackageJsonPath, convexPackageJson, { spaces: 2 });
4013
+ }
4014
+ async function initializeGit(projectDir, useGit) {
4015
+ if (!useGit) return;
4016
+ const gitVersionResult = await $({
4017
+ cwd: projectDir,
4018
+ reject: false,
4019
+ stderr: "pipe"
4020
+ })`git --version`;
4021
+ if (gitVersionResult.exitCode !== 0) {
4022
+ log.warn(pc.yellow("Git is not installed"));
4023
+ return;
4024
+ }
4025
+ const result = await $({
4026
+ cwd: projectDir,
4027
+ reject: false,
4028
+ stderr: "pipe"
4029
+ })`git init`;
4030
+ if (result.exitCode !== 0) throw new Error(`Git initialization failed: ${result.stderr}`);
4031
+ }
4032
+
4033
+ //#endregion
4034
+ //#region src/helpers/project-generation/create-project.ts
4035
+ async function createProject(options) {
4036
+ const projectDir = options.projectDir;
4037
+ const isConvex = options.backend === "convex";
4038
+ try {
4039
+ await fs.ensureDir(projectDir);
4040
+ await copyBaseTemplate(projectDir, options);
4041
+ await setupFrontendTemplates(projectDir, options);
4042
+ await setupBackendFramework(projectDir, options);
4043
+ if (!isConvex) {
4044
+ await setupDbOrmTemplates(projectDir, options);
4045
+ await setupAuthTemplate(projectDir, options);
4046
+ }
4047
+ if (options.examples.length > 0 && options.examples[0] !== "none") await setupExamplesTemplate(projectDir, options);
4048
+ await setupAddonsTemplate(projectDir, options);
4049
+ await setupApi(options);
4050
+ if (!isConvex) {
4051
+ await setupBackendDependencies(options);
4052
+ await setupDatabase(options);
4053
+ await setupRuntime(options);
4054
+ if (options.examples.length > 0 && options.examples[0] !== "none") await setupExamples(options);
4055
+ }
4056
+ if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
4057
+ if (!isConvex && options.auth) await setupAuth(options);
4058
+ await handleExtras(projectDir, options);
4059
+ await setupEnvironmentVariables(options);
4060
+ await updatePackageConfigurations(projectDir, options);
4061
+ await createReadme(projectDir, options);
4062
+ await writeTviConfig(options);
4063
+ await initializeGit(projectDir, options.git);
4064
+ log.success("Project template successfully scaffolded!");
4065
+ if (options.install) {
4066
+ await installDependencies({
4067
+ projectDir,
4068
+ packageManager: options.packageManager
4069
+ });
4070
+ await generateCloudflareWorkerTypes(options);
4071
+ }
4072
+ displayPostInstallInstructions({
4073
+ ...options,
4074
+ depsInstalled: options.install
4075
+ });
4076
+ return projectDir;
4077
+ } catch (error) {
4078
+ if (error instanceof Error) {
4079
+ cancel(pc.red(`Error during project creation: ${error.message}`));
4080
+ console.error(error.stack);
4081
+ process.exit(1);
4082
+ } else {
4083
+ cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
4084
+ console.error(error);
4085
+ process.exit(1);
4086
+ }
4087
+ }
4088
+ }
4089
+
4090
+ //#endregion
4091
+ //#region src/helpers/project-generation/command-handlers.ts
4092
+ async function createProjectHandler(input) {
4093
+ const startTime = Date.now();
4094
+ try {
4095
+ renderTitle();
4096
+ intro(pc.magenta("Creating a new tvi project"));
4097
+ let currentPathInput;
4098
+ if (input.yes && input.projectName) currentPathInput = input.projectName;
4099
+ else if (input.yes) {
4100
+ let defaultName = DEFAULT_CONFIG.relativePath;
4101
+ let counter = 1;
4102
+ while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0) {
4103
+ defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
4104
+ counter++;
4105
+ }
4106
+ currentPathInput = defaultName;
4107
+ } else currentPathInput = await getProjectName(input.projectName);
4108
+ const { finalPathInput, shouldClearDirectory } = await handleDirectoryConflict(currentPathInput);
4109
+ const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(finalPathInput, shouldClearDirectory);
4110
+ const cliInput = {
4111
+ ...input,
4112
+ projectDirectory: input.projectName
4113
+ };
4114
+ const providedFlags = getProvidedFlags(cliInput);
4115
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
4116
+ const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
4117
+ if (!input.yes && Object.keys(otherFlags).length > 0) {
4118
+ log.info(pc.yellow("Using these pre-selected options:"));
4119
+ log.message(displayConfig(otherFlags));
4120
+ log.message("");
4121
+ }
4122
+ let config;
4123
+ if (input.yes) {
4124
+ config = {
4125
+ ...DEFAULT_CONFIG,
4126
+ ...flagConfig,
4127
+ projectName: finalBaseName,
4128
+ projectDir: finalResolvedPath,
4129
+ relativePath: finalPathInput
4130
+ };
4131
+ if (config.backend === "convex") log.info("Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo");
4132
+ else if (config.backend === "none") log.info("Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none");
4133
+ log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
4134
+ log.message(displayConfig(config));
4135
+ log.message("");
4136
+ } else config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
4137
+ await createProject(config);
4138
+ const reproducibleCommand = generateReproducibleCommand(config);
4139
+ log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
4140
+ await trackProjectCreation(config);
4141
+ const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
4142
+ outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
4143
+ } catch (error) {
4144
+ console.error(error);
4145
+ process.exit(1);
4146
+ }
4147
+ }
4148
+ async function addAddonsHandler(input) {
4149
+ try {
4150
+ if (!input.addons || input.addons.length === 0) {
4151
+ const projectDir = input.projectDir || process.cwd();
4152
+ const detectedConfig = await detectProjectConfig(projectDir);
4153
+ if (!detectedConfig) {
4154
+ cancel(pc.red("Could not detect project configuration. Please ensure this is a valid tvi project."));
4155
+ process.exit(1);
4156
+ }
4157
+ const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
4158
+ if (addonsPrompt.length === 0) {
4159
+ outro(pc.yellow("No addons to add or all compatible addons are already present."));
4160
+ return;
4161
+ }
4162
+ input.addons = addonsPrompt;
4163
+ }
4164
+ if (!input.addons || input.addons.length === 0) {
4165
+ outro(pc.yellow("No addons specified to add."));
4166
+ return;
4167
+ }
4168
+ await addAddonsToProject({
4169
+ ...input,
4170
+ addons: input.addons
4171
+ });
4172
+ } catch (error) {
4173
+ console.error(error);
4174
+ process.exit(1);
4175
+ }
4176
+ }
4177
+
4178
+ //#endregion
4179
+ //#region src/utils/open-url.ts
4180
+ async function openUrl(url) {
4181
+ const platform = process.platform;
4182
+ let command;
4183
+ let args = [];
4184
+ if (platform === "darwin") {
4185
+ command = "open";
4186
+ args = [url];
4187
+ } else if (platform === "win32") {
4188
+ command = "cmd";
4189
+ args = [
4190
+ "/c",
4191
+ "start",
4192
+ "",
4193
+ url.replace(/&/g, "^&")
4194
+ ];
4195
+ } else {
4196
+ command = "xdg-open";
4197
+ args = [url];
4198
+ }
4199
+ try {
4200
+ await execa(command, args, { stdio: "ignore" });
4201
+ } catch {
4202
+ log.message(`Please open ${url} in your browser.`);
4203
+ }
4204
+ }
4205
+
4206
+ //#endregion
4207
+ //#region src/utils/sponsors.ts
4208
+ const SPONSORS_JSON_URL = "https://sponsors.shaneholloman.dev/sponsors.json";
4209
+ async function fetchSponsors(url = SPONSORS_JSON_URL) {
4210
+ const s = spinner();
4211
+ s.start("Fetching sponsors…");
4212
+ const response = await fetch(url);
4213
+ if (!response.ok) {
4214
+ s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
4215
+ throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
4216
+ }
4217
+ const sponsors = await response.json();
4218
+ s.stop("Sponsors fetched successfully!");
4219
+ return sponsors;
4220
+ }
4221
+ function displaySponsors(sponsors) {
4222
+ if (sponsors.length === 0) {
4223
+ log.info("No sponsors found. You can be the first one! SPARKLE");
4224
+ outro(pc.cyan("Visit https://github.com/sponsors/shaneholloman to become a sponsor."));
4225
+ return;
4226
+ }
4227
+ sponsors.forEach((entry, idx) => {
4228
+ const sponsor = entry.sponsor;
4229
+ const displayName = sponsor.name ?? sponsor.login;
4230
+ const tier = entry.tierName ? ` (${entry.tierName})` : "";
4231
+ log.step(`${idx + 1}. ${pc.green(displayName)}${pc.yellow(tier)}`);
4232
+ log.message(` ${pc.dim("GitHub:")} https://github.com/${sponsor.login}`);
4233
+ const website = sponsor.websiteUrl ?? sponsor.linkUrl;
4234
+ if (website) log.message(` ${pc.dim("Website:")} ${website}`);
4235
+ });
4236
+ log.message("");
4237
+ outro(pc.magenta("Visit https://github.com/sponsors/shaneholloman to become a sponsor."));
4238
+ }
4239
+
4240
+ //#endregion
4241
+ //#region src/index.ts
4242
+ const t = trpcServer.initTRPC.create();
4243
+ const router = t.router({
4244
+ init: t.procedure.meta({
4245
+ description: "Create a new tvi project",
4246
+ default: true
4247
+ }).input(zod.tuple([ProjectNameSchema.optional(), zod.object({
4248
+ yes: zod.boolean().optional().default(false).describe("Use default configuration"),
4249
+ database: DatabaseSchema.optional(),
4250
+ orm: ORMSchema.optional(),
4251
+ auth: zod.boolean().optional(),
4252
+ frontend: zod.array(FrontendSchema).optional(),
4253
+ addons: zod.array(AddonsSchema).optional(),
4254
+ examples: zod.array(ExamplesSchema).optional(),
4255
+ git: zod.boolean().optional(),
4256
+ packageManager: PackageManagerSchema.optional(),
4257
+ install: zod.boolean().optional(),
4258
+ dbSetup: DatabaseSetupSchema.optional(),
4259
+ backend: BackendSchema.optional(),
4260
+ runtime: RuntimeSchema.optional(),
4261
+ api: APISchema.optional()
4262
+ }).optional().default({})])).mutation(async ({ input }) => {
4263
+ const [projectName, options] = input;
4264
+ const combinedInput = {
4265
+ projectName,
4266
+ ...options
4267
+ };
4268
+ await createProjectHandler(combinedInput);
4269
+ }),
4270
+ add: t.procedure.meta({ description: "Add addons to an existing tvi project" }).input(zod.tuple([zod.object({
4271
+ addons: zod.array(AddonsSchema).optional().default([]),
4272
+ projectDir: zod.string().optional(),
4273
+ install: zod.boolean().optional().default(false).describe("Install dependencies after adding addons"),
4274
+ packageManager: PackageManagerSchema.optional()
4275
+ }).optional().default({})])).mutation(async ({ input }) => {
4276
+ const [options] = input;
4277
+ await addAddonsHandler(options);
4278
+ }),
4279
+ sponsors: t.procedure.meta({ description: "Show tvi sponsors" }).mutation(async () => {
4280
+ try {
4281
+ renderTitle();
4282
+ intro(pc.magenta("tvi Sponsors"));
4283
+ const sponsors = await fetchSponsors();
4284
+ displaySponsors(sponsors);
4285
+ } catch (error) {
4286
+ consola$1.error(error);
4287
+ process.exit(1);
4288
+ }
4289
+ }),
4290
+ docs: t.procedure.meta({ description: "Open tvi documentation" }).mutation(async () => {
4291
+ const DOCS_URL = "https://github.com/shaneholloman/tvi";
4292
+ try {
4293
+ await openUrl(DOCS_URL);
4294
+ log.success(pc.blue("Opened docs in your default browser."));
4295
+ } catch {
4296
+ log.message(`Please visit ${DOCS_URL}`);
4297
+ }
4298
+ }),
4299
+ builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(async () => {
4300
+ const BUILDER_URL = "https://github.com/shaneholloman/tvi-web";
4301
+ try {
4302
+ await openUrl(BUILDER_URL);
4303
+ log.success(pc.blue("Opened builder in your default browser."));
4304
+ } catch {
4305
+ log.message(`Please visit ${BUILDER_URL}`);
4306
+ }
4307
+ })
4308
+ });
4309
+ createCli({
4310
+ router,
4311
+ name: "tvi",
4312
+ version: getLatestCLIVersion()
4313
+ }).run();
4314
+
4315
+ //#endregion