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,570 lines (1,541 loc) • 209 kB
JavaScript
#!/usr/bin/env node
import { cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
import consola, { consola as consola$1 } from "consola";
import pc from "picocolors";
import { createCli, trpcServer } from "trpc-cli";
import z from "zod";
import path from "node:path";
import fs from "fs-extra";
import { fileURLToPath } from "node:url";
import gradient from "gradient-string";
import * as JSONC from "jsonc-parser";
import { $, execa } from "execa";
import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
import { globby } from "globby";
import handlebars from "handlebars";
import os from "node:os";
//#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",
webDeploy: "none"
};
const dependencyVersionMap = {
"better-auth": "^1.3.4",
"@better-auth/expo": "^1.3.4",
"drizzle-orm": "^0.44.2",
"drizzle-kit": "^0.31.2",
"@libsql/client": "^0.15.9",
"@neondatabase/serverless": "^1.0.1",
pg: "^8.14.1",
"@types/pg": "^8.11.11",
"@types/ws": "^8.18.1",
ws: "^8.18.3",
mysql2: "^3.14.0",
"@prisma/client": "^6.13.0",
prisma: "^6.13.0",
"@prisma/extension-accelerate": "^2.0.2",
mongoose: "^8.14.0",
"vite-plugin-pwa": "^1.0.1",
"@vite-pwa/assets-generator": "^1.0.0",
"@tauri-apps/cli": "^2.4.0",
"@biomejs/biome": "^2.1.2",
oxlint: "^1.8.0",
ultracite: "5.1.1",
husky: "^9.1.7",
"lint-staged": "^16.1.2",
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/node-server": "^1.14.4",
"@hono/trpc-server": "^0.4.0",
hono: "^4.8.2",
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",
"@orpc/server": "^1.5.0",
"@orpc/client": "^1.5.0",
"@orpc/tanstack-query": "^1.5.0",
"@trpc/tanstack-react-query": "^11.4.2",
"@trpc/server": "^11.4.2",
"@trpc/client": "^11.4.2",
convex: "^1.25.4",
"@convex-dev/react-query": "^0.0.0-alpha.8",
"convex-svelte": "^0.0.11",
"convex-nuxt": "0.1.5",
"convex-vue": "^0.1.5",
"@tanstack/svelte-query": "^5.74.4",
"@tanstack/vue-query-devtools": "^5.83.0",
"@tanstack/vue-query": "^5.83.0",
"@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.23.0",
"@cloudflare/vite-plugin": "^1.9.0",
"@opennextjs/cloudflare": "^1.3.0",
"nitro-cloudflare-dev": "^0.2.2",
"@sveltejs/adapter-cloudflare": "^7.0.4"
};
const ADDON_COMPATIBILITY = {
pwa: [
"tanstack-router",
"react-router",
"solid",
"next"
],
tauri: [
"tanstack-router",
"react-router",
"nuxt",
"svelte",
"solid"
],
biome: [],
husky: [],
turborepo: [],
starlight: [],
ultracite: [],
oxlint: [],
fumadocs: [],
none: []
};
const WEB_FRAMEWORKS = [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"svelte",
"solid"
];
//#endregion
//#region src/types.ts
const DatabaseSchema = z.enum([
"none",
"sqlite",
"postgres",
"mysql",
"mongodb"
]).describe("Database type");
const ORMSchema = z.enum([
"drizzle",
"prisma",
"mongoose",
"none"
]).describe("ORM type");
const BackendSchema = z.enum([
"hono",
"express",
"fastify",
"next",
"elysia",
"convex",
"none"
]).describe("Backend framework");
const RuntimeSchema = z.enum([
"bun",
"node",
"workers",
"none"
]).describe("Runtime environment (workers only available with hono backend and drizzle orm)");
const FrontendSchema = z.enum([
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"native-nativewind",
"native-unistyles",
"svelte",
"solid",
"none"
]).describe("Frontend framework");
const AddonsSchema = z.enum([
"pwa",
"tauri",
"starlight",
"biome",
"husky",
"turborepo",
"fumadocs",
"ultracite",
"oxlint",
"none"
]).describe("Additional addons");
const ExamplesSchema = z.enum([
"todo",
"ai",
"none"
]).describe("Example templates to include");
const PackageManagerSchema = z.enum([
"npm",
"pnpm",
"bun"
]).describe("Package manager");
const DatabaseSetupSchema = z.enum([
"turso",
"neon",
"prisma-postgres",
"mongodb-atlas",
"supabase",
"d1",
"docker",
"none"
]).describe("Database hosting setup");
const APISchema = z.enum([
"trpc",
"orpc",
"none"
]).describe("API type");
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) => {
const invalidChars = [
"<",
">",
":",
"\"",
"|",
"?",
"*"
];
return !invalidChars.some((char) => name.includes(char));
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
const WebDeploySchema = z.enum(["workers", "none"]).describe("Web deployment");
//#endregion
//#region src/utils/addon-compatibility.ts
function validateAddonCompatibility(addon, frontend) {
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
if (compatibleFrontends.length === 0) return { isCompatible: true };
const hasCompatibleFrontend = frontend.some((f) => compatibleFrontends.includes(f));
if (!hasCompatibleFrontend) {
const frontendList = compatibleFrontends.join(", ");
return {
isCompatible: false,
reason: `${addon} addon requires one of these frontends: ${frontendList}`
};
}
return { isCompatible: true };
}
function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
return allAddons.filter((addon) => {
if (existingAddons.includes(addon)) return false;
if (addon === "none") return false;
const { isCompatible } = validateAddonCompatibility(addon, frontend);
return isCompatible;
});
}
//#endregion
//#region src/prompts/addons.ts
function getAddonDisplay(addon) {
let label;
let hint;
switch (addon) {
case "turborepo":
label = "Turborepo";
hint = "High-performance build system";
break;
case "pwa":
label = "PWA";
hint = "Make your app installable and work offline";
break;
case "tauri":
label = "Tauri";
hint = "Build native desktop apps from your web frontend";
break;
case "biome":
label = "Biome";
hint = "Format, lint, and more";
break;
case "oxlint":
label = "Oxlint";
hint = "Rust-powered linter";
break;
case "ultracite":
label = "Ultracite";
hint = "Zero-config Biome preset with AI integration";
break;
case "husky":
label = "Husky";
hint = "Modern native Git hooks made easy";
break;
case "starlight":
label = "Starlight";
hint = "Build stellar docs with astro";
break;
case "fumadocs":
label = "Fumadocs";
hint = "Build excellent documentation site";
break;
default:
label = addon;
hint = `Add ${addon}`;
}
return {
label,
hint
};
}
const ADDON_GROUPS = {
Documentation: ["starlight", "fumadocs"],
Linting: [
"biome",
"oxlint",
"ultracite"
],
Other: [
"turborepo",
"pwa",
"tauri",
"husky"
]
};
async function getAddonsChoice(addons, frontends) {
if (addons !== void 0) return addons;
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
const groupedOptions = {
Documentation: [],
Linting: [],
Other: []
};
const frontendsArray = frontends || [];
for (const addon of allAddons) {
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
if (!isCompatible) continue;
const { label, hint } = getAddonDisplay(addon);
const option = {
value: addon,
label,
hint
};
if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
else if (ADDON_GROUPS.Other.includes(addon)) groupedOptions.Other.push(option);
}
Object.keys(groupedOptions).forEach((group$1) => {
if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
});
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue)));
const response = await groupMultiselect({
message: "Select addons",
options: groupedOptions,
initialValues,
required: false,
selectableGroups: false
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
async function getAddonsToAdd(frontend, existingAddons = []) {
const groupedOptions = {
Documentation: [],
Linting: [],
Other: []
};
const frontendArray = frontend || [];
const compatibleAddons = getCompatibleAddons(AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons);
for (const addon of compatibleAddons) {
const { label, hint } = getAddonDisplay(addon);
const option = {
value: addon,
label,
hint
};
if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
else if (ADDON_GROUPS.Other.includes(addon)) groupedOptions.Other.push(option);
}
Object.keys(groupedOptions).forEach((group$1) => {
if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
});
if (Object.keys(groupedOptions).length === 0) return [];
const response = await groupMultiselect({
message: "Select addons to add",
options: groupedOptions,
required: false,
selectableGroups: false
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/api.ts
async function getApiChoice(Api, frontend, backend) {
if (backend === "convex" || backend === "none") return "none";
if (Api) return Api;
const includesNuxt = frontend?.includes("nuxt");
const includesSvelte = frontend?.includes("svelte");
const includesSolid = frontend?.includes("solid");
let apiOptions = [
{
value: "trpc",
label: "tRPC",
hint: "End-to-end typesafe APIs made easy"
},
{
value: "orpc",
label: "oRPC",
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
},
{
value: "none",
label: "None",
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
}
];
if (includesNuxt || includesSvelte || includesSolid) apiOptions = [{
value: "orpc",
label: "oRPC",
hint: `End-to-end type-safe APIs (Recommended for ${includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"} frontend)`
}, {
value: "none",
label: "None",
hint: "No API layer"
}];
const apiType = await select({
message: "Select API type",
options: apiOptions,
initialValue: apiOptions[0].value
});
if (isCancel(apiType)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return apiType;
}
//#endregion
//#region src/prompts/auth.ts
async function getAuthChoice(auth, hasDatabase, backend) {
if (backend === "convex") return false;
if (!hasDatabase) return false;
if (auth !== void 0) return auth;
const response = await confirm({
message: "Add authentication with Better-Auth?",
initialValue: DEFAULT_CONFIG.auth
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/backend.ts
async function getBackendFrameworkChoice(backendFramework, frontends) {
if (backendFramework !== void 0) return backendFramework;
const hasIncompatibleFrontend = frontends?.some((f) => f === "solid");
const backendOptions = [
{
value: "hono",
label: "Hono",
hint: "Lightweight, ultrafast web framework"
},
{
value: "next",
label: "Next.js",
hint: "separate api routes only backend"
},
{
value: "express",
label: "Express",
hint: "Fast, unopinionated, minimalist web framework for Node.js"
},
{
value: "fastify",
label: "Fastify",
hint: "Fast, low-overhead web framework for Node.js"
},
{
value: "elysia",
label: "Elysia",
hint: "Ergonomic web framework for building backend servers"
}
];
if (!hasIncompatibleFrontend) backendOptions.push({
value: "convex",
label: "Convex",
hint: "Reactive backend-as-a-service platform"
});
backendOptions.push({
value: "none",
label: "None",
hint: "No backend server"
});
const response = await select({
message: "Select backend",
options: backendOptions,
initialValue: DEFAULT_CONFIG.backend
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/database.ts
async function getDatabaseChoice(database, backend, runtime) {
if (backend === "convex" || backend === "none") return "none";
if (database !== void 0) return database;
const databaseOptions = [
{
value: "none",
label: "None",
hint: "No database setup"
},
{
value: "sqlite",
label: "SQLite",
hint: "lightweight, server-less, embedded relational database"
},
{
value: "postgres",
label: "PostgreSQL",
hint: "powerful, open source object-relational database system"
},
{
value: "mysql",
label: "MySQL",
hint: "popular open-source relational database system"
}
];
if (runtime !== "workers") databaseOptions.push({
value: "mongodb",
label: "MongoDB",
hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
});
const response = await select({
message: "Select database",
options: databaseOptions,
initialValue: DEFAULT_CONFIG.database
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/database-setup.ts
async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
if (backend === "convex") return "none";
if (dbSetup !== void 0) return dbSetup;
if (databaseType === "none") return "none";
if (databaseType === "sqlite" && orm === "prisma") return "none";
let options = [];
if (databaseType === "sqlite") options = [
{
value: "turso",
label: "Turso",
hint: "SQLite for Production. Powered by libSQL"
},
...runtime === "workers" ? [{
value: "d1",
label: "Cloudflare D1",
hint: "Cloudflare's managed, serverless database with SQLite's SQL semantics"
}] : [],
{
value: "none",
label: "None",
hint: "Manual setup"
}
];
else if (databaseType === "postgres") options = [
{
value: "neon",
label: "Neon Postgres",
hint: "Serverless Postgres with branching capability"
},
{
value: "supabase",
label: "Supabase",
hint: "Local Supabase stack (requires Docker)"
},
{
value: "prisma-postgres",
label: "Prisma Postgres",
hint: "Instant Postgres for Global Applications"
},
{
value: "docker",
label: "Docker",
hint: "Run locally with docker compose"
},
{
value: "none",
label: "None",
hint: "Manual setup"
}
];
else if (databaseType === "mysql") options = [{
value: "docker",
label: "Docker",
hint: "Run locally with docker compose"
}, {
value: "none",
label: "None",
hint: "Manual setup"
}];
else if (databaseType === "mongodb") options = [
{
value: "mongodb-atlas",
label: "MongoDB Atlas",
hint: "The most effective way to deploy MongoDB"
},
{
value: "docker",
label: "Docker",
hint: "Run locally with docker compose"
},
{
value: "none",
label: "None",
hint: "Manual setup"
}
];
else return "none";
const response = await select({
message: `Select ${databaseType} setup option`,
options,
initialValue: "none"
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/examples.ts
async function getExamplesChoice(examples, database, frontends, backend, api) {
if (api === "none") return [];
if (examples !== void 0) return examples;
if (backend === "convex") return ["todo"];
if (backend === "none") return [];
if (database === "none") return [];
const noFrontendSelected = !frontends || frontends.length === 0;
if (noFrontendSelected) return [];
let response = [];
const options = [{
value: "todo",
label: "Todo App",
hint: "A simple CRUD example app"
}];
if (backend !== "elysia" && !frontends?.includes("solid")) options.push({
value: "ai",
label: "AI Chat",
hint: "A simple AI chat interface using AI SDK"
});
response = await multiselect({
message: "Include examples",
options,
required: false,
initialValues: DEFAULT_CONFIG.examples
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/frontend.ts
async function getFrontendChoice(frontendOptions, backend) {
if (frontendOptions !== void 0) return frontendOptions;
const frontendTypes = await multiselect({
message: "Select project type",
options: [{
value: "web",
label: "Web",
hint: "React, Vue or Svelte Web Application"
}, {
value: "native",
label: "Native",
hint: "Create a React Native/Expo app"
}],
required: false,
initialValues: ["web"]
});
if (isCancel(frontendTypes)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
const result = [];
if (frontendTypes.includes("web")) {
const allWebOptions = [
{
value: "tanstack-router",
label: "TanStack Router",
hint: "Modern and scalable routing for React Applications"
},
{
value: "react-router",
label: "React Router",
hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
},
{
value: "next",
label: "Next.js",
hint: "The React Framework for the Web"
},
{
value: "nuxt",
label: "Nuxt",
hint: "The Progressive Web Framework for Vue.js"
},
{
value: "svelte",
label: "Svelte",
hint: "web development for the rest of us"
},
{
value: "solid",
label: "Solid",
hint: "Simple and performant reactivity for building user interfaces"
},
{
value: "tanstack-start",
label: "TanStack Start (vite)",
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
}
];
const webOptions = allWebOptions.filter((option) => {
if (backend === "convex") return option.value !== "solid";
return true;
});
const webFramework = await select({
message: "Choose web",
options: webOptions,
initialValue: DEFAULT_CONFIG.frontend[0]
});
if (isCancel(webFramework)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
result.push(webFramework);
}
if (frontendTypes.includes("native")) {
const nativeFramework = await select({
message: "Choose native",
options: [{
value: "native-nativewind",
label: "NativeWind",
hint: "Use Tailwind CSS for React Native"
}, {
value: "native-unistyles",
label: "Unistyles",
hint: "Consistent styling for React Native"
}],
initialValue: "native-nativewind"
});
if (isCancel(nativeFramework)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
result.push(nativeFramework);
}
return result;
}
//#endregion
//#region src/prompts/git.ts
async function getGitChoice(git) {
if (git !== void 0) return git;
const response = await confirm({
message: "Initialize git repository?",
initialValue: DEFAULT_CONFIG.git
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/install.ts
async function getinstallChoice(install) {
if (install !== void 0) return install;
const response = await confirm({
message: "Install dependencies?",
initialValue: DEFAULT_CONFIG.install
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/orm.ts
const ormOptions = {
prisma: {
value: "prisma",
label: "Prisma",
hint: "Powerful, feature-rich ORM"
},
mongoose: {
value: "mongoose",
label: "Mongoose",
hint: "Elegant object modeling tool"
},
drizzle: {
value: "drizzle",
label: "Drizzle",
hint: "Lightweight and performant TypeScript ORM"
}
};
async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
if (backend === "convex") return "none";
if (!hasDatabase) return "none";
if (orm !== void 0) return orm;
if (runtime === "workers") return "drizzle";
const options = [...database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma]];
const response = await select({
message: "Select ORM",
options,
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/package-manager.ts
async function getPackageManagerChoice(packageManager) {
if (packageManager !== void 0) return packageManager;
const detectedPackageManager = getUserPkgManager();
const response = await select({
message: "Choose package manager",
options: [
{
value: "npm",
label: "npm",
hint: "Node Package Manager"
},
{
value: "pnpm",
label: "pnpm",
hint: "Fast, disk space efficient package manager"
},
{
value: "bun",
label: "bun",
hint: "All-in-one JavaScript runtime & toolkit"
}
],
initialValue: detectedPackageManager
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/runtime.ts
async function getRuntimeChoice(runtime, backend) {
if (backend === "convex" || backend === "none") return "none";
if (runtime !== void 0) return runtime;
if (backend === "next") return "node";
const runtimeOptions = [{
value: "bun",
label: "Bun",
hint: "Fast all-in-one JavaScript runtime"
}, {
value: "node",
label: "Node.js",
hint: "Traditional Node.js runtime"
}];
if (backend === "hono") runtimeOptions.push({
value: "workers",
label: "Cloudflare Workers",
hint: "Edge runtime on Cloudflare's global network"
});
const response = await select({
message: "Select runtime",
options: runtimeOptions,
initialValue: DEFAULT_CONFIG.runtime
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/web-deploy.ts
function hasWebFrontend(frontends) {
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
}
function getDeploymentDisplay(deployment) {
if (deployment === "workers") return {
label: "Cloudflare Workers",
hint: "Deploy to Cloudflare Workers using Wrangler"
};
return {
label: deployment,
hint: `Add ${deployment} deployment`
};
}
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
if (deployment !== void 0) return deployment;
if (!hasWebFrontend(frontend)) return "none";
const options = [{
value: "workers",
label: "Cloudflare Workers",
hint: "Deploy to Cloudflare Workers using Wrangler"
}, {
value: "none",
label: "None",
hint: "Manual setup"
}];
const response = await select({
message: "Select web deployment",
options,
initialValue: DEFAULT_CONFIG.webDeploy
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
async function getDeploymentToAdd(frontend, existingDeployment) {
if (!hasWebFrontend(frontend)) return "none";
const options = [];
if (existingDeployment !== "workers") {
const { label, hint } = getDeploymentDisplay("workers");
options.push({
value: "workers",
label,
hint
});
}
if (existingDeployment && existingDeployment !== "none") return "none";
if (options.length > 0) options.push({
value: "none",
label: "None",
hint: "Skip deployment setup"
});
if (options.length === 0) return "none";
const response = await select({
message: "Select web deployment",
options,
initialValue: DEFAULT_CONFIG.webDeploy
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}
//#endregion
//#region src/prompts/config-prompts.ts
async function gatherConfig(flags, projectName, projectDir, relativePath) {
const result = await group({
frontend: () => getFrontendChoice(flags.frontend, flags.backend),
backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
database: ({ results }) => getDatabaseChoice(flags.database, results.backend, results.runtime),
orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
auth: ({ results }) => getAuthChoice(flags.auth, results.database !== "none", results.backend),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager),
install: () => getinstallChoice(flags.install)
}, { onCancel: () => {
cancel(pc.red("Operation cancelled"));
process.exit(0);
} });
if (result.backend === "convex") {
result.runtime = "none";
result.database = "none";
result.orm = "none";
result.api = "none";
result.auth = false;
result.dbSetup = "none";
result.examples = ["todo"];
result.webDeploy = "none";
}
if (result.backend === "none") {
result.runtime = "none";
result.database = "none";
result.orm = "none";
result.api = "none";
result.auth = false;
result.dbSetup = "none";
result.examples = [];
result.webDeploy = "none";
}
return {
projectName,
projectDir,
relativePath,
frontend: result.frontend,
backend: result.backend,
runtime: result.runtime,
database: result.database,
orm: result.orm,
auth: result.auth,
addons: result.addons,
examples: result.examples,
git: result.git,
packageManager: result.packageManager,
install: result.install,
dbSetup: result.dbSetup,
api: result.api,
webDeploy: result.webDeploy
};
}
//#endregion
//#region src/prompts/project-name.ts
function validateDirectoryName(name) {
if (name === ".") return void 0;
const result = ProjectNameSchema.safeParse(name);
if (!result.success) return result.error.issues[0]?.message || "Invalid project name";
return void 0;
}
async function getProjectName(initialName) {
if (initialName) {
if (initialName === ".") return initialName;
const finalDirName = path.basename(initialName);
const validationError = validateDirectoryName(finalDirName);
if (!validationError) return initialName;
}
let isValid = false;
let projectPath = "";
let defaultName = DEFAULT_CONFIG.projectName;
let counter = 1;
while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0) {
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
counter++;
}
while (!isValid) {
const response = await text({
message: "Enter your project name or path (relative to current directory)",
placeholder: defaultName,
initialValue: initialName,
defaultValue: defaultName,
validate: (value) => {
const nameToUse = value.trim() || defaultName;
const finalDirName = path.basename(nameToUse);
const validationError = validateDirectoryName(finalDirName);
if (validationError) return validationError;
if (nameToUse !== ".") {
const projectDir = path.resolve(process.cwd(), nameToUse);
if (!projectDir.startsWith(process.cwd())) return "Project path must be within current directory";
}
return void 0;
}
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
projectPath = response || defaultName;
isValid = true;
}
return projectPath;
}
//#endregion
//#region src/utils/get-latest-cli-version.ts
const getLatestCLIVersion = () => {
const packageJsonPath = path.join(PKG_ROOT, "package.json");
const packageJsonContent = fs.readJSONSync(packageJsonPath);
return packageJsonContent.version ?? "1.0.0";
};
//#endregion
//#region src/utils/telemetry.ts
/**
* Returns true if telemetry/analytics should be enabled, false otherwise.
*
* - If BTS_TELEMETRY_DISABLED is present and "1", disables analytics.
* - Otherwise, BTS_TELEMETRY: "0" disables, "1" enables (default: enabled).
*/
function isTelemetryEnabled() {
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
const BTS_TELEMETRY = "1";
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
return true;
}
//#endregion
//#region src/utils/analytics.ts
const POSTHOG_API_KEY = "phc_8ZUxEwwfKMajJLvxz1daGd931dYbQrwKNficBmsdIrs";
const POSTHOG_HOST = "https://us.i.posthog.com";
async function trackProjectCreation(config) {
if (!isTelemetryEnabled()) return;
const sessionId = `cli_${crypto.randomUUID().replace(/-/g, "")}`;
const { projectName, projectDir, relativePath,...safeConfig } = config;
const payload = {
api_key: POSTHOG_API_KEY,
event: "project_created",
properties: {
...safeConfig,
cli_version: getLatestCLIVersion(),
node_version: process.version,
platform: process.platform,
$ip: null
},
distinct_id: sessionId
};
try {
await fetch(`${POSTHOG_HOST}/capture`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
} catch (_error) {}
}
//#endregion
//#region src/utils/display-config.ts
function displayConfig(config) {
const configDisplay = [];
if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
if (config.frontend !== void 0) {
const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
}
if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
if (config.auth !== void 0) {
const authText = typeof config.auth === "boolean" ? config.auth ? "Yes" : "No" : String(config.auth);
configDisplay.push(`${pc.blue("Authentication:")} ${authText}`);
}
if (config.addons !== void 0) {
const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
}
if (config.examples !== void 0) {
const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
}
if (config.git !== void 0) {
const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
}
if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
if (config.install !== void 0) {
const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
}
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
return configDisplay.join("\n");
}
//#endregion
//#region src/utils/generate-reproducible-command.ts
function generateReproducibleCommand(config) {
const flags = [];
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
else flags.push("--frontend none");
flags.push(`--backend ${config.backend}`);
flags.push(`--runtime ${config.runtime}`);
flags.push(`--database ${config.database}`);
flags.push(`--orm ${config.orm}`);
flags.push(`--api ${config.api}`);
flags.push(config.auth ? "--auth" : "--no-auth");
if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
else flags.push("--addons none");
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
else flags.push("--examples none");
flags.push(`--db-setup ${config.dbSetup}`);
flags.push(`--web-deploy ${config.webDeploy}`);
flags.push(config.git ? "--git" : "--no-git");
flags.push(`--package-manager ${config.packageManager}`);
flags.push(config.install ? "--install" : "--no-install");
let baseCommand = "";
const pkgManager = config.packageManager;
if (pkgManager === "npm") baseCommand = "npx create-better-t-stack@latest";
else if (pkgManager === "pnpm") baseCommand = "pnpm create better-t-stack@latest";
else if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
}
//#endregion
//#region src/utils/project-directory.ts
async function handleDirectoryConflict(currentPathInput) {
while (true) {
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
const dirExists = fs.pathExistsSync(resolvedPath);
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
if (!dirIsNotEmpty) return {
finalPathInput: currentPathInput,
shouldClearDirectory: false
};
log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
const action = await select({
message: "What would you like to do?",
options: [
{
value: "overwrite",
label: "Overwrite",
hint: "Empty the directory and create the project"
},
{
value: "merge",
label: "Merge",
hint: "Create project files inside, potentially overwriting conflicts"
},
{
value: "rename",
label: "Choose a different name/path",
hint: "Keep the existing directory and create a new one"
},
{
value: "cancel",
label: "Cancel",
hint: "Abort the process"
}
],
initialValue: "rename"
});
if (isCancel(action)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
switch (action) {
case "overwrite": return {
finalPathInput: currentPathInput,
shouldClearDirectory: true
};
case "merge":
log.info(`Proceeding into existing directory "${pc.yellow(currentPathInput)}". Files may be overwritten.`);
return {
finalPathInput: currentPathInput,
shouldClearDirectory: false
};
case "rename": {
log.info("Please choose a different project name or path.");
const newPathInput = await getProjectName(void 0);
return await handleDirectoryConflict(newPathInput);
}
case "cancel":
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
}
}
async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
let finalResolvedPath;
let finalBaseName;
if (finalPathInput === ".") {
finalResolvedPath = process.cwd();
finalBaseName = path.basename(finalResolvedPath);
} else {
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
finalBaseName = path.basename(finalResolvedPath);
}
if (shouldClearDirectory) {
const s = spinner();
s.start(`Clearing directory "${finalResolvedPath}"...`);
try {
await fs.emptyDir(finalResolvedPath);
s.stop(`Directory "${finalResolvedPath}" cleared.`);
} catch (error) {
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
consola$1.error(error);
process.exit(1);
}
} else await fs.ensureDir(finalResolvedPath);
return {
finalResolvedPath,
finalBaseName
};
}
//#endregion
//#region src/utils/render-title.ts
const TITLE_TEXT = `
██████╗ ███████╗████████╗████████╗███████╗██████╗
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████╗ ██║ ███████║██║ █████╔╝
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`;
const catppuccinTheme = {
pink: "#F5C2E7",
mauve: "#CBA6F7",
red: "#F38BA8",
maroon: "#E78284",
peach: "#FAB387",
yellow: "#F9E2AF",
green: "#A6E3A1",
teal: "#94E2D5",
sky: "#89DCEB",
sapphire: "#74C7EC",
lavender: "#B4BEFE"
};
const renderTitle = () => {
const terminalWidth = process.stdout.columns || 80;
const titleLines = TITLE_TEXT.split("\n");
const titleWidth = Math.max(...titleLines.map((line) => line.length));
if (terminalWidth < titleWidth) {
const simplifiedTitle = `
╔══════════════════╗
║ Better T Stack ║
╚══════════════════╝
`;
console.log(gradient(Object.values(catppuccinTheme)).multiline(simplifiedTitle));
} else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
};
//#endregion
//#region src/validation.ts
function processAndValidateFlags(options, providedFlags, projectName) {
const config = {};
if (options.api) {
config.api = options.api;
if (options.api === "none") {
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") {
consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
process.exit(1);
}
}
}
if (options.backend) config.backend = options.backend;
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
if (providedFlags.has("runtime") && options.runtime === "none") {
consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
process.exit(1);
}
}
if (options.database) config.database = options.database;
if (options.orm) config.orm = options.orm;
if (options.auth !== void 0) config.auth = options.auth;
if (options.git !== void 0) config.git = options.git;
if (options.install !== void 0) config.install = options.install;
if (options.runtime) config.runtime = options.runtime;
if (options.dbSetup) config.dbSetup = options.dbSetup;
if (options.packageManager) config.packageManager = options.packageManager;
if (options.webDeploy) config.webDeploy = options.webDeploy;
if (projectName) {
const result = ProjectNameSchema.safeParse(path.basename(projectName));
if (!result.success) {
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
process.exit(1);
}
config.projectName = projectName;
} else if (options.projectDirectory) {
const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
const result = ProjectNameSchema.safeParse(baseName);
if (!result.success) {
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
process.exit(1);
}
config.projectName = baseName;
}
if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
if (options.frontend.length > 1) {
consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
process.exit(1);
}
config.frontend = [];
} else {
const validOptions = options.frontend.filter((f) => f !== "none");
const webFrontends = validOptions.filter((f) => WEB_FRAMEWORKS.includes(f));
const nativeFrontends = validOptions.filter((f) => f === "native-nativewind" || f === "native-unistyles");
if (webFrontends.length > 1) {
consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
process.exit(1);
}
if (nativeFrontends.length > 1) {
consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
process.exit(1);
}
config.frontend = validOptions;
}
if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
if (options.addons.length > 1) {
consola$1.fatal(`Cannot combine 'none' with other addons.`);
process.exit(1);
}
config.addons = [];
} else config.addons = options.addons.filter((addon) => addon !== "none");
if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
if (options.examples.length > 1) {
consola$1.fatal("Cannot combine 'none' with other examples.");
process.exit(1);
}
config.examples = [];
} else {
config.examples = options.examples.filter((ex) => ex !== "none");
if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
}
if (config.backend === "convex") {
const incompatibleFlags = [];
if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
if (incompatibleFlags.length > 0) {
consola$1.fatal(`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(", ")}. Please remove them.`);
process.exit(1);
}
if (providedFlags.has("frontend") && options.frontend) {
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
if (incompatibleFrontends.length > 0) {
consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
process.exit(1);
}
}
config.auth = false;
config.database = "none";
config.orm = "none";
config.api = "none";
config.runtime = "none";
config.dbSetup = "none";
config.examples = ["todo"];
} else if (config.backend === "none") {
const incompatibleFlags = [];
if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
if (providedFlags.has("examples") && options.examples) {
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
if (hasNonNoneExamples) incompatibleFlags.push("--examples");
}
if (incompatibleFlags.length > 0) {
consola$1.fatal(`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(", ")}. Please remove them.`);
process.exit(1);
}
config.auth = false;
config.database = "none";
config.orm = "none";
config.api = "none";
config.runtime = "none";
config.dbSetup = "none";
config.examples = [];
}
if (config.orm === "mongoose" && config.database !== "mongodb") {
consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
process.exit(1);
}
if (config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
process.exit(1);
}
if (config.orm === "drizzle" && config.database === "mongodb") {
consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
process.exit(1);
}
if (config.database && config.database !== "none" && config.orm === "none") {
consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
process.exit(1);
}
if (config.orm && config.orm !== "none" && config.database === "none") {
consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
process.exit(1);
}
if (config.auth && config.database === "none") {
consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
process.exit(1);
}
if (config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
process.exit(1);
}
if (config.dbSetup === "turso" && config.database !== "sqlite") {
consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "neon" && config.database !== "postgres") {
consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "supabase" && config.database !== "postgres") {
consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "d1") {
if (config.database !== "sqlite") {
consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
process.exit(1);
}
if (config.runtime !== "workers") {
consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
process.exit(1);
}
}
if (config.dbSetup === "docker" && config.database === "sqlite") {
consola$1.fatal("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
process.exit(1);
}
if (config.dbSetup === "docker" && config.runtime === "workers") {
consola$1.fatal("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
process.exit(1);
}
if (providedFlags.has("runtime") && options.runtime === "w