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,542 lines (1,511 loc) • 283 kB
JavaScript
#!/usr/bin/env node
import { autocompleteMultiselect, cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
import pc from "picocolors";
import { createCli, trpcServer } from "trpc-cli";
import z from "zod";
import path from "node:path";
import consola, { consola as consola$1 } from "consola";
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 { glob } from "tinyglobby";
import handlebars from "handlebars";
import { Biome } from "@biomejs/js-api/nodejs";
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_BASE = {
projectName: "my-better-t-app",
relativePath: "my-better-t-app",
frontend: ["tanstack-router"],
database: "sqlite",
orm: "drizzle",
auth: "better-auth",
payments: "none",
addons: ["turborepo"],
examples: [],
git: true,
install: true,
dbSetup: "none",
backend: "hono",
runtime: "bun",
api: "trpc",
webDeploy: "none",
serverDeploy: "none"
};
function getDefaultConfig() {
return {
...DEFAULT_CONFIG_BASE,
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
packageManager: getUserPkgManager(),
frontend: [...DEFAULT_CONFIG_BASE.frontend],
addons: [...DEFAULT_CONFIG_BASE.addons],
examples: [...DEFAULT_CONFIG_BASE.examples]
};
}
const DEFAULT_CONFIG = getDefaultConfig();
const dependencyVersionMap = {
"better-auth": "^1.3.13",
"@better-auth/expo": "^1.3.13",
"@clerk/nextjs": "^6.31.5",
"@clerk/clerk-react": "^5.45.0",
"@clerk/tanstack-react-start": "^0.23.1",
"@clerk/clerk-expo": "^2.14.25",
"drizzle-orm": "^0.44.2",
"drizzle-kit": "^0.31.2",
"@planetscale/database": "^1.19.0",
"@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.15.0",
prisma: "^6.15.0",
"@prisma/adapter-d1": "^6.15.0",
"@prisma/extension-accelerate": "^2.0.2",
"@prisma/adapter-libsql": "^6.15.0",
"@prisma/adapter-planetscale": "^6.15.0",
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.2.0",
oxlint: "^1.12.0",
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.3.1",
"@elysiajs/cors": "^1.3.3",
"@elysiajs/trpc": "^1.1.0",
elysia: "^1.3.21",
"@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: "^5.0.39",
"@ai-sdk/google": "^2.0.13",
"@ai-sdk/vue": "^2.0.39",
"@ai-sdk/svelte": "^3.0.39",
"@ai-sdk/react": "^2.0.39",
streamdown: "^1.3.0",
shiki: "^3.12.2",
"@orpc/server": "^1.8.6",
"@orpc/client": "^1.8.6",
"@orpc/openapi": "^1.8.6",
"@orpc/zod": "^1.8.6",
"@orpc/tanstack-query": "^1.8.6",
"@trpc/tanstack-react-query": "^11.5.0",
"@trpc/server": "^11.5.0",
"@trpc/client": "^11.5.0",
convex: "^1.27.0",
"@convex-dev/react-query": "^0.0.0-alpha.8",
"convex-svelte": "^0.0.11",
"convex-nuxt": "0.1.5",
"convex-vue": "^0.1.5",
"@convex-dev/better-auth": "^0.8.4",
"@tanstack/svelte-query": "^5.85.3",
"@tanstack/svelte-query-devtools": "^5.85.3",
"@tanstack/vue-query-devtools": "^5.83.0",
"@tanstack/vue-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-query": "^5.85.5",
"@tanstack/solid-query": "^5.87.4",
"@tanstack/solid-query-devtools": "^5.87.4",
"@tanstack/solid-router-devtools": "^1.131.44",
wrangler: "^4.23.0",
"@cloudflare/vite-plugin": "^1.9.0",
"@opennextjs/cloudflare": "^1.6.5",
"nitro-cloudflare-dev": "^0.2.2",
"@sveltejs/adapter-cloudflare": "^7.2.1",
"@cloudflare/workers-types": "^4.20250822.0",
alchemy: "^0.67.0",
nitropack: "^2.12.4",
dotenv: "^17.2.1",
"@polar-sh/better-auth": "^1.1.3",
"@polar-sh/sdk": "^0.34.16"
};
const ADDON_COMPATIBILITY = {
pwa: [
"tanstack-router",
"react-router",
"solid",
"next"
],
tauri: [
"tanstack-router",
"react-router",
"nuxt",
"svelte",
"solid",
"next"
],
biome: [],
husky: [],
turborepo: [],
starlight: [],
ultracite: [],
ruler: [],
oxlint: [],
fumadocs: [],
none: []
};
//#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");
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",
"ruler",
"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",
"planetscale",
"mongodb-atlas",
"supabase",
"d1",
"docker",
"none"
]).describe("Database hosting setup");
const APISchema = z.enum([
"trpc",
"orpc",
"none"
]).describe("API type");
const AuthSchema = z.enum([
"better-auth",
"clerk",
"none"
]).describe("Authentication provider");
const PaymentsSchema = z.enum(["polar", "none"]).describe("Payments provider");
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([
"wrangler",
"alchemy",
"none"
]).describe("Web deployment");
const ServerDeploySchema = z.enum([
"wrangler",
"alchemy",
"none"
]).describe("Server deployment");
const DirectoryConflictSchema = z.enum([
"merge",
"overwrite",
"increment",
"error"
]).describe("How to handle existing directory conflicts");
//#endregion
//#region src/utils/compatibility.ts
const WEB_FRAMEWORKS = [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"svelte",
"solid"
];
//#endregion
//#region src/utils/errors.ts
function isProgrammatic() {
return process.env.BTS_PROGRAMMATIC === "1";
}
function exitWithError(message) {
consola.error(pc.red(message));
if (isProgrammatic()) throw new Error(message);
process.exit(1);
}
function exitCancelled(message = "Operation cancelled") {
cancel(pc.red(message));
if (isProgrammatic()) throw new Error(message);
process.exit(0);
}
function handleError(error, fallbackMessage) {
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
consola.error(pc.red(message));
if (isProgrammatic()) throw new Error(message);
process.exit(1);
}
//#endregion
//#region src/utils/compatibility-rules.ts
function isWebFrontend(value) {
return WEB_FRAMEWORKS.includes(value);
}
function splitFrontends(values = []) {
const web = values.filter((f) => isWebFrontend(f));
const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
return {
web,
native
};
}
function ensureSingleWebAndNative(frontends) {
const { web, native } = splitFrontends(frontends);
if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
}
function validateWorkersCompatibility(providedFlags, options, config) {
if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. 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.");
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup (--db-setup docker) 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.");
}
function validateApiFrontendCompatibility(api, frontends = []) {
const includesNuxt = frontends.includes("nuxt");
const includesSvelte = frontends.includes("svelte");
const includesSolid = frontends.includes("solid");
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
}
function isFrontendAllowedWithBackend(frontend, backend, auth) {
if (backend === "convex" && frontend === "solid") return false;
if (auth === "clerk" && backend === "convex") {
const incompatibleFrontends = [
"nuxt",
"svelte",
"solid"
];
if (incompatibleFrontends.includes(frontend)) return false;
}
return true;
}
function allowedApisForFrontends(frontends = []) {
const includesNuxt = frontends.includes("nuxt");
const includesSvelte = frontends.includes("svelte");
const includesSolid = frontends.includes("solid");
const base = [
"trpc",
"orpc",
"none"
];
if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
return base;
}
function isExampleTodoAllowed(backend, database) {
return !(backend !== "convex" && backend !== "none" && database === "none");
}
function isExampleAIAllowed(_backend, frontends = []) {
const includesSolid = frontends.includes("solid");
if (includesSolid) return false;
return true;
}
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
}
function validateServerDeployRequiresBackend(serverDeploy, backend) {
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
}
function validateAddonCompatibility(addon, frontend, _auth) {
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
if (compatibleFrontends.length > 0) {
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 = [], auth) {
return allAddons.filter((addon) => {
if (existingAddons.includes(addon)) return false;
if (addon === "none") return false;
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
return isCompatible;
});
}
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
for (const addon of addons) {
if (addon === "none") continue;
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
}
}
function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
if (!payments || payments === "none") return;
if (payments === "polar") {
if (!auth || auth === "none" || auth !== "better-auth") exitWithError("Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.");
const { web } = splitFrontends(frontends);
if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
}
}
function validateExamplesCompatibility(examples, backend, database, frontend) {
const examplesArr = examples ?? [];
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
}
//#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 "ruler":
label = "Ruler";
hint = "Centralize your AI rules";
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: [
"ruler",
"turborepo",
"pwa",
"tauri",
"husky"
]
};
async function getAddonsChoice(addons, frontends, auth) {
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, auth);
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)) return exitCancelled("Operation cancelled");
return response;
}
async function getAddonsToAdd(frontend, existingAddons = [], auth) {
const groupedOptions = {
Documentation: [],
Linting: [],
Other: []
};
const frontendArray = frontend || [];
const compatibleAddons = getCompatibleAddons(AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
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)) return exitCancelled("Operation cancelled");
return response;
}
//#endregion
//#region src/prompts/api.ts
async function getApiChoice(Api, frontend, backend) {
if (backend === "convex" || backend === "none") return "none";
const allowed = allowedApisForFrontends(frontend ?? []);
if (Api) return allowed.includes(Api) ? Api : allowed[0];
const apiOptions = allowed.map((a) => a === "trpc" ? {
value: "trpc",
label: "tRPC",
hint: "End-to-end typesafe APIs made easy"
} : a === "orpc" ? {
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)"
});
const apiType = await select({
message: "Select API type",
options: apiOptions,
initialValue: apiOptions[0].value
});
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
return apiType;
}
//#endregion
//#region src/prompts/auth.ts
async function getAuthChoice(auth, hasDatabase, backend, frontend) {
if (auth !== void 0) return auth;
if (backend === "convex") {
const supportedBetterAuthFrontends = frontend?.some((f) => [
"tanstack-router",
"tanstack-start",
"next"
].includes(f));
const hasClerkCompatibleFrontends = frontend?.some((f) => [
"react-router",
"tanstack-router",
"tanstack-start",
"next",
"native-nativewind",
"native-unistyles"
].includes(f));
const options = [];
if (supportedBetterAuthFrontends) options.push({
value: "better-auth",
label: "Better-Auth",
hint: "comprehensive auth framework for TypeScript"
});
if (hasClerkCompatibleFrontends) options.push({
value: "clerk",
label: "Clerk",
hint: "More than auth, Complete User Management"
});
options.push({
value: "none",
label: "None",
hint: "No auth"
});
const response$1 = await select({
message: "Select authentication provider",
options,
initialValue: "none"
});
if (isCancel(response$1)) return exitCancelled("Operation cancelled");
return response$1;
}
if (!hasDatabase) return "none";
const response = await select({
message: "Select authentication provider",
options: [{
value: "better-auth",
label: "Better-Auth",
hint: "comprehensive auth framework for TypeScript"
}, {
value: "none",
label: "None"
}],
initialValue: DEFAULT_CONFIG.auth
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
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";
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: "planetscale",
label: "PlanetScale",
hint: "Postgres & Vitess (MySQL) on NVMe"
},
{
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: "planetscale",
label: "PlanetScale",
hint: "MySQL on Vitess (NVMe, HA)"
},
{
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)) return exitCancelled("Operation cancelled");
return response;
}
//#endregion
//#region src/prompts/examples.ts
async function getExamplesChoice(examples, database, frontends, backend, api) {
if (examples !== void 0) return examples;
if (api === "none") {
if (backend === "convex") return ["todo"];
return [];
}
if (backend === "convex") return ["todo"];
if (backend === "none") return [];
if (database === "none") return [];
let response = [];
const options = [];
if (isExampleTodoAllowed(backend, database)) options.push({
value: "todo",
label: "Todo App",
hint: "A simple CRUD example app"
});
if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
value: "ai",
label: "AI Chat",
hint: "A simple AI chat interface using AI SDK"
});
if (options.length === 0) return [];
response = await multiselect({
message: "Include examples",
options,
required: false,
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}
//#endregion
//#region src/prompts/frontend.ts
async function getFrontendChoice(frontendOptions, backend, auth) {
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)) return exitCancelled("Operation cancelled");
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",
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
}
];
const webOptions = allWebOptions.filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
const webFramework = await select({
message: "Choose web",
options: webOptions,
initialValue: DEFAULT_CONFIG.frontend[0]
});
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
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;
const options = [...database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma]];
const response = await select({
message: "Select ORM",
options,
initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
return response;
}
//#endregion
//#region src/prompts/payments.ts
async function getPaymentsChoice(payments, auth, backend, frontends) {
if (payments !== void 0) return payments;
const isPolarCompatible = auth === "better-auth" && backend !== "convex" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
if (!isPolarCompatible) return "none";
const options = [{
value: "polar",
label: "Polar",
hint: "Turn your software into a business. 6 lines of code."
}, {
value: "none",
label: "None",
hint: "No payments integration"
}];
const response = await select({
message: "Select payments provider",
options,
initialValue: DEFAULT_CONFIG.payments
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
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)) return exitCancelled("Operation cancelled");
return response;
}
//#endregion
//#region src/prompts/server-deploy.ts
function getDeploymentDisplay$1(deployment) {
if (deployment === "wrangler") return {
label: "Wrangler",
hint: "Deploy to Cloudflare Workers using Wrangler"
};
if (deployment === "alchemy") return {
label: "Alchemy",
hint: "Deploy to Cloudflare Workers using Alchemy"
};
return {
label: deployment,
hint: `Add ${deployment} deployment`
};
}
async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
if (deployment !== void 0) return deployment;
if (backend === "none" || backend === "convex") return "none";
if (backend !== "hono") return "none";
const options = [];
if (runtime !== "workers") return "none";
["alchemy", "wrangler"].forEach((deploy) => {
const { label, hint } = getDeploymentDisplay$1(deploy);
options.unshift({
value: deploy,
label,
hint
});
});
const response = await select({
message: "Select server deployment",
options,
initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}
async function getServerDeploymentToAdd(runtime, existingDeployment, backend) {
if (backend !== "hono") return "none";
const options = [];
if (runtime === "workers") {
if (existingDeployment !== "wrangler") {
const { label, hint } = getDeploymentDisplay$1("wrangler");
options.push({
value: "wrangler",
label,
hint
});
}
if (existingDeployment !== "alchemy") {
const { label, hint } = getDeploymentDisplay$1("alchemy");
options.push({
value: "alchemy",
label,
hint
});
}
}
if (existingDeployment && existingDeployment !== "none") return "none";
if (options.length > 0) {}
if (options.length === 0) return "none";
const response = await select({
message: "Select server deployment",
options,
initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
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 === "wrangler") return {
label: "Wrangler",
hint: "Deploy to Cloudflare Workers using Wrangler"
};
if (deployment === "alchemy") return {
label: "Alchemy",
hint: "Deploy to Cloudflare Workers using Alchemy"
};
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 availableDeployments = [
"wrangler",
"alchemy",
"none"
];
const options = availableDeployments.map((deploy) => {
const { label, hint } = getDeploymentDisplay(deploy);
return {
value: deploy,
label,
hint
};
});
const response = await select({
message: "Select web deployment",
options,
initialValue: DEFAULT_CONFIG.webDeploy
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}
async function getDeploymentToAdd(frontend, existingDeployment) {
if (!hasWebFrontend(frontend)) return "none";
const options = [];
if (existingDeployment !== "wrangler") {
const { label, hint } = getDeploymentDisplay("wrangler");
options.push({
value: "wrangler",
label,
hint
});
}
if (existingDeployment !== "alchemy") {
const { label, hint } = getDeploymentDisplay("alchemy");
options.push({
value: "alchemy",
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)) return exitCancelled("Operation cancelled");
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, flags.auth),
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, results.frontend),
payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth),
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),
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager),
install: () => getinstallChoice(flags.install)
}, { onCancel: () => exitCancelled("Operation cancelled") });
return {
projectName,
projectDir,
relativePath,
frontend: result.frontend,
backend: result.backend,
runtime: result.runtime,
database: result.database,
orm: result.orm,
auth: result.auth,
payments: result.payments,
addons: result.addons,
examples: result.examples,
git: result.git,
packageManager: result.packageManager,
install: result.install,
dbSetup: result.dbSetup,
api: result.api,
webDeploy: result.webDeploy,
serverDeploy: result.serverDeploy
};
}
//#endregion
//#region src/prompts/project-name.ts
function isPathWithinCwd(targetPath) {
const resolved = path.resolve(targetPath);
const rel = path.relative(process.cwd(), resolved);
return !rel.startsWith("..") && !path.isAbsolute(rel);
}
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) {
const projectDir = path.resolve(process.cwd(), initialName);
if (isPathWithinCwd(projectDir)) return initialName;
consola.error(pc.red("Project path must be within current directory"));
}
}
let isValid = false;
let projectPath = "";
let defaultName = DEFAULT_CONFIG.projectName;
let counter = 1;
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(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 = String(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 (!isPathWithinCwd(projectDir)) return "Project path must be within current directory";
}
return void 0;
}
});
if (isCancel(response)) return exitCancelled("Operation cancelled.");
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";
function generateSessionId() {
const rand = Math.random().toString(36).slice(2);
const now = Date.now().toString(36);
return `cli_${now}${rand}`;
}
async function trackProjectCreation(config, disableAnalytics = false) {
if (!isTelemetryEnabled() || disableAnalytics) return;
const sessionId = generateSessionId();
const { projectName, projectDir, relativePath,...safeConfig } = config;
const payload = {
api_key: POSTHOG_API_KEY,
event: "project_created",
properties: {
...safeConfig,
cli_version: getLatestCLIVersion(),
node_version: typeof process !== "undefined" ? process.version : "",
platform: typeof process !== "undefined" ? 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) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
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 (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
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(`--auth ${config.auth}`);
flags.push(`--payments ${config.payments}`);
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(`--server-deploy ${config.serverDeploy}`);
flags.push(config.git ? "--git" : "--no-git");
flags.push(`--package-manager ${config.packageManager}`);
flags.push(config.install ? "--install" : "--no-install");
let baseCommand = "npx create-better-t-stack@latest";
const pkgManager = config.packageManager;
if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
else if (pkgManager === "pnpm") baseCommand = "pnpm create better-t-stack@latest";
else if (pkgManager === "npm") baseCommand = "npx 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, silent = false) {
while (true) {
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
const dirExists = await fs.pathExists(resolvedPath);
const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
if (!dirIsNotEmpty) return {
finalPathInput: currentPathInput,
shouldClearDirectory: false
};
if (silent) throw new Error(`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`);
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"