UNPKG

create-better-t-stack

Version:

A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations

1,456 lines (1,427 loc) 155 kB
#!/usr/bin/env node import path from "node:path"; import { cancel, confirm, group, intro, isCancel, log, multiselect, outro, password, select, spinner, text } from "@clack/prompts"; import consola, { consola as consola$1 } from "consola"; import fs from "fs-extra"; import pc from "picocolors"; import { createCli, trpcServer, zod } from "trpc-cli"; import { fileURLToPath } from "node:url"; import { $, execa } from "execa"; import os from "node:os"; import { globby } from "globby"; import handlebars from "handlebars"; import { z } from "zod"; import { PostHog } from "posthog-node"; import gradient from "gradient-string"; //#region src/utils/get-package-manager.ts const getUserPkgManager = () => { const userAgent = process.env.npm_config_user_agent; if (userAgent?.startsWith("pnpm")) return "pnpm"; if (userAgent?.startsWith("bun")) return "bun"; return "npm"; }; //#endregion //#region src/constants.ts const __filename = fileURLToPath(import.meta.url); const distPath = path.dirname(__filename); const PKG_ROOT = path.join(distPath, "../"); const DEFAULT_CONFIG = { projectName: "my-better-t-app", projectDir: path.resolve(process.cwd(), "my-better-t-app"), relativePath: "my-better-t-app", frontend: ["tanstack-router"], database: "sqlite", orm: "drizzle", auth: true, addons: ["turborepo"], examples: [], git: true, packageManager: getUserPkgManager(), install: true, dbSetup: "none", backend: "hono", runtime: "bun", api: "trpc" }; const dependencyVersionMap = { "better-auth": "^1.2.9", "@better-auth/expo": "^1.2.9", "drizzle-orm": "^0.38.4", "drizzle-kit": "^0.30.5", "@libsql/client": "^0.14.0", pg: "^8.14.1", "@types/pg": "^8.11.11", mysql2: "^3.14.0", "@prisma/client": "^6.9.0", prisma: "^6.9.0", mongoose: "^8.14.0", "vite-plugin-pwa": "^0.21.2", "@vite-pwa/assets-generator": "^0.2.6", "@tauri-apps/cli": "^2.4.0", "@biomejs/biome": "^2.0.0", husky: "^9.1.7", "lint-staged": "^15.5.0", "@hono/node-server": "^1.14.0", tsx: "^4.19.2", "@types/node": "^22.13.11", "@types/bun": "^1.2.6", "@elysiajs/node": "^1.2.6", "@elysiajs/cors": "^1.2.0", "@elysiajs/trpc": "^1.1.0", elysia: "^1.2.25", "@hono/trpc-server": "^0.3.4", hono: "^4.7.6", cors: "^2.8.5", express: "^5.1.0", "@types/express": "^5.0.1", "@types/cors": "^2.8.17", fastify: "^5.3.3", "@fastify/cors": "^11.0.1", turbo: "^2.5.4", ai: "^4.3.16", "@ai-sdk/google": "^1.2.3", "@ai-sdk/vue": "^1.2.8", "@ai-sdk/svelte": "^2.1.9", "@ai-sdk/react": "^1.2.12", "@prisma/extension-accelerate": "^1.3.0", "@orpc/server": "^1.5.0", "@orpc/client": "^1.5.0", "@orpc/tanstack-query": "^1.5.0", "@trpc/tanstack-react-query": "^11.0.0", "@trpc/server": "^11.0.0", "@trpc/client": "^11.0.0", convex: "^1.23.0", "@convex-dev/react-query": "^0.0.0-alpha.8", "convex-svelte": "^0.0.11", "@tanstack/svelte-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.80.5", "@tanstack/react-query": "^5.80.5", "@tanstack/solid-query": "^5.75.0", "@tanstack/solid-query-devtools": "^5.75.0", wrangler: "^4.20.0" }; //#endregion //#region src/utils/add-package-deps.ts const addPackageDependency = async (opts) => { const { dependencies = [], devDependencies = [], projectDir } = opts; const pkgJsonPath = path.join(projectDir, "package.json"); const pkgJson = await fs.readJson(pkgJsonPath); if (!pkgJson.dependencies) pkgJson.dependencies = {}; if (!pkgJson.devDependencies) pkgJson.devDependencies = {}; for (const pkgName of dependencies) { const version = dependencyVersionMap[pkgName]; if (version) pkgJson.dependencies[pkgName] = version; else console.warn(`Warning: Dependency ${pkgName} not found in version map.`); } for (const pkgName of devDependencies) { const version = dependencyVersionMap[pkgName]; if (version) pkgJson.devDependencies[pkgName] = version; else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`); } await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); }; //#endregion //#region src/utils/get-package-execution-command.ts /** * Returns the appropriate command for running a package without installing it globally, * based on the selected package manager. * * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun'). * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma"). * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma"). */ function getPackageExecutionCommand(packageManager, commandWithArgs) { switch (packageManager) { case "pnpm": return `pnpm dlx ${commandWithArgs}`; case "bun": return `bunx ${commandWithArgs}`; default: return `npx ${commandWithArgs}`; } } //#endregion //#region src/helpers/setup/starlight-setup.ts async function setupStarlight(config) { const { packageManager, projectDir } = config; const s = spinner(); try { s.start("Setting up Starlight docs..."); const starlightArgs = [ "docs", "--template", "starlight", "--no-install", "--add", "tailwind", "--no-git", "--skip-houston" ]; const starlightArgsString = starlightArgs.join(" "); const commandWithArgs = `create-astro@latest ${starlightArgsString}`; const starlightInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs); await execa(starlightInitCommand, { cwd: path.join(projectDir, "apps"), env: { CI: "true" }, shell: true }); s.stop("Starlight docs setup successfully!"); } catch (error) { s.stop(pc.red("Failed to set up Starlight docs")); if (error instanceof Error) consola.error(pc.red(error.message)); } } //#endregion //#region src/helpers/setup/tauri-setup.ts async function setupTauri(config) { const { packageManager, frontend, projectDir } = config; const s = spinner(); const clientPackageDir = path.join(projectDir, "apps/web"); if (!await fs.pathExists(clientPackageDir)) return; try { s.start("Setting up Tauri desktop app support..."); await addPackageDependency({ devDependencies: ["@tauri-apps/cli"], projectDir: clientPackageDir }); const clientPackageJsonPath = path.join(clientPackageDir, "package.json"); if (await fs.pathExists(clientPackageJsonPath)) { const packageJson = await fs.readJson(clientPackageJsonPath); packageJson.scripts = { ...packageJson.scripts, tauri: "tauri", "desktop:dev": "tauri dev", "desktop:build": "tauri build" }; await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 }); } const _hasTanstackRouter = frontend.includes("tanstack-router"); const hasReactRouter = frontend.includes("react-router"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); const _hasSolid = frontend.includes("solid"); const hasNext = frontend.includes("next"); const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001"; const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist"; const tauriArgs = [ "init", `--app-name=${path.basename(projectDir)}`, `--window-title=${path.basename(projectDir)}`, `--frontend-dist=${frontendDist}`, `--dev-url=${devUrl}`, `--before-dev-command=\"${packageManager} run dev\"`, `--before-build-command=\"${packageManager} run build\"` ]; const tauriArgsString = tauriArgs.join(" "); const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`; const tauriInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs); await execa(tauriInitCommand, { cwd: clientPackageDir, env: { CI: "true" }, shell: true }); s.stop("Tauri desktop app support configured successfully!"); } catch (error) { s.stop(pc.red("Failed to set up Tauri")); if (error instanceof Error) consola$1.error(pc.red(error.message)); } } //#endregion //#region src/helpers/setup/addons-setup.ts async function setupAddons(config) { const { addons, frontend, projectDir } = config; const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next"); const hasNuxtFrontend = frontend.includes("nuxt"); const hasSvelteFrontend = frontend.includes("svelte"); const hasSolidFrontend = frontend.includes("solid"); const hasNextFrontend = frontend.includes("next"); if (addons.includes("turborepo")) await addPackageDependency({ devDependencies: ["turbo"], projectDir }); if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend); if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config); if (addons.includes("biome")) await setupBiome(projectDir); if (addons.includes("husky")) await setupHusky(projectDir); if (addons.includes("starlight")) await setupStarlight(config); } function getWebAppDir(projectDir, frontends) { if (frontends.some((f) => [ "react-router", "tanstack-router", "nuxt", "svelte", "solid" ].includes(f))) return path.join(projectDir, "apps/web"); return path.join(projectDir, "apps/web"); } async function setupBiome(projectDir) { await addPackageDependency({ devDependencies: ["@biomejs/biome"], projectDir }); const packageJsonPath = path.join(projectDir, "package.json"); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); packageJson.scripts = { ...packageJson.scripts, check: "biome check --write ." }; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } } async function setupHusky(projectDir) { await addPackageDependency({ devDependencies: ["husky", "lint-staged"], projectDir }); const packageJsonPath = path.join(projectDir, "package.json"); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); packageJson.scripts = { ...packageJson.scripts, prepare: "husky" }; packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] }; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } } async function setupPwa(projectDir, frontends) { const isCompatibleFrontend = frontends.some((f) => [ "react-router", "tanstack-router", "solid" ].includes(f)); if (!isCompatibleFrontend) return; const clientPackageDir = getWebAppDir(projectDir, frontends); if (!await fs.pathExists(clientPackageDir)) return; await addPackageDependency({ dependencies: ["vite-plugin-pwa"], devDependencies: ["@vite-pwa/assets-generator"], projectDir: clientPackageDir }); const clientPackageJsonPath = path.join(clientPackageDir, "package.json"); if (await fs.pathExists(clientPackageJsonPath)) { const packageJson = await fs.readJson(clientPackageJsonPath); packageJson.scripts = { ...packageJson.scripts, "generate-pwa-assets": "pwa-assets-generator" }; await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 }); } } //#endregion //#region src/helpers/setup/api-setup.ts async function setupApi(config) { const { api, projectName, frontend, backend, packageManager, projectDir } = config; const isConvex = backend === "convex"; const webDir = path.join(projectDir, "apps/web"); const nativeDir = path.join(projectDir, "apps/native"); const webDirExists = await fs.pathExists(webDir); const nativeDirExists = await fs.pathExists(nativeDir); const hasReactWeb = frontend.some((f) => [ "tanstack-router", "react-router", "tanstack-start", "next" ].includes(f)); const hasNuxtWeb = frontend.includes("nuxt"); const hasSvelteWeb = frontend.includes("svelte"); const hasSolidWeb = frontend.includes("solid"); if (!isConvex && api !== "none") { const serverDir = path.join(projectDir, "apps/server"); const serverDirExists = await fs.pathExists(serverDir); if (serverDirExists) { if (api === "orpc") await addPackageDependency({ dependencies: ["@orpc/server", "@orpc/client"], projectDir: serverDir }); else if (api === "trpc") { await addPackageDependency({ dependencies: ["@trpc/server", "@trpc/client"], projectDir: serverDir }); if (config.backend === "hono") await addPackageDependency({ dependencies: ["@hono/trpc-server"], projectDir: serverDir }); else if (config.backend === "elysia") await addPackageDependency({ dependencies: ["@elysiajs/trpc"], projectDir: serverDir }); } } if (webDirExists) { if (hasReactWeb) { if (api === "orpc") await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", "@orpc/client", "@orpc/server" ], projectDir: webDir }); else if (api === "trpc") await addPackageDependency({ dependencies: [ "@trpc/tanstack-react-query", "@trpc/client", "@trpc/server" ], projectDir: webDir }); } else if (hasNuxtWeb) { if (api === "orpc") await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", "@orpc/client", "@orpc/server" ], projectDir: webDir }); } else if (hasSvelteWeb) { if (api === "orpc") await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", "@orpc/client", "@orpc/server", "@tanstack/svelte-query" ], projectDir: webDir }); } else if (hasSolidWeb) { if (api === "orpc") await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", "@orpc/client", "@orpc/server", "@tanstack/solid-query" ], projectDir: webDir }); } } if (nativeDirExists) { if (api === "trpc") await addPackageDependency({ dependencies: [ "@trpc/tanstack-react-query", "@trpc/client", "@trpc/server" ], projectDir: nativeDir }); else if (api === "orpc") await addPackageDependency({ dependencies: [ "@orpc/tanstack-query", "@orpc/client", "@orpc/server" ], projectDir: nativeDir }); } } const reactBasedFrontends = [ "react-router", "tanstack-router", "tanstack-start", "next", "native-nativewind", "native-unistyles" ]; const needsSolidQuery = frontend.includes("solid"); const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f)); if (needsReactQuery && !isConvex) { const reactQueryDeps = ["@tanstack/react-query"]; const reactQueryDevDeps = ["@tanstack/react-query-devtools"]; const hasReactWeb$1 = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f)); const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles"); if (hasReactWeb$1 && webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); if (await fs.pathExists(webPkgJsonPath)) try { await addPackageDependency({ dependencies: reactQueryDeps, devDependencies: reactQueryDevDeps, projectDir: webDir }); } catch (_error) {} } if (hasNative && nativeDirExists) { const nativePkgJsonPath = path.join(nativeDir, "package.json"); if (await fs.pathExists(nativePkgJsonPath)) try { await addPackageDependency({ dependencies: reactQueryDeps, projectDir: nativeDir }); } catch (_error) {} } } if (needsSolidQuery && !isConvex) { const solidQueryDeps = ["@tanstack/solid-query"]; const solidQueryDevDeps = ["@tanstack/solid-query-devtools"]; if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); if (await fs.pathExists(webPkgJsonPath)) try { await addPackageDependency({ dependencies: solidQueryDeps, devDependencies: solidQueryDevDeps, projectDir: webDir }); } catch (_error) {} } } if (isConvex) { if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); if (await fs.pathExists(webPkgJsonPath)) try { const webDepsToAdd = ["convex"]; if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query"); if (hasSvelteWeb) webDepsToAdd.push("convex-svelte"); await addPackageDependency({ dependencies: webDepsToAdd, projectDir: webDir }); } catch (_error) {} } if (nativeDirExists) { const nativePkgJsonPath = path.join(nativeDir, "package.json"); if (await fs.pathExists(nativePkgJsonPath)) try { await addPackageDependency({ dependencies: ["convex"], projectDir: nativeDir }); } catch (_error) {} } const backendPackageName = `@${projectName}/backend`; const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*"; const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => { try { const pkgJson = await fs.readJson(pkgJsonPath); if (!pkgJson.dependencies) pkgJson.dependencies = {}; if (pkgJson.dependencies[depName] !== depVersion) { pkgJson.dependencies[depName] = depVersion; await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); } } catch (_error) {} }; if (webDirExists) { const webPkgJsonPath = path.join(webDir, "package.json"); if (await fs.pathExists(webPkgJsonPath)) await addWorkspaceDepManually(webPkgJsonPath, backendPackageName, backendWorkspaceVersion); } if (nativeDirExists) { const nativePkgJsonPath = path.join(nativeDir, "package.json"); if (await fs.pathExists(nativePkgJsonPath)) await addWorkspaceDepManually(nativePkgJsonPath, backendPackageName, backendWorkspaceVersion); } } } //#endregion //#region src/helpers/setup/auth-setup.ts async function setupAuth(config) { const { auth, frontend, backend, projectDir } = config; if (backend === "convex" || !auth) return; const serverDir = path.join(projectDir, "apps/server"); const clientDir = path.join(projectDir, "apps/web"); const nativeDir = path.join(projectDir, "apps/native"); const clientDirExists = await fs.pathExists(clientDir); const nativeDirExists = await fs.pathExists(nativeDir); const serverDirExists = await fs.pathExists(serverDir); try { if (serverDirExists) await addPackageDependency({ dependencies: ["better-auth"], projectDir: serverDir }); const hasWebFrontend = frontend.some((f) => [ "react-router", "tanstack-router", "tanstack-start", "next", "nuxt", "svelte", "solid" ].includes(f)); if (hasWebFrontend && clientDirExists) await addPackageDependency({ dependencies: ["better-auth"], projectDir: clientDir }); if ((frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) && nativeDirExists) { await addPackageDependency({ dependencies: ["better-auth", "@better-auth/expo"], projectDir: nativeDir }); if (serverDirExists) await addPackageDependency({ dependencies: ["@better-auth/expo"], projectDir: serverDir }); } } catch (error) { consola.error(pc.red("Failed to configure authentication dependencies")); if (error instanceof Error) consola.error(pc.red(error.message)); } } function generateAuthSecret(length = 32) { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; const charactersLength = characters.length; for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength)); return result; } //#endregion //#region src/helpers/setup/backend-setup.ts async function setupBackendDependencies(config) { const { backend, runtime, api, projectDir } = config; if (backend === "convex") return; const framework = backend; const serverDir = path.join(projectDir, "apps/server"); const dependencies = []; const devDependencies = []; if (framework === "hono") { dependencies.push("hono"); if (api === "trpc") dependencies.push("@hono/trpc-server"); if (runtime === "node") { dependencies.push("@hono/node-server"); devDependencies.push("tsx", "@types/node"); } } else if (framework === "elysia") { dependencies.push("elysia", "@elysiajs/cors"); if (api === "trpc") dependencies.push("@elysiajs/trpc"); if (runtime === "node") { dependencies.push("@elysiajs/node"); devDependencies.push("tsx", "@types/node"); } } else if (framework === "express") { dependencies.push("express", "cors"); devDependencies.push("@types/express", "@types/cors"); if (runtime === "node") devDependencies.push("tsx", "@types/node"); } else if (framework === "fastify") { dependencies.push("fastify", "@fastify/cors"); if (runtime === "node") devDependencies.push("tsx", "@types/node"); } if (runtime === "bun") devDependencies.push("@types/bun"); if (dependencies.length > 0 || devDependencies.length > 0) await addPackageDependency({ dependencies, devDependencies, projectDir: serverDir }); } //#endregion //#region src/utils/command-exists.ts async function commandExists(command) { try { const isWindows = process.platform === "win32"; if (isWindows) { const result$1 = await execa("where", [command]); return result$1.exitCode === 0; } const result = await execa("which", [command]); return result.exitCode === 0; } catch { return false; } } //#endregion //#region src/helpers/project-generation/env-setup.ts async function addEnvVariablesToFile(filePath, variables) { await fs.ensureDir(path.dirname(filePath)); let envContent = ""; if (await fs.pathExists(filePath)) envContent = await fs.readFile(filePath, "utf8"); let modified = false; let contentToAdd = ""; const exampleVariables = []; for (const { key, value, condition } of variables) if (condition) { const regex = new RegExp(`^${key}=.*$`, "m"); const valueToWrite = value ?? ""; exampleVariables.push(`${key}=`); if (regex.test(envContent)) { const existingMatch = envContent.match(regex); if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) { envContent = envContent.replace(regex, `${key}=${valueToWrite}`); modified = true; } } else { contentToAdd += `${key}=${valueToWrite}\n`; modified = true; } } if (contentToAdd) { if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n"; envContent += contentToAdd; } if (modified) await fs.writeFile(filePath, envContent.trimEnd()); const exampleFilePath = filePath.replace(/\.env$/, ".env.example"); let exampleEnvContent = ""; if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8"); let exampleModified = false; let exampleContentToAdd = ""; for (const exampleVar of exampleVariables) { const key = exampleVar.split("=")[0]; const regex = new RegExp(`^${key}=.*$`, "m"); if (!regex.test(exampleEnvContent)) { exampleContentToAdd += `${exampleVar}\n`; exampleModified = true; } } if (exampleContentToAdd) { if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) exampleEnvContent += "\n"; exampleEnvContent += exampleContentToAdd; } if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd()); } async function setupEnvironmentVariables(config) { const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config; const hasReactRouter = frontend.includes("react-router"); const hasTanStackRouter = frontend.includes("tanstack-router"); const hasTanStackStart = frontend.includes("tanstack-start"); const hasNextJs = frontend.includes("next"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); const hasSolid = frontend.includes("solid"); const hasWebFrontend = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || hasSolid || hasSvelte; if (hasWebFrontend) { const clientDir = path.join(projectDir, "apps/web"); if (await fs.pathExists(clientDir)) { let envVarName = "VITE_SERVER_URL"; let serverUrl = "http://localhost:3000"; if (hasNextJs) envVarName = "NEXT_PUBLIC_SERVER_URL"; else if (hasNuxt) envVarName = "NUXT_PUBLIC_SERVER_URL"; else if (hasSvelte) envVarName = "PUBLIC_SERVER_URL"; if (backend === "convex") { if (hasNextJs) envVarName = "NEXT_PUBLIC_CONVEX_URL"; else if (hasNuxt) envVarName = "NUXT_PUBLIC_CONVEX_URL"; else if (hasSvelte) envVarName = "PUBLIC_CONVEX_URL"; else envVarName = "VITE_CONVEX_URL"; serverUrl = "https://<YOUR_CONVEX_URL>"; } const clientVars = [{ key: envVarName, value: serverUrl, condition: true }]; await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars); } } if (frontend.includes("native-nativewind") || frontend.includes("native-unistyles")) { const nativeDir = path.join(projectDir, "apps/native"); if (await fs.pathExists(nativeDir)) { let envVarName = "EXPO_PUBLIC_SERVER_URL"; let serverUrl = "http://localhost:3000"; if (backend === "convex") { envVarName = "EXPO_PUBLIC_CONVEX_URL"; serverUrl = "https://<YOUR_CONVEX_URL>"; } const nativeVars = [{ key: envVarName, value: serverUrl, condition: true }]; await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars); } } if (backend === "convex") return; const serverDir = path.join(projectDir, "apps/server"); if (!await fs.pathExists(serverDir)) return; const envPath = path.join(serverDir, ".env"); let corsOrigin = "http://localhost:3001"; if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173"; let databaseUrl = null; const specializedSetup = dbSetup === "turso" || dbSetup === "prisma-postgres" || dbSetup === "mongodb-atlas" || dbSetup === "neon" || dbSetup === "supabase"; if (database !== "none" && !specializedSetup) switch (database) { case "postgres": databaseUrl = "postgresql://postgres:password@localhost:5432/postgres"; break; case "mysql": databaseUrl = "mysql://root:password@localhost:3306/mydb"; break; case "mongodb": databaseUrl = "mongodb://localhost:27017/mydatabase"; break; case "sqlite": if (config.runtime === "workers") databaseUrl = "http://127.0.0.1:8080"; else databaseUrl = "file:./local.db"; break; } const serverVars = [ { key: "CORS_ORIGIN", value: corsOrigin, condition: true }, { key: "BETTER_AUTH_SECRET", value: generateAuthSecret(), condition: !!auth }, { key: "BETTER_AUTH_URL", value: "http://localhost:3000", condition: !!auth }, { key: "DATABASE_URL", value: databaseUrl, condition: database !== "none" && !specializedSetup }, { key: "GOOGLE_GENERATIVE_AI_API_KEY", value: "", condition: examples?.includes("ai") || false } ]; await addEnvVariablesToFile(envPath, serverVars); if (config.runtime === "workers") { const devVarsPath = path.join(serverDir, ".dev.vars"); try { await fs.copy(envPath, devVarsPath); } catch (_err) {} } } //#endregion //#region src/helpers/database-providers/mongodb-atlas-setup.ts async function checkAtlasCLI() { const s = spinner(); s.start("Checking for MongoDB Atlas CLI..."); try { const exists = await commandExists("atlas"); s.stop(exists ? "MongoDB Atlas CLI found" : pc.yellow("MongoDB Atlas CLI not found")); return exists; } catch (_error) { s.stop(pc.red("Error checking MongoDB Atlas CLI")); return false; } } async function initMongoDBAtlas(serverDir) { try { const hasAtlas = await checkAtlasCLI(); if (!hasAtlas) { consola.error(pc.red("MongoDB Atlas CLI not found.")); log.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/")); return null; } log.info(pc.blue("Running MongoDB Atlas setup...")); await execa("atlas", ["deployments", "setup"], { cwd: serverDir, stdio: "inherit" }); log.info(pc.green("MongoDB Atlas deployment ready")); const connectionString = await text({ message: "Enter your MongoDB connection string:", placeholder: "mongodb+srv://username:password@cluster.mongodb.net/database", validate(value) { if (!value) return "Please enter a connection string"; if (!value.startsWith("mongodb")) return "URL should start with mongodb:// or mongodb+srv://"; } }); if (isCancel(connectionString)) { cancel("MongoDB setup cancelled"); return null; } return { connectionString }; } catch (error) { if (error instanceof Error) consola.error(pc.red(error.message)); return null; } } async function writeEnvFile$3(projectDir, config) { try { const envPath = path.join(projectDir, "apps/server", ".env"); const variables = [{ key: "DATABASE_URL", value: config?.connectionString ?? "mongodb://localhost:27017/mydb", condition: true }]; await addEnvVariablesToFile(envPath, variables); } catch (_error) { consola.error("Failed to update environment configuration"); } } function displayManualSetupInstructions$3() { log.info(` ${pc.green("MongoDB Atlas Manual Setup Instructions:")} 1. Install Atlas CLI: ${pc.blue("https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/")} 2. Run the following command and follow the prompts: ${pc.blue("atlas deployments setup")} 3. Get your connection string from the Atlas dashboard: Format: ${pc.dim("mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME")} 4. Add the connection string to your .env file: ${pc.dim("DATABASE_URL=\"your_connection_string\"")} `); } async function setupMongoDBAtlas(config) { const { projectDir } = config; const mainSpinner = spinner(); mainSpinner.start("Setting up MongoDB Atlas..."); const serverDir = path.join(projectDir, "apps/server"); try { await fs.ensureDir(serverDir); mainSpinner.stop("MongoDB Atlas setup ready"); const config$1 = await initMongoDBAtlas(serverDir); if (config$1) { await writeEnvFile$3(projectDir, config$1); log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file.")); } else { log.warn(pc.yellow("Falling back to local MongoDB configuration")); await writeEnvFile$3(projectDir); displayManualSetupInstructions$3(); } } catch (error) { mainSpinner.stop(pc.red("MongoDB Atlas setup failed")); consola.error(pc.red(`Error during MongoDB Atlas setup: ${error instanceof Error ? error.message : String(error)}`)); try { await writeEnvFile$3(projectDir); displayManualSetupInstructions$3(); } catch {} } } //#endregion //#region src/helpers/database-providers/neon-setup.ts const NEON_REGIONS = [ { label: "AWS US East (N. Virginia)", value: "aws-us-east-1" }, { label: "AWS US East (Ohio)", value: "aws-us-east-2" }, { label: "AWS US West (Oregon)", value: "aws-us-west-2" }, { label: "AWS Europe (Frankfurt)", value: "aws-eu-central-1" }, { label: "AWS Asia Pacific (Singapore)", value: "aws-ap-southeast-1" }, { label: "AWS Asia Pacific (Sydney)", value: "aws-ap-southeast-2" }, { label: "Azure East US 2 region (Virginia)", value: "azure-eastus2" } ]; async function executeNeonCommand(packageManager, commandArgsString, spinnerText) { const s = spinner(); try { const fullCommand = getPackageExecutionCommand(packageManager, commandArgsString); if (spinnerText) s.start(spinnerText); const result = await execa(fullCommand, { shell: true }); if (spinnerText) s.stop(pc.green(spinnerText.replace("...", "").replace("ing ", "ed ").trim())); return result; } catch (error) { if (s) s.stop(pc.red(`Failed: ${spinnerText || "Command execution"}`)); throw error; } } async function createNeonProject(projectName, regionId, packageManager) { try { const commandArgsString = `neonctl projects create --name ${projectName} --region-id ${regionId} --output json`; const { stdout } = await executeNeonCommand(packageManager, commandArgsString, `Creating Neon project "${projectName}"...`); const response = JSON.parse(stdout); if (response.project && response.connection_uris && response.connection_uris.length > 0) { const projectId = response.project.id; const connectionUri = response.connection_uris[0].connection_uri; const params = response.connection_uris[0].connection_parameters; return { connectionString: connectionUri, projectId, dbName: params.database, roleName: params.role }; } consola$1.error(pc.red("Failed to extract connection information from response")); return null; } catch (_error) { consola$1.error(pc.red("Failed to create Neon project")); } } async function writeEnvFile$2(projectDir, config) { const envPath = path.join(projectDir, "apps/server", ".env"); const variables = [{ key: "DATABASE_URL", value: config?.connectionString ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public", condition: true }]; await addEnvVariablesToFile(envPath, variables); return true; } async function setupWithNeonDb(projectDir, packageManager) { try { const s = spinner(); s.start("Creating Neon database using neondb..."); const serverDir = path.join(projectDir, "apps/server"); await fs.ensureDir(serverDir); const packageCmd = getPackageExecutionCommand(packageManager, "neondb --yes"); await execa(packageCmd, { shell: true, cwd: serverDir }); s.stop(pc.green("Neon database created successfully!")); return true; } catch (error) { consola$1.error(pc.red("Failed to create database with neondb")); throw error; } } function displayManualSetupInstructions$2() { log.info(`Manual Neon PostgreSQL Setup Instructions: 1. Visit https://neon.tech and create an account 2. Create a new project from the dashboard 3. Get your connection string 4. Add the database URL to the .env file in apps/server/.env DATABASE_URL="your_connection_string"`); } async function setupNeonPostgres(config) { const { packageManager, projectDir } = config; try { const setupMethod = await select({ message: "Choose your Neon setup method:", options: [{ label: "Quick setup with neondb", value: "neondb", hint: "fastest, no auth required" }, { label: "Custom setup with neonctl", value: "neonctl", hint: "More control - choose project name and region" }], initialValue: "neondb" }); if (isCancel(setupMethod)) { cancel(pc.red("Operation cancelled")); process.exit(0); } if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager); else { const suggestedProjectName = path.basename(projectDir); const projectName = await text({ message: "Enter a name for your Neon project:", defaultValue: suggestedProjectName, initialValue: suggestedProjectName }); const regionId = await select({ message: "Select a region for your Neon project:", options: NEON_REGIONS, initialValue: NEON_REGIONS[0].value }); if (isCancel(projectName) || isCancel(regionId)) { cancel(pc.red("Operation cancelled")); process.exit(0); } const neonConfig = await createNeonProject(projectName, regionId, packageManager); if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information"); const finalSpinner = spinner(); finalSpinner.start("Configuring database connection"); await fs.ensureDir(path.join(projectDir, "apps/server")); await writeEnvFile$2(projectDir, neonConfig); finalSpinner.stop("Neon database configured!"); } } catch (error) { if (error instanceof Error) consola$1.error(pc.red(error.message)); await writeEnvFile$2(projectDir); displayManualSetupInstructions$2(); } } //#endregion //#region src/helpers/database-providers/prisma-postgres-setup.ts async function initPrismaDatabase(serverDir, packageManager) { const s = spinner(); try { s.start("Initializing Prisma PostgreSQL..."); const prismaDir = path.join(serverDir, "prisma"); await fs.ensureDir(prismaDir); s.stop("Prisma PostgreSQL initialized. Follow the prompts below:"); const prismaInitCommand = getPackageExecutionCommand(packageManager, "prisma init --db"); await execa(prismaInitCommand, { cwd: serverDir, stdio: "inherit", shell: true }); 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=...")); const databaseUrl = await password({ message: "Paste your Prisma Postgres database URL:", validate(value) { if (!value) return "Please enter a database URL"; if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://"; } }); if (isCancel(databaseUrl)) { cancel("Database setup cancelled"); return null; } return { databaseUrl }; } catch (error) { s.stop(pc.red("Prisma PostgreSQL initialization failed")); if (error instanceof Error) consola$1.error(error.message); return null; } } async function writeEnvFile$1(projectDir, config) { try { const envPath = path.join(projectDir, "apps/server", ".env"); const variables = [{ key: "DATABASE_URL", value: config?.databaseUrl ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public", condition: true }]; await addEnvVariablesToFile(envPath, variables); } catch (_error) { consola$1.error("Failed to update environment configuration"); } } function displayManualSetupInstructions$1() { log.info(`Manual Prisma PostgreSQL Setup Instructions: 1. Visit https://console.prisma.io and create an account 2. Create a new PostgreSQL database from the dashboard 3. Get your database URL 4. Add the database URL to the .env file in apps/server/.env DATABASE_URL="your_database_url"`); } async function addPrismaAccelerateExtension(serverDir) { try { await addPackageDependency({ dependencies: ["@prisma/extension-accelerate"], projectDir: serverDir }); const prismaIndexPath = path.join(serverDir, "prisma/index.ts"); const prismaIndexContent = ` import { PrismaClient } from "./generated/client"; import { withAccelerate } from "@prisma/extension-accelerate"; const prisma = new PrismaClient().$extends(withAccelerate()); export default prisma; `; await fs.writeFile(prismaIndexPath, prismaIndexContent.trim()); const dbFilePath = path.join(serverDir, "src/db/index.ts"); if (await fs.pathExists(dbFilePath)) { let dbFileContent = await fs.readFile(dbFilePath, "utf8"); if (!dbFileContent.includes("@prisma/extension-accelerate")) { dbFileContent = `import { withAccelerate } from "@prisma/extension-accelerate";\n${dbFileContent}`; dbFileContent = dbFileContent.replace("export const db = new PrismaClient();", "export const db = new PrismaClient().$extends(withAccelerate());"); await fs.writeFile(dbFilePath, dbFileContent); } } return true; } catch (_error) { log.warn(pc.yellow("Could not add Prisma Accelerate extension automatically")); return false; } } async function setupPrismaPostgres(config) { const { packageManager, projectDir } = config; const serverDir = path.join(projectDir, "apps/server"); const s = spinner(); s.start("Setting up Prisma PostgreSQL..."); try { await fs.ensureDir(serverDir); s.stop("Prisma PostgreSQL setup ready"); const config$1 = await initPrismaDatabase(serverDir, packageManager); if (config$1) { await writeEnvFile$1(projectDir, config$1); await addPrismaAccelerateExtension(serverDir); log.success(pc.green("Prisma PostgreSQL database configured successfully!")); log.info(pc.cyan("NOTE: Make sure to uncomment `import \"dotenv/config\";` in `apps/server/src/prisma.config.ts` to load environment variables.")); } else { const fallbackSpinner = spinner(); fallbackSpinner.start("Setting up fallback configuration..."); await writeEnvFile$1(projectDir); fallbackSpinner.stop("Fallback configuration ready"); displayManualSetupInstructions$1(); } } catch (error) { s.stop(pc.red("Prisma PostgreSQL setup failed")); consola$1.error(pc.red(`Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`)); try { await writeEnvFile$1(projectDir); displayManualSetupInstructions$1(); } catch {} log.info("Setup completed with manual configuration required."); } } //#endregion //#region src/helpers/database-providers/supabase-setup.ts async function writeSupabaseEnvFile(projectDir, databaseUrl) { try { const envPath = path.join(projectDir, "apps/server", ".env"); const dbUrlToUse = databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres"; const variables = [{ key: "DATABASE_URL", value: dbUrlToUse, condition: true }, { key: "DIRECT_URL", value: dbUrlToUse, condition: true }]; await addEnvVariablesToFile(envPath, variables); return true; } catch (error) { consola$1.error(pc.red("Failed to update .env file for Supabase.")); if (error instanceof Error) consola$1.error(error.message); return false; } } function extractDbUrl(output) { const dbUrlMatch = output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/); const url = dbUrlMatch?.[1]; if (url) return url; return null; } async function initializeSupabase(serverDir, packageManager) { log.info("Initializing Supabase project..."); try { const supabaseInitCommand = getPackageExecutionCommand(packageManager, "supabase init"); await execa(supabaseInitCommand, { cwd: serverDir, stdio: "inherit", shell: true }); log.success("Supabase project initialized"); return true; } catch (error) { consola$1.error(pc.red("Failed to initialize Supabase project.")); if (error instanceof Error) consola$1.error(error.message); else consola$1.error(String(error)); if (error instanceof Error && error.message.includes("ENOENT")) { log.error(pc.red("Supabase CLI not found. Please install it globally or ensure it's in your PATH.")); log.info("You can install it using: npm install -g supabase"); } return false; } } async function startSupabase(serverDir, packageManager) { log.info("Starting Supabase services (this may take a moment)..."); const supabaseStartCommand = getPackageExecutionCommand(packageManager, "supabase start"); try { const subprocess = execa(supabaseStartCommand, { cwd: serverDir, shell: true }); let stdoutData = ""; if (subprocess.stdout) subprocess.stdout.on("data", (data) => { const text$1 = data.toString(); process.stdout.write(text$1); stdoutData += text$1; }); if (subprocess.stderr) subprocess.stderr.pipe(process.stderr); await subprocess; await new Promise((resolve) => setTimeout(resolve, 100)); return stdoutData; } catch (error) { consola$1.error(pc.red("Failed to start Supabase services.")); const execaError = error; if (execaError?.message) { consola$1.error(`Error details: ${execaError.message}`); if (execaError.message.includes("Docker is not running")) log.error(pc.red("Docker is not running. Please start Docker and try again.")); } else consola$1.error(String(error)); return null; } } function displayManualSupabaseInstructions(output) { log.info(`"Manual Supabase Setup Instructions:" 1. Ensure Docker is installed and running. 2. Install the Supabase CLI (e.g., \`npm install -g supabase\`). 3. Run \`supabase init\` in your project's \`apps/server\` directory. 4. Run \`supabase start\` in your project's \`apps/server\` directory. 5. Copy the 'DB URL' from the output.${output ? ` ${pc.bold("Relevant output from `supabase start`:")} ${pc.dim(output)}` : ""} 6. Add the DB URL to the .env file in \`apps/server/.env\` as \`DATABASE_URL\`: ${pc.gray("DATABASE_URL=\"your_supabase_db_url\"")}`); } async function setupSupabase(config) { const { projectDir, packageManager } = config; const serverDir = path.join(projectDir, "apps", "server"); try { await fs.ensureDir(serverDir); const initialized = await initializeSupabase(serverDir, packageManager); if (!initialized) { displayManualSupabaseInstructions(); return; } const supabaseOutput = await startSupabase(serverDir, packageManager); if (!supabaseOutput) { displayManualSupabaseInstructions(); return; } const dbUrl = extractDbUrl(supabaseOutput); if (dbUrl) { const envUpdated = await writeSupabaseEnvFile(projectDir, dbUrl); if (envUpdated) log.success(pc.green("Supabase local development setup ready!")); else { log.error(pc.red("Supabase setup completed, but failed to update .env automatically.")); displayManualSupabaseInstructions(supabaseOutput); } } else { log.error(pc.yellow("Supabase started, but could not extract DB URL automatically.")); displayManualSupabaseInstructions(supabaseOutput); } } catch (error) { if (error instanceof Error) consola$1.error(pc.red(`Error during Supabase setup: ${error.message}`)); else consola$1.error(pc.red(`An unknown error occurred during Supabase setup: ${String(error)}`)); displayManualSupabaseInstructions(); } } //#endregion //#region src/helpers/database-providers/turso-setup.ts async function isTursoInstalled() { return commandExists("turso"); } async function isTursoLoggedIn() { try { const output = await $`turso auth whoami`; return !output.stdout.includes("You are not logged in"); } catch { return false; } } async function loginToTurso() { const s = spinner(); try { s.start("Logging in to Turso..."); await $`turso auth login`; s.stop("Logged into Turso"); return true; } catch (_error) { s.stop(pc.red("Failed to log in to Turso")); } } async function installTursoCLI(isMac) { const s = spinner(); try { s.start("Installing Turso CLI..."); if (isMac) await $`brew install tursodatabase/tap/turso`; else { const { stdout: installScript } = await $`curl -sSfL https://get.tur.so/install.sh`; await $`bash -c '${installScript}'`; } s.stop("Turso CLI installed"); return true; } catch (error) { if (error instanceof Error && error.message.includes("User force closed")) { s.stop("Turso CLI installation cancelled"); log.warn(pc.yellow("Turso CLI installation cancelled by user")); throw new Error("Installation cancelled"); } s.stop(pc.red("Failed to install Turso CLI")); } } async function getTursoGroups() { const s = spinner(); try { s.start("Fetching Turso groups..."); const { stdout } = await $`turso group list`; const lines = stdout.trim().split("\n"); if (lines.length <= 1) { s.stop("No Turso groups found"); return []; } const groups = lines.slice(1).map((line) => { const [name, locations, version, status] = line.trim().split(/\s{2,}/); return { name, locations, version, status }; }); s.stop(`Found ${groups.length} Turso groups`); return groups; } catch (error) { s.stop(pc.red("Error fetching Turso groups")); console.error("Error fetching Turso groups:", error); return []; } } async function selectTursoGroup() { const groups = await getTursoGroups(); if (groups.length === 0) return null; if (groups.length === 1) { log.info(`Using the only available group: ${pc.blue(groups[0].name)}`); return groups[0].name; } const groupOptions = groups.map((group$1) => ({ value: group$1.name, label: `${group$1.name} (${group$1.locations})` })); const selectedGroup = await select({ message: "Select a Turso database group:", options: groupOptions }); if (isCancel(selectedGroup)) { cancel(pc.red("Operation cancelled")); process.exit(0); } return selectedGroup; } async function createTursoDatabase(dbName, groupName) { const s = spinner(); try { s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`); if (groupName) await $`turso db create ${dbName} --group ${groupName}`; else await $`turso db create ${dbName}`; s.stop(`Turso database "${dbName}" created`); } catch (error) { s.stop(pc.red(`Failed to create database "${dbName}"`)); if (error instanceof Error && error.message.includes("already exists")) throw new Error("DATABASE_EXISTS"); } s.start("Retrieving database connection details..."); try { const { stdout: dbUrl } = await $`turso db show ${dbName} --url`; const { stdout: authToken } = await $`turso db tokens create ${dbName}`; s.stop("Database connection details retrieved"); return { dbUrl: dbUrl.trim(), authToken: authToken.trim() }; } catch (_error) { s.stop(pc.red("Failed to retrieve database connection details")); } } async function writeEnvFile(projectDir, config) { const envPath = path.join(projectDir, "apps/server", ".env"); const variables = [{ key: "DATABASE_URL", value: config?.dbUrl ?? "", condition: true }, { key: "DATABASE_AUTH_TOKEN", value: config?.authToken ?? "", condition: true }]; await addEnvVariablesToFile(envPath, variables); } function displayManualSetupInstructions() { log.info(`Manual Turso Setup Instructions: 1. Visit https://turso.tech and create an account 2. Create a new database from the dashboard 3. Get your database URL and authentication token 4. Add these credentials to the .env file in apps/server/.env DATABASE_URL=your_database_url DATABASE_AUTH_TOKEN=your_auth_token`); } async function setupTurso(config) { const { orm, projectDir } = config; const _isDrizzle = orm === "drizzle"; const setupSpinner = spinner(); setupSpinner.start("Checking Turso CLI availability..."); try { const platform = os.platform(); const isMac = platform === "darwin"; const _isLinux = platform === "linux"; const isWindows = platform === "win32"; if (isWindows) { setupSpinner.stop(pc.yellow("Turso setup not supported on Windows")); log.warn(pc.yellow("Automatic Turso setup is not supported on Windows.")); await writeEnvFile(projectDir); displayManualSetupInstructions(); return; } setupSpinner.stop("Turso CLI availability checked"); const isCliInstalled = await isTursoInstalled(); if (!isCliInstalled) { const shouldInstall = await confirm({ message: "Would you like to install Turso CLI?", initialValue: true }); if (isCancel(shouldInstall)) { cancel(pc.red("Operation cancelled")); pro