diff --git a/apps/cli/README.md b/apps/cli/README.md
index 6d1eb50..f592b6a 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -27,7 +27,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **TypeScript** | End-to-end type safety across all parts of your application |
| **Frontend** | • React with TanStack Router
• React with React Router
• React with TanStack Start (SSR)
• Next.js
• SvelteKit
• Nuxt (Vue)
• SolidJS
• React Native with NativeWind (via Expo)
• React Native with Unistyles (via Expo)
• None |
-| **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex
• None |
+| **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex
• Fastify
• None |
| **API Layer** | • tRPC (type-safe APIs)
• oRPC (OpenAPI-compatible type-safe APIs)
• None |
| **Runtime** | • Bun
• Node.js |
| **Database** | • SQLite
• PostgreSQL
• MySQL
• MongoDB
• None |
@@ -60,7 +60,7 @@ Options:
--install Install dependencies
--no-install Skip installing dependencies
--db-setup Database setup (turso, neon, prisma-postgres, mongodb-atlas, none)
- --backend Backend framework (hono, express, elysia, next, convex)
+ --backend Backend framework (hono, express, elysia, next, convex, fastify, none)
--runtime Runtime (bun, node, none)
--api API type (trpc, orpc, none)
-h, --help Display help
diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts
new file mode 100644
index 0000000..c12d381
--- /dev/null
+++ b/apps/cli/src/cli.ts
@@ -0,0 +1,134 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import type { YargsArgv } from "./types";
+import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
+
+export async function parseCliArguments(): Promise {
+ const argv = await yargs(hideBin(process.argv))
+ .scriptName("create-better-t-stack")
+ .usage(
+ "$0 [project-directory] [options]",
+ "Create a new Better-T Stack project",
+ )
+ .positional("project-directory", {
+ describe: "Project name/directory",
+ type: "string",
+ })
+ .option("yes", {
+ alias: "y",
+ type: "boolean",
+ describe: "Use default configuration and skip prompts",
+ default: false,
+ })
+ .option("database", {
+ type: "string",
+ describe: "Database type",
+ choices: ["none", "sqlite", "postgres", "mysql", "mongodb"],
+ })
+ .option("orm", {
+ type: "string",
+ describe: "ORM type",
+ choices: ["drizzle", "prisma", "mongoose", "none"],
+ })
+ .option("auth", {
+ type: "boolean",
+ describe: "Include authentication (use --no-auth to exclude)",
+ })
+ .option("frontend", {
+ type: "array",
+ string: true,
+ describe: "Frontend types",
+ choices: [
+ "tanstack-router",
+ "react-router",
+ "tanstack-start",
+ "next",
+ "nuxt",
+ "native-nativewind",
+ "native-unistyles",
+ "svelte",
+ "solid",
+ "none",
+ ],
+ })
+ .option("addons", {
+ type: "array",
+ string: true,
+ describe: "Additional addons",
+ choices: [
+ "pwa",
+ "tauri",
+ "starlight",
+ "biome",
+ "husky",
+ "turborepo",
+ "none",
+ ],
+ })
+ .option("examples", {
+ type: "array",
+ string: true,
+ describe: "Examples to include",
+ choices: ["todo", "ai", "none"],
+ })
+ .option("git", {
+ type: "boolean",
+ describe: "Initialize git repository (use --no-git to skip)",
+ })
+ .option("package-manager", {
+ alias: "pm",
+ type: "string",
+ describe: "Package manager",
+ choices: ["npm", "pnpm", "bun"],
+ })
+ .option("install", {
+ type: "boolean",
+ describe: "Install dependencies (use --no-install to skip)",
+ })
+ .option("db-setup", {
+ type: "string",
+ describe: "Database setup",
+ choices: [
+ "turso",
+ "neon",
+ "prisma-postgres",
+ "mongodb-atlas",
+ "supabase",
+ "none",
+ ],
+ })
+ .option("backend", {
+ type: "string",
+ describe: "Backend framework",
+ choices: [
+ "hono",
+ "express",
+ "fastify",
+ "next",
+ "elysia",
+ "convex",
+ "none",
+ ],
+ })
+ .option("runtime", {
+ type: "string",
+ describe: "Runtime",
+ choices: ["bun", "node", "none"],
+ })
+ .option("api", {
+ type: "string",
+ describe: "API type",
+ choices: ["trpc", "orpc", "none"],
+ })
+ .completion()
+ .recommendCommands()
+ .version(getLatestCLIVersion())
+ .alias("version", "v")
+ .help()
+ .alias("help", "h")
+ .strict()
+ .wrap(null)
+ .parse();
+
+ return argv as YargsArgv;
+}
diff --git a/apps/cli/src/helpers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts
similarity index 95%
rename from apps/cli/src/helpers/mongodb-atlas-setup.ts
rename to apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts
index 18ac7aa..c7ff434 100644
--- a/apps/cli/src/helpers/mongodb-atlas-setup.ts
+++ b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts
@@ -4,9 +4,12 @@ import consola from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectConfig } from "../types";
-import { commandExists } from "../utils/command-exists";
-import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
+import type { ProjectConfig } from "../../types";
+import { commandExists } from "../../utils/command-exists";
+import {
+ type EnvVariable,
+ addEnvVariablesToFile,
+} from "../project-generation/env-setup";
type MongoDBConfig = {
connectionString: string;
diff --git a/apps/cli/src/helpers/neon-setup.ts b/apps/cli/src/helpers/database-providers/neon-setup.ts
similarity index 91%
rename from apps/cli/src/helpers/neon-setup.ts
rename to apps/cli/src/helpers/database-providers/neon-setup.ts
index c65c030..be9dddb 100644
--- a/apps/cli/src/helpers/neon-setup.ts
+++ b/apps/cli/src/helpers/database-providers/neon-setup.ts
@@ -4,9 +4,12 @@ import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectPackageManager } from "../types";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
-import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
+import type { PackageManager } from "../../types";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
+import {
+ type EnvVariable,
+ addEnvVariablesToFile,
+} from "../project-generation/env-setup";
type NeonConfig = {
connectionString: string;
@@ -31,7 +34,7 @@ const NEON_REGIONS: NeonRegion[] = [
];
async function executeNeonCommand(
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
commandArgsString: string,
spinnerText?: string,
) {
@@ -52,7 +55,7 @@ async function executeNeonCommand(
}
}
-async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
+async function isNeonAuthenticated(packageManager: PackageManager) {
try {
const commandArgsString = "neonctl projects list";
const result = await executeNeonCommand(packageManager, commandArgsString);
@@ -65,7 +68,7 @@ async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
}
}
-async function authenticateWithNeon(packageManager: ProjectPackageManager) {
+async function authenticateWithNeon(packageManager: PackageManager) {
try {
await executeNeonCommand(
packageManager,
@@ -82,7 +85,7 @@ async function authenticateWithNeon(packageManager: ProjectPackageManager) {
async function createNeonProject(
projectName: string,
regionId: string,
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
) {
try {
const commandArgsString = `neonctl projects create --name ${projectName} --region-id ${regionId} --output json`;
@@ -146,7 +149,7 @@ function displayManualSetupInstructions() {
DATABASE_URL="your_connection_string"`);
}
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupNeonPostgres(config: ProjectConfig): Promise {
const { packageManager, projectDir } = config;
diff --git a/apps/cli/src/helpers/prisma-postgres-setup.ts b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts
similarity index 92%
rename from apps/cli/src/helpers/prisma-postgres-setup.ts
rename to apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts
index ca3a47a..329c516 100644
--- a/apps/cli/src/helpers/prisma-postgres-setup.ts
+++ b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts
@@ -4,10 +4,13 @@ import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectPackageManager } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
-import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
+import type { PackageManager } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
+import {
+ type EnvVariable,
+ addEnvVariablesToFile,
+} from "../project-generation/env-setup";
type PrismaConfig = {
databaseUrl: string;
@@ -15,7 +18,7 @@ type PrismaConfig = {
async function initPrismaDatabase(
serverDir: string,
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
): Promise {
const s = spinner();
try {
@@ -141,7 +144,7 @@ export default prisma;
}
}
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupPrismaPostgres(config: ProjectConfig) {
const { packageManager, projectDir } = config;
diff --git a/apps/cli/src/helpers/supabase-setup.ts b/apps/cli/src/helpers/database-providers/supabase-setup.ts
similarity index 94%
rename from apps/cli/src/helpers/supabase-setup.ts
rename to apps/cli/src/helpers/database-providers/supabase-setup.ts
index ce60b3c..bcf1707 100644
--- a/apps/cli/src/helpers/supabase-setup.ts
+++ b/apps/cli/src/helpers/database-providers/supabase-setup.ts
@@ -4,9 +4,12 @@ import { consola } from "consola";
import { type ExecaError, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectConfig, ProjectPackageManager } from "../types";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
-import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
+import type { PackageManager, ProjectConfig } from "../../types";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
+import {
+ type EnvVariable,
+ addEnvVariablesToFile,
+} from "../project-generation/env-setup";
async function writeSupabaseEnvFile(
projectDir: string,
@@ -50,7 +53,7 @@ function extractDbUrl(output: string): string | null {
async function initializeSupabase(
serverDir: string,
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
): Promise {
log.info("Initializing Supabase project...");
try {
@@ -86,7 +89,7 @@ async function initializeSupabase(
async function startSupabase(
serverDir: string,
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
): Promise {
log.info("Starting Supabase services (this may take a moment)...");
const supabaseStartCommand = getPackageExecutionCommand(
diff --git a/apps/cli/src/helpers/turso-setup.ts b/apps/cli/src/helpers/database-providers/turso-setup.ts
similarity index 97%
rename from apps/cli/src/helpers/turso-setup.ts
rename to apps/cli/src/helpers/database-providers/turso-setup.ts
index ac68c6a..5e54616 100644
--- a/apps/cli/src/helpers/turso-setup.ts
+++ b/apps/cli/src/helpers/database-providers/turso-setup.ts
@@ -12,8 +12,12 @@ import {
import consola from "consola";
import { $ } from "execa";
import pc from "picocolors";
-import { commandExists } from "../utils/command-exists";
-import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
+import type { ProjectConfig } from "../../types";
+import { commandExists } from "../../utils/command-exists";
+import {
+ type EnvVariable,
+ addEnvVariablesToFile,
+} from "../project-generation/env-setup";
type TursoConfig = {
dbUrl: string;
@@ -202,8 +206,6 @@ DATABASE_URL=your_database_url
DATABASE_AUTH_TOKEN=your_auth_token`);
}
-import type { ProjectConfig } from "../types";
-
export async function setupTurso(config: ProjectConfig): Promise {
const { orm, projectDir } = config;
const _isDrizzle = orm === "drizzle";
diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/project-generation/create-project.ts
similarity index 85%
rename from apps/cli/src/helpers/create-project.ts
rename to apps/cli/src/helpers/project-generation/create-project.ts
index db55021..0c73519 100644
--- a/apps/cli/src/helpers/create-project.ts
+++ b/apps/cli/src/helpers/project-generation/create-project.ts
@@ -1,19 +1,19 @@
import { cancel, log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectConfig } from "../types";
-import { setupAddons } from "./addons-setup";
-import { setupApi } from "./api-setup";
-import { setupAuth } from "./auth-setup";
-import { setupBackendDependencies } from "./backend-framework-setup";
+import type { ProjectConfig } from "../../types";
+import { setupAddons } from "../setup/addons-setup";
+import { setupApi } from "../setup/api-setup";
+import { setupAuth } from "../setup/auth-setup";
+import { setupBackendDependencies } from "../setup/backend-setup";
+import { setupDatabase } from "../setup/db-setup";
+import { setupExamples } from "../setup/examples-setup";
+import { setupRuntime } from "../setup/runtime-setup";
import { createReadme } from "./create-readme";
-import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup";
-import { setupExamples } from "./examples-setup";
import { installDependencies } from "./install-dependencies";
import { displayPostInstallInstructions } from "./post-installation";
import { initializeGit, updatePackageConfigurations } from "./project-config";
-import { setupRuntime } from "./runtime-setup";
import {
copyBaseTemplate,
handleExtras,
diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/project-generation/create-readme.ts
similarity index 96%
rename from apps/cli/src/helpers/create-readme.ts
rename to apps/cli/src/helpers/project-generation/create-readme.ts
index 18726cd..3e48683 100644
--- a/apps/cli/src/helpers/create-readme.ts
+++ b/apps/cli/src/helpers/project-generation/create-readme.ts
@@ -2,14 +2,14 @@ import path from "node:path";
import consola from "consola";
import fs from "fs-extra";
import type {
- ProjectAddons,
- ProjectApi,
+ API,
+ Addons,
+ Database,
+ Frontend,
+ ORM,
ProjectConfig,
- ProjectDatabase,
- ProjectFrontend,
- ProjectOrm,
- ProjectRuntime,
-} from "../types";
+ Runtime,
+} from "../../types";
export async function createReadme(projectDir: string, options: ProjectConfig) {
const readmePath = path.join(projectDir, "README.md");
@@ -206,14 +206,14 @@ ${generateScriptsList(
}
function generateFeaturesList(
- database: ProjectDatabase,
+ database: Database,
auth: boolean,
- addons: ProjectAddons[],
- orm: ProjectOrm,
- runtime: ProjectRuntime,
- frontend: ProjectFrontend[],
+ addons: Addons[],
+ orm: ORM,
+ runtime: Runtime,
+ frontend: Frontend[],
backend: string,
- api: ProjectApi,
+ api: API,
): string {
const isConvex = backend === "convex";
const hasTanstackRouter = frontend.includes("tanstack-router");
@@ -332,10 +332,10 @@ function generateFeaturesList(
}
function generateDatabaseSetup(
- database: ProjectDatabase,
+ database: Database,
auth: boolean,
packageManagerRunCmd: string,
- orm: ProjectOrm,
+ orm: ORM,
): string {
if (database === "none") {
return "";
@@ -405,11 +405,11 @@ ${packageManagerRunCmd} db:push
function generateScriptsList(
packageManagerRunCmd: string,
- database: ProjectDatabase,
- orm: ProjectOrm,
+ database: Database,
+ orm: ORM,
_auth: boolean,
hasNative: boolean,
- addons: ProjectAddons[],
+ addons: Addons[],
backend: string,
): string {
const isConvex = backend === "convex";
diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/project-generation/env-setup.ts
similarity index 98%
rename from apps/cli/src/helpers/env-setup.ts
rename to apps/cli/src/helpers/project-generation/env-setup.ts
index c72d20e..fbaa684 100644
--- a/apps/cli/src/helpers/env-setup.ts
+++ b/apps/cli/src/helpers/project-generation/env-setup.ts
@@ -1,7 +1,7 @@
import path from "node:path";
import fs from "fs-extra";
-import type { ProjectConfig } from "../types";
-import { generateAuthSecret } from "./auth-setup";
+import type { ProjectConfig } from "../../types";
+import { generateAuthSecret } from "../setup/auth-setup";
export interface EnvVariable {
key: string;
diff --git a/apps/cli/src/helpers/install-dependencies.ts b/apps/cli/src/helpers/project-generation/install-dependencies.ts
similarity index 87%
rename from apps/cli/src/helpers/install-dependencies.ts
rename to apps/cli/src/helpers/project-generation/install-dependencies.ts
index 921a21b..0f48ad4 100644
--- a/apps/cli/src/helpers/install-dependencies.ts
+++ b/apps/cli/src/helpers/project-generation/install-dependencies.ts
@@ -2,7 +2,7 @@ import { log, spinner } from "@clack/prompts";
import consola from "consola";
import { $ } from "execa";
import pc from "picocolors";
-import type { ProjectAddons, ProjectPackageManager } from "../types";
+import type { Addons, PackageManager } from "../../types";
export async function installDependencies({
projectDir,
@@ -10,8 +10,8 @@ export async function installDependencies({
addons = [],
}: {
projectDir: string;
- packageManager: ProjectPackageManager;
- addons?: ProjectAddons[];
+ packageManager: PackageManager;
+ addons?: Addons[];
}) {
const s = spinner();
@@ -38,7 +38,7 @@ export async function installDependencies({
async function runBiomeCheck(
projectDir: string,
- packageManager: ProjectPackageManager,
+ packageManager: PackageManager,
) {
const s = spinner();
diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/project-generation/post-installation.ts
similarity index 96%
rename from apps/cli/src/helpers/post-installation.ts
rename to apps/cli/src/helpers/project-generation/post-installation.ts
index b841a58..42b82c1 100644
--- a/apps/cli/src/helpers/post-installation.ts
+++ b/apps/cli/src/helpers/project-generation/post-installation.ts
@@ -1,9 +1,9 @@
import { consola } from "consola";
import pc from "picocolors";
-import type { ProjectDatabase, ProjectOrm, ProjectRuntime } from "../types";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
+import type { Database, ORM, Runtime } from "../../types";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export function displayPostInstallInstructions(
config: ProjectConfig & { depsInstalled: boolean },
@@ -157,10 +157,10 @@ function getLintingInstructions(runCmd?: string): string {
}
function getDatabaseInstructions(
- database: ProjectDatabase,
- orm?: ProjectOrm,
+ database: Database,
+ orm?: ORM,
runCmd?: string,
- runtime?: ProjectRuntime,
+ runtime?: Runtime,
): string {
const instructions = [];
diff --git a/apps/cli/src/helpers/project-config.ts b/apps/cli/src/helpers/project-generation/project-config.ts
similarity index 99%
rename from apps/cli/src/helpers/project-config.ts
rename to apps/cli/src/helpers/project-generation/project-config.ts
index ab109d5..9cda063 100644
--- a/apps/cli/src/helpers/project-config.ts
+++ b/apps/cli/src/helpers/project-generation/project-config.ts
@@ -3,7 +3,7 @@ import { log } from "@clack/prompts";
import { $, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function updatePackageConfigurations(
projectDir: string,
diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts
similarity index 99%
rename from apps/cli/src/helpers/template-manager.ts
rename to apps/cli/src/helpers/project-generation/template-manager.ts
index c18d388..6d697d5 100644
--- a/apps/cli/src/helpers/template-manager.ts
+++ b/apps/cli/src/helpers/project-generation/template-manager.ts
@@ -1,9 +1,9 @@
import path from "node:path";
import fs from "fs-extra";
import { globby } from "globby";
-import { PKG_ROOT } from "../constants";
-import type { ProjectConfig } from "../types";
-import { processTemplate } from "../utils/template-processor";
+import { PKG_ROOT } from "../../constants";
+import type { ProjectConfig } from "../../types";
+import { processTemplate } from "../../utils/template-processor";
async function processAndCopyFiles(
sourcePattern: string | string[],
diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/setup/addons-setup.ts
similarity index 91%
rename from apps/cli/src/helpers/addons-setup.ts
rename to apps/cli/src/helpers/setup/addons-setup.ts
index c538a1a..b980d3c 100644
--- a/apps/cli/src/helpers/addons-setup.ts
+++ b/apps/cli/src/helpers/setup/addons-setup.ts
@@ -1,11 +1,11 @@
import path from "node:path";
import fs from "fs-extra";
-import type { ProjectFrontend } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { Frontend } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
import { setupStarlight } from "./starlight-setup";
import { setupTauri } from "./tauri-setup";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupAddons(config: ProjectConfig) {
const { addons, frontend, projectDir } = config;
@@ -49,10 +49,7 @@ export async function setupAddons(config: ProjectConfig) {
}
}
-function getWebAppDir(
- projectDir: string,
- frontends: ProjectFrontend[],
-): string {
+function getWebAppDir(projectDir: string, frontends: Frontend[]): string {
if (
frontends.some((f) =>
["react-router", "tanstack-router", "nuxt", "svelte", "solid"].includes(
@@ -109,7 +106,7 @@ async function setupHusky(projectDir: string) {
}
}
-async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
+async function setupPwa(projectDir: string, frontends: Frontend[]) {
const isCompatibleFrontend = frontends.some((f) =>
["react-router", "tanstack-router", "solid"].includes(f),
);
diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/setup/api-setup.ts
similarity index 96%
rename from apps/cli/src/helpers/api-setup.ts
rename to apps/cli/src/helpers/setup/api-setup.ts
index 73a4088..c1089ea 100644
--- a/apps/cli/src/helpers/api-setup.ts
+++ b/apps/cli/src/helpers/setup/api-setup.ts
@@ -1,8 +1,8 @@
import path from "node:path";
import fs from "fs-extra";
-import type { AvailableDependencies } from "../constants";
-import type { ProjectConfig, ProjectFrontend } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { AvailableDependencies } from "../../constants";
+import type { Frontend, ProjectConfig } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupApi(config: ProjectConfig): Promise {
const { api, projectName, frontend, backend, packageManager, projectDir } =
@@ -120,7 +120,7 @@ export async function setupApi(config: ProjectConfig): Promise {
}
}
- const reactBasedFrontends: ProjectFrontend[] = [
+ const reactBasedFrontends: Frontend[] = [
"react-router",
"tanstack-router",
"tanstack-start",
diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/setup/auth-setup.ts
similarity index 94%
rename from apps/cli/src/helpers/auth-setup.ts
rename to apps/cli/src/helpers/setup/auth-setup.ts
index 429df83..89772f1 100644
--- a/apps/cli/src/helpers/auth-setup.ts
+++ b/apps/cli/src/helpers/setup/auth-setup.ts
@@ -2,8 +2,8 @@ import path from "node:path";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
-import type { ProjectConfig } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { ProjectConfig } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupAuth(config: ProjectConfig): Promise {
const { auth, frontend, backend, projectDir } = config;
diff --git a/apps/cli/src/helpers/backend-framework-setup.ts b/apps/cli/src/helpers/setup/backend-setup.ts
similarity index 89%
rename from apps/cli/src/helpers/backend-framework-setup.ts
rename to apps/cli/src/helpers/setup/backend-setup.ts
index a4a51af..aab86c9 100644
--- a/apps/cli/src/helpers/backend-framework-setup.ts
+++ b/apps/cli/src/helpers/setup/backend-setup.ts
@@ -1,8 +1,8 @@
import path from "node:path";
-import type { AvailableDependencies } from "../constants";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { AvailableDependencies } from "../../constants";
+import { addPackageDependency } from "../../utils/add-package-deps";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupBackendDependencies(
config: ProjectConfig,
diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/setup/db-setup.ts
similarity index 82%
rename from apps/cli/src/helpers/db-setup.ts
rename to apps/cli/src/helpers/setup/db-setup.ts
index 1228676..112c6d7 100644
--- a/apps/cli/src/helpers/db-setup.ts
+++ b/apps/cli/src/helpers/setup/db-setup.ts
@@ -3,15 +3,15 @@ import { spinner } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
-import { addPackageDependency } from "../utils/add-package-deps";
-import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
-import { setupPrismaPostgres } from "./prisma-postgres-setup";
-import { setupSupabase } from "./supabase-setup";
-import { setupTurso } from "./turso-setup";
+import { addPackageDependency } from "../../utils/add-package-deps";
+import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
+import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
+import { setupSupabase } from "../database-providers/supabase-setup";
+import { setupTurso } from "../database-providers/turso-setup";
-import { setupNeonPostgres } from "./neon-setup";
+import { setupNeonPostgres } from "../database-providers/neon-setup";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupDatabase(config: ProjectConfig): Promise {
const { database, orm, dbSetup, backend, projectDir } = config;
diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/setup/examples-setup.ts
similarity index 88%
rename from apps/cli/src/helpers/examples-setup.ts
rename to apps/cli/src/helpers/setup/examples-setup.ts
index 790828d..33da140 100644
--- a/apps/cli/src/helpers/examples-setup.ts
+++ b/apps/cli/src/helpers/setup/examples-setup.ts
@@ -1,8 +1,8 @@
import path from "node:path";
import fs from "fs-extra";
-import type { AvailableDependencies } from "../constants";
-import type { ProjectConfig } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { AvailableDependencies } from "../../constants";
+import type { ProjectConfig } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupExamples(config: ProjectConfig): Promise {
const { examples, frontend, backend, projectDir } = config;
diff --git a/apps/cli/src/helpers/runtime-setup.ts b/apps/cli/src/helpers/setup/runtime-setup.ts
similarity index 91%
rename from apps/cli/src/helpers/runtime-setup.ts
rename to apps/cli/src/helpers/setup/runtime-setup.ts
index b02af0d..c7f6158 100644
--- a/apps/cli/src/helpers/runtime-setup.ts
+++ b/apps/cli/src/helpers/setup/runtime-setup.ts
@@ -1,7 +1,7 @@
import path from "node:path";
import fs from "fs-extra";
-import type { ProjectBackend, ProjectConfig } from "../types";
-import { addPackageDependency } from "../utils/add-package-deps";
+import type { Backend, ProjectConfig } from "../../types";
+import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupRuntime(config: ProjectConfig): Promise {
const { runtime, backend, projectDir } = config;
@@ -25,7 +25,7 @@ export async function setupRuntime(config: ProjectConfig): Promise {
async function setupBunRuntime(
serverDir: string,
- _backend: ProjectBackend,
+ _backend: Backend,
): Promise {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
@@ -48,7 +48,7 @@ async function setupBunRuntime(
async function setupNodeRuntime(
serverDir: string,
- backend: ProjectBackend,
+ backend: Backend,
): Promise {
const packageJsonPath = path.join(serverDir, "package.json");
if (!(await fs.pathExists(packageJsonPath))) return;
diff --git a/apps/cli/src/helpers/starlight-setup.ts b/apps/cli/src/helpers/setup/starlight-setup.ts
similarity index 88%
rename from apps/cli/src/helpers/starlight-setup.ts
rename to apps/cli/src/helpers/setup/starlight-setup.ts
index 0a51d6e..868fd3a 100644
--- a/apps/cli/src/helpers/starlight-setup.ts
+++ b/apps/cli/src/helpers/setup/starlight-setup.ts
@@ -3,8 +3,8 @@ import { spinner } from "@clack/prompts";
import consola from "consola";
import { execa } from "execa";
import pc from "picocolors";
-import type { ProjectConfig } from "../types";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
+import type { ProjectConfig } from "../../types";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
export async function setupStarlight(config: ProjectConfig): Promise {
const { packageManager, projectDir } = config;
diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/setup/tauri-setup.ts
similarity index 92%
rename from apps/cli/src/helpers/tauri-setup.ts
rename to apps/cli/src/helpers/setup/tauri-setup.ts
index 9157023..576ef83 100644
--- a/apps/cli/src/helpers/tauri-setup.ts
+++ b/apps/cli/src/helpers/setup/tauri-setup.ts
@@ -4,10 +4,10 @@ import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
-import { addPackageDependency } from "../utils/add-package-deps";
-import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
+import { addPackageDependency } from "../../utils/add-package-deps";
+import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
-import type { ProjectConfig } from "../types";
+import type { ProjectConfig } from "../../types";
export async function setupTauri(config: ProjectConfig): Promise {
const { packageManager, frontend, projectDir } = config;
diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts
index 5d67653..fe0d1bb 100644
--- a/apps/cli/src/index.ts
+++ b/apps/cli/src/index.ts
@@ -11,30 +11,16 @@ import {
import { consola } from "consola";
import fs from "fs-extra";
import pc from "picocolors";
-import yargs from "yargs";
-import { hideBin } from "yargs/helpers";
+import { parseCliArguments } from "./cli";
import { DEFAULT_CONFIG } from "./constants";
-import { createProject } from "./helpers/create-project";
+import { createProject } from "./helpers/project-generation/create-project";
import { gatherConfig } from "./prompts/config-prompts";
import { getProjectName } from "./prompts/project-name";
-import type {
- ProjectAddons,
- ProjectApi,
- ProjectBackend,
- ProjectConfig,
- ProjectDBSetup,
- ProjectDatabase,
- ProjectExamples,
- ProjectFrontend,
- ProjectOrm,
- ProjectPackageManager,
- ProjectRuntime,
- YargsArgv,
-} from "./types";
+import type { ProjectConfig } from "./types";
import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
-import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
import { renderTitle } from "./utils/render-title";
+import { processAndValidateFlags } from "./validation";
const exit = () => process.exit(0);
process.on("SIGINT", exit);
@@ -44,133 +30,7 @@ async function main() {
const startTime = Date.now();
try {
- const argv = await yargs(hideBin(process.argv))
- .scriptName("create-better-t-stack")
- .usage(
- "$0 [project-directory] [options]",
- "Create a new Better-T Stack project",
- )
- .positional("project-directory", {
- describe: "Project name/directory",
- type: "string",
- })
- .option("yes", {
- alias: "y",
- type: "boolean",
- describe: "Use default configuration and skip prompts",
- default: false,
- })
- .option("database", {
- type: "string",
- describe: "Database type",
- choices: ["none", "sqlite", "postgres", "mysql", "mongodb"],
- })
- .option("orm", {
- type: "string",
- describe: "ORM type",
- choices: ["drizzle", "prisma", "mongoose", "none"],
- })
- .option("auth", {
- type: "boolean",
- describe: "Include authentication (use --no-auth to exclude)",
- })
- .option("frontend", {
- type: "array",
- string: true,
- describe: "Frontend types",
- choices: [
- "tanstack-router",
- "react-router",
- "tanstack-start",
- "next",
- "nuxt",
- "native-nativewind",
- "native-unistyles",
- "svelte",
- "solid",
- "none",
- ],
- })
- .option("addons", {
- type: "array",
- string: true,
- describe: "Additional addons",
- choices: [
- "pwa",
- "tauri",
- "starlight",
- "biome",
- "husky",
- "turborepo",
- "none",
- ],
- })
- .option("examples", {
- type: "array",
- string: true,
- describe: "Examples to include",
- choices: ["todo", "ai", "none"],
- })
- .option("git", {
- type: "boolean",
- describe: "Initialize git repository (use --no-git to skip)",
- })
- .option("package-manager", {
- alias: "pm",
- type: "string",
- describe: "Package manager",
- choices: ["npm", "pnpm", "bun"],
- })
- .option("install", {
- type: "boolean",
- describe: "Install dependencies (use --no-install to skip)",
- })
- .option("db-setup", {
- type: "string",
- describe: "Database setup",
- choices: [
- "turso",
- "neon",
- "prisma-postgres",
- "mongodb-atlas",
- "supabase",
- "none",
- ],
- })
- .option("backend", {
- type: "string",
- describe: "Backend framework",
- choices: [
- "hono",
- "express",
- "fastify",
- "next",
- "elysia",
- "convex",
- "none",
- ],
- })
- .option("runtime", {
- type: "string",
- describe: "Runtime",
- choices: ["bun", "node", "none"],
- })
- .option("api", {
- type: "string",
- describe: "API type",
- choices: ["trpc", "orpc", "none"],
- })
- .completion()
- .recommendCommands()
- .version(getLatestCLIVersion())
- .alias("version", "v")
- .help()
- .alias("help", "h")
- .strict()
- .wrap(null)
- .parse();
-
- const options = argv as YargsArgv;
+ const options = await parseCliArguments();
const cliProjectNameArg = options.projectDirectory;
renderTitle();
@@ -312,42 +172,13 @@ async function main() {
};
if (config.backend === "convex") {
- config.auth = false;
- config.database = "none";
- config.orm = "none";
- config.api = "none";
- config.runtime = "none";
- config.dbSetup = "none";
- config.examples = ["todo"];
log.info(
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
);
} else if (config.backend === "none") {
- config.auth = false;
- config.database = "none";
- config.orm = "none";
- config.api = "none";
- config.runtime = "none";
- config.dbSetup = "none";
- config.examples = [];
log.info(
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
);
- } else if (config.database === "none") {
- config.orm = "none";
- log.info(
- "Due to '--database none', '--orm' has been automatically set to 'none'.",
- );
-
- config.auth = false;
- log.info(
- "Due to '--database none', '--auth' has been automatically set to 'false'.",
- );
-
- config.dbSetup = "none";
- log.info(
- "Due to '--database none', '--db-setup' has been automatically set to 'none'.",
- );
}
log.info(
@@ -401,518 +232,6 @@ async function main() {
}
}
-function processAndValidateFlags(
- options: YargsArgv,
- projectName?: string,
-): Partial {
- const config: Partial = {};
- const providedFlags: Set = new Set(
- Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
- );
-
- if (options.api) {
- config.api = options.api as ProjectApi;
- if (options.api === "none") {
- if (
- options.examples &&
- !(options.examples.length === 1 && options.examples[0] === "none")
- ) {
- consola.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 as ProjectBackend;
- }
-
- if (
- providedFlags.has("backend") &&
- config.backend &&
- config.backend !== "convex" &&
- config.backend !== "none"
- ) {
- if (providedFlags.has("runtime") && options.runtime === "none") {
- consola.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 as ProjectDatabase;
- }
- if (options.orm) {
- config.orm = options.orm as ProjectOrm;
- }
- if (options.auth !== undefined) {
- config.auth = options.auth;
- }
- if (options.git !== undefined) {
- config.git = options.git;
- }
- if (options.install !== undefined) {
- config.install = options.install;
- }
- if (options.runtime) {
- config.runtime = options.runtime as ProjectRuntime;
- }
- if (options.dbSetup) {
- config.dbSetup = options.dbSetup as ProjectDBSetup;
- }
- if (options.packageManager) {
- config.packageManager = options.packageManager as ProjectPackageManager;
- }
-
- if (projectName) {
- config.projectName = projectName;
- } else if (options.projectDirectory) {
- config.projectName = path.basename(
- path.resolve(process.cwd(), options.projectDirectory),
- );
- }
-
- if (options.frontend && options.frontend.length > 0) {
- if (options.frontend.includes("none")) {
- if (options.frontend.length > 1) {
- consola.fatal(`Cannot combine 'none' with other frontend options.`);
- process.exit(1);
- }
- config.frontend = [];
- } else {
- const validOptions = options.frontend.filter(
- (f): f is ProjectFrontend => f !== "none",
- );
- const webFrontends = validOptions.filter(
- (f) =>
- f === "tanstack-router" ||
- f === "react-router" ||
- f === "tanstack-start" ||
- f === "next" ||
- f === "nuxt" ||
- f === "svelte" ||
- f === "solid",
- );
- const nativeFrontends = validOptions.filter(
- (f) => f === "native-nativewind" || f === "native-unistyles",
- );
-
- if (webFrontends.length > 1) {
- consola.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.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.fatal(`Cannot combine 'none' with other addons.`);
- process.exit(1);
- }
- config.addons = [];
- } else {
- config.addons = options.addons.filter(
- (addon): addon is ProjectAddons => addon !== "none",
- );
- }
- }
- if (options.examples && options.examples.length > 0) {
- if (options.examples.includes("none")) {
- if (options.examples.length > 1) {
- consola.fatal("Cannot combine 'none' with other examples.");
- process.exit(1);
- }
- config.examples = [];
- } else {
- config.examples = options.examples.filter(
- (ex): ex is ProjectExamples => ex !== "none",
- );
- if (options.examples.includes("none") && config.backend !== "convex") {
- config.examples = [];
- }
- }
- }
-
- if (config.backend === "convex") {
- const incompatibleFlags: string[] = [];
-
- 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.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 === "nuxt" || f === "solid",
- );
- if (incompatibleFrontends.length > 0) {
- consola.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: string[] = [];
-
- 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.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";
- if (
- options.examples &&
- !options.examples.includes("none") &&
- options.examples.length > 0
- ) {
- consola.fatal(
- "Cannot select examples when backend is 'none'. Please remove the --examples flag or set --examples none.",
- );
- process.exit(1);
- }
- config.examples = [];
- } else {
- const effectiveDatabase =
- config.database ?? (options.yes ? DEFAULT_CONFIG.database : undefined);
- const effectiveOrm =
- config.orm ?? (options.yes ? DEFAULT_CONFIG.orm : undefined);
- const _effectiveAuth =
- config.auth ?? (options.yes ? DEFAULT_CONFIG.auth : undefined);
- const _effectiveDbSetup =
- config.dbSetup ?? (options.yes ? DEFAULT_CONFIG.dbSetup : undefined);
- const _effectiveExamples =
- config.examples ?? (options.yes ? DEFAULT_CONFIG.examples : undefined);
- const effectiveFrontend =
- config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined);
- const effectiveApi =
- config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined);
- const effectiveBackend =
- config.backend ?? (options.yes ? DEFAULT_CONFIG.backend : undefined);
-
- if (effectiveDatabase === "none") {
- if (providedFlags.has("orm") && options.orm !== "none") {
- consola.fatal(
- `Cannot use ORM '--orm ${options.orm}' when database is 'none'.`,
- );
- process.exit(1);
- }
- config.orm = "none";
- log.info(
- "Due to '--database none', '--orm' has been automatically set to 'none'.",
- );
-
- if (providedFlags.has("auth") && options.auth === true) {
- consola.fatal(
- "Authentication requires a database. Cannot use --auth when database is 'none'.",
- );
- process.exit(1);
- }
- config.auth = false;
- log.info(
- "Due to '--database none', '--auth' has been automatically set to 'false'.",
- );
-
- if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
- consola.fatal(
- `Database setup '--db-setup ${options.dbSetup}' requires a database. Cannot use when database is 'none'.`,
- );
- process.exit(1);
- }
- config.dbSetup = "none";
- log.info(
- "Due to '--database none', '--db-setup' has been automatically set to 'none'.",
- );
- }
-
- if (config.orm === "mongoose" && !providedFlags.has("database")) {
- if (effectiveDatabase && effectiveDatabase !== "mongodb") {
- consola.fatal(
- `Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
- config.database = "mongodb";
- }
-
- if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") {
- consola.fatal(
- "Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.",
- );
- process.exit(1);
- }
-
- if (
- effectiveOrm === "mongoose" &&
- effectiveDatabase &&
- effectiveDatabase !== "mongodb"
- ) {
- consola.fatal(
- `Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
-
- if (config.dbSetup && config.dbSetup !== "none") {
- const dbSetup = config.dbSetup;
-
- if (!effectiveDatabase || effectiveDatabase === "none") {
- consola.fatal(
- `Database setup '--db-setup ${dbSetup}' requires a database. Cannot use when database is 'none'.`,
- );
- process.exit(1);
- }
-
- if (dbSetup === "turso") {
- if (effectiveDatabase && effectiveDatabase !== "sqlite") {
- consola.fatal(
- `Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`,
- );
- process.exit(1);
- }
- if (effectiveOrm !== "drizzle") {
- consola.fatal(
- `Turso setup requires Drizzle ORM. Cannot use --db-setup turso with --orm ${
- effectiveOrm ?? "none"
- }.`,
- );
- process.exit(1);
- }
- } else if (dbSetup === "supabase") {
- if (effectiveDatabase !== "postgres") {
- consola.fatal(
- `Supabase setup requires PostgreSQL. Cannot use --db-setup supabase with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
- } else if (dbSetup === "prisma-postgres") {
- if (effectiveDatabase !== "postgres") {
- consola.fatal(
- `Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
- if (effectiveOrm !== "prisma") {
- consola.fatal(
- `Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`,
- );
- process.exit(1);
- }
- } else if (dbSetup === "mongodb-atlas") {
- if (effectiveDatabase !== "mongodb") {
- consola.fatal(
- `MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
- if (effectiveOrm !== "prisma" && effectiveOrm !== "mongoose") {
- consola.fatal(
- `MongoDB Atlas setup requires Prisma or Mongoose ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`,
- );
- process.exit(1);
- }
- } else if (dbSetup === "neon") {
- if (effectiveDatabase !== "postgres") {
- consola.fatal(
- `Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`,
- );
- process.exit(1);
- }
- }
- }
-
- const includesNuxt = effectiveFrontend?.includes("nuxt");
- const includesSvelte = effectiveFrontend?.includes("svelte");
- const includesSolid = effectiveFrontend?.includes("solid");
-
- if (
- (includesNuxt || includesSvelte || includesSolid) &&
- effectiveApi === "trpc"
- ) {
- consola.fatal(
- `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.`,
- );
- process.exit(1);
- }
-
- if (config.addons && config.addons.length > 0) {
- const webSpecificAddons = ["pwa", "tauri"];
- const hasWebSpecificAddons = config.addons.some((addon) =>
- webSpecificAddons.includes(addon),
- );
- const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
- const isPwaCompatible =
- f === "tanstack-router" ||
- f === "react-router" ||
- f === "solid" ||
- f === "next";
- const isTauriCompatible =
- f === "tanstack-router" ||
- f === "react-router" ||
- f === "nuxt" ||
- f === "svelte" ||
- f === "solid" ||
- f === "next";
-
- if (
- config.addons?.includes("pwa") &&
- config.addons?.includes("tauri")
- ) {
- return isPwaCompatible && isTauriCompatible;
- }
- if (config.addons?.includes("pwa")) {
- return isPwaCompatible;
- }
- if (config.addons?.includes("tauri")) {
- return isTauriCompatible;
- }
- return true;
- });
-
- if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
- let incompatibleReason = "Selected frontend is not compatible.";
- if (config.addons.includes("pwa")) {
- incompatibleReason =
- "PWA requires tanstack-router, react-router, next, or solid.";
- }
- if (config.addons.includes("tauri")) {
- incompatibleReason =
- "Tauri requires tanstack-router, react-router, nuxt, svelte, solid, or next.";
- }
- consola.fatal(
- `Incompatible addon/frontend combination: ${incompatibleReason}`,
- );
- process.exit(1);
- }
-
- if (config.addons.includes("husky") && !config.addons.includes("biome")) {
- consola.warn(
- "Husky addon is recommended to be used with Biome for lint-staged configuration.",
- );
- }
- config.addons = [...new Set(config.addons)];
- }
-
- const onlyNativeFrontend =
- effectiveFrontend &&
- effectiveFrontend.length === 1 &&
- (effectiveFrontend[0] === "native-nativewind" ||
- effectiveFrontend[0] === "native-unistyles");
-
- if (
- onlyNativeFrontend &&
- config.examples &&
- config.examples.length > 0 &&
- !config.examples.includes("none")
- ) {
- consola.fatal(
- "Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
- );
- process.exit(1);
- }
-
- if (
- config.examples &&
- config.examples.length > 0 &&
- !config.examples.includes("none")
- ) {
- if (
- config.examples.includes("todo") &&
- effectiveBackend !== "convex" &&
- effectiveBackend !== "none" &&
- effectiveDatabase === "none"
- ) {
- consola.fatal(
- "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.",
- );
- process.exit(1);
- }
-
- if (config.examples.includes("ai") && effectiveBackend === "elysia") {
- consola.fatal(
- "The 'ai' example is not compatible with the Elysia backend.",
- );
- process.exit(1);
- }
-
- if (config.examples.includes("ai") && includesSolid) {
- consola.fatal(
- "The 'ai' example is not compatible with the Solid frontend.",
- );
- process.exit(1);
- }
- }
- }
-
- return config;
-}
-
main().catch((err) => {
consola.error("Aborting installation due to unexpected error...");
if (err instanceof Error) {
diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts
index 4fe0abe..7e5c31b 100644
--- a/apps/cli/src/prompts/addons.ts
+++ b/apps/cli/src/prompts/addons.ts
@@ -1,18 +1,18 @@
import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectAddons, ProjectFrontend } from "../types";
+import type { Addons, Frontend } from "../types";
type AddonOption = {
- value: ProjectAddons;
+ value: Addons;
label: string;
hint: string;
};
export async function getAddonsChoice(
- addons?: ProjectAddons[],
- frontends?: ProjectFrontend[],
-): Promise {
+ addons?: Addons[],
+ frontends?: Frontend[],
+): Promise {
if (addons !== undefined) return addons;
const hasCompatiblePwaFrontend =
diff --git a/apps/cli/src/prompts/api.ts b/apps/cli/src/prompts/api.ts
index dd8277c..48d12b3 100644
--- a/apps/cli/src/prompts/api.ts
+++ b/apps/cli/src/prompts/api.ts
@@ -1,12 +1,12 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
-import type { ProjectApi, ProjectBackend, ProjectFrontend } from "../types";
+import type { API, Backend, Frontend } from "../types";
export async function getApiChoice(
- Api?: ProjectApi | undefined,
- frontend?: ProjectFrontend[],
- backend?: ProjectBackend,
-): Promise {
+ Api?: API | undefined,
+ frontend?: Frontend[],
+ backend?: Backend,
+): Promise {
if (backend === "convex" || backend === "none") {
return "none";
}
@@ -52,7 +52,7 @@ export async function getApiChoice(
];
}
- const apiType = await select({
+ const apiType = await select({
message: "Select API type",
options: apiOptions,
initialValue: apiOptions[0].value,
diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts
index b02fdda..166c1de 100644
--- a/apps/cli/src/prompts/auth.ts
+++ b/apps/cli/src/prompts/auth.ts
@@ -1,12 +1,12 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend } from "../types";
+import type { Backend } from "../types";
export async function getAuthChoice(
auth: boolean | undefined,
hasDatabase: boolean,
- backend?: ProjectBackend,
+ backend?: Backend,
): Promise {
if (backend === "convex") {
return false;
diff --git a/apps/cli/src/prompts/backend-framework.ts b/apps/cli/src/prompts/backend.ts
similarity index 78%
rename from apps/cli/src/prompts/backend-framework.ts
rename to apps/cli/src/prompts/backend.ts
index c6eba28..16f5514 100644
--- a/apps/cli/src/prompts/backend-framework.ts
+++ b/apps/cli/src/prompts/backend.ts
@@ -1,12 +1,12 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend, ProjectFrontend } from "../types";
+import type { Backend, Frontend } from "../types";
export async function getBackendFrameworkChoice(
- backendFramework?: ProjectBackend,
- frontends?: ProjectFrontend[],
-): Promise {
+ backendFramework?: Backend,
+ frontends?: Frontend[],
+): Promise {
if (backendFramework !== undefined) return backendFramework;
const hasIncompatibleFrontend = frontends?.some(
@@ -14,7 +14,7 @@ export async function getBackendFrameworkChoice(
);
const backendOptions: Array<{
- value: ProjectBackend;
+ value: Backend;
label: string;
hint: string;
}> = [
@@ -26,7 +26,7 @@ export async function getBackendFrameworkChoice(
{
value: "next" as const,
label: "Next.js",
- hint: "Full-stack framework with API routes",
+ hint: "Nextjs API routes",
},
{
value: "express" as const,
@@ -56,7 +56,7 @@ export async function getBackendFrameworkChoice(
backendOptions.push({
value: "none" as const,
label: "None",
- hint: "No backend server (e.g., for a static site or client-only app)",
+ hint: "No backend server",
});
let initialValue = DEFAULT_CONFIG.backend;
@@ -64,8 +64,8 @@ export async function getBackendFrameworkChoice(
initialValue = "hono";
}
- const response = await select({
- message: "Select backend framework",
+ const response = await select({
+ message: "Select backend",
options: backendOptions,
initialValue,
});
diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts
index d0182c7..4db4aad 100644
--- a/apps/cli/src/prompts/config-prompts.ts
+++ b/apps/cli/src/prompts/config-prompts.ts
@@ -1,26 +1,26 @@
import { cancel, group } from "@clack/prompts";
import pc from "picocolors";
import type {
- ProjectAddons,
- ProjectApi,
- ProjectBackend,
+ API,
+ Addons,
+ Backend,
+ Database,
+ DatabaseSetup,
+ Examples,
+ Frontend,
+ ORM,
+ PackageManager,
ProjectConfig,
- ProjectDBSetup,
- ProjectDatabase,
- ProjectExamples,
- ProjectFrontend,
- ProjectOrm,
- ProjectPackageManager,
- ProjectRuntime,
+ Runtime,
} from "../types";
import { getAddonsChoice } from "./addons";
import { getApiChoice } from "./api";
import { getAuthChoice } from "./auth";
-import { getBackendFrameworkChoice } from "./backend-framework";
+import { getBackendFrameworkChoice } from "./backend";
import { getDatabaseChoice } from "./database";
-import { getDBSetupChoice } from "./db-setup";
+import { getDBSetupChoice } from "./database-setup";
import { getExamplesChoice } from "./examples";
-import { getFrontendChoice } from "./frontend-option";
+import { getFrontendChoice } from "./frontend";
import { getGitChoice } from "./git";
import { getinstallChoice } from "./install";
import { getORMChoice } from "./orm";
@@ -28,18 +28,18 @@ import { getPackageManagerChoice } from "./package-manager";
import { getRuntimeChoice } from "./runtime";
type PromptGroupResults = {
- frontend: ProjectFrontend[];
- backend: ProjectBackend;
- runtime: ProjectRuntime;
- database: ProjectDatabase;
- orm: ProjectOrm;
- api: ProjectApi;
+ frontend: Frontend[];
+ backend: Backend;
+ runtime: Runtime;
+ database: Database;
+ orm: ORM;
+ api: API;
auth: boolean;
- addons: ProjectAddons[];
- examples: ProjectExamples[];
- dbSetup: ProjectDBSetup;
+ addons: Addons[];
+ examples: Examples[];
+ dbSetup: DatabaseSetup;
git: boolean;
- packageManager: ProjectPackageManager;
+ packageManager: PackageManager;
install: boolean;
};
diff --git a/apps/cli/src/prompts/db-setup.ts b/apps/cli/src/prompts/database-setup.ts
similarity index 81%
rename from apps/cli/src/prompts/db-setup.ts
rename to apps/cli/src/prompts/database-setup.ts
index e881d60..62de71a 100644
--- a/apps/cli/src/prompts/db-setup.ts
+++ b/apps/cli/src/prompts/database-setup.ts
@@ -1,18 +1,18 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
-import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types";
+import type { Backend, DatabaseSetup, ORM } from "../types";
export async function getDBSetupChoice(
databaseType: string,
- dbSetup: ProjectDBSetup | undefined,
- orm?: ProjectOrm,
- backend?: ProjectBackend,
-): Promise {
+ dbSetup: DatabaseSetup | undefined,
+ orm?: ORM,
+ backend?: Backend,
+): Promise {
if (backend === "convex") {
return "none";
}
- if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
+ if (dbSetup !== undefined) return dbSetup as DatabaseSetup;
if (databaseType === "none") {
return "none";
@@ -22,7 +22,7 @@ export async function getDBSetupChoice(
return "none";
}
- let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> =
+ let options: Array<{ value: DatabaseSetup; label: string; hint: string }> =
[];
if (databaseType === "sqlite") {
@@ -70,7 +70,7 @@ export async function getDBSetupChoice(
return "none";
}
- const response = await select({
+ const response = await select({
message: `Select ${databaseType} setup option`,
options,
initialValue: "none",
diff --git a/apps/cli/src/prompts/database.ts b/apps/cli/src/prompts/database.ts
index d3eed78..2ba1046 100644
--- a/apps/cli/src/prompts/database.ts
+++ b/apps/cli/src/prompts/database.ts
@@ -1,19 +1,19 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend, ProjectDatabase } from "../types";
+import type { Backend, Database } from "../types";
export async function getDatabaseChoice(
- database?: ProjectDatabase,
- backend?: ProjectBackend,
-): Promise {
+ database?: Database,
+ backend?: Backend,
+): Promise {
if (backend === "convex" || backend === "none") {
return "none";
}
if (database !== undefined) return database;
- const response = await select({
+ const response = await select({
message: "Select database",
options: [
{
diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts
index 01e6374..d562ea5 100644
--- a/apps/cli/src/prompts/examples.ts
+++ b/apps/cli/src/prompts/examples.ts
@@ -1,21 +1,15 @@
import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type {
- ProjectApi,
- ProjectBackend,
- ProjectDatabase,
- ProjectExamples,
- ProjectFrontend,
-} from "../types";
+import type { API, Backend, Database, Examples, Frontend } from "../types";
export async function getExamplesChoice(
- examples?: ProjectExamples[],
- database?: ProjectDatabase,
- frontends?: ProjectFrontend[],
- backend?: ProjectBackend,
- api?: ProjectApi,
-): Promise {
+ examples?: Examples[],
+ database?: Database,
+ frontends?: Frontend[],
+ backend?: Backend,
+ api?: API,
+): Promise {
if (api === "none") {
return [];
}
@@ -56,8 +50,8 @@ export async function getExamplesChoice(
if (!hasWebFrontend && !noFrontendSelected) return [];
- let response: ProjectExamples[] | symbol = [];
- const options: { value: ProjectExamples; label: string; hint: string }[] = [
+ let response: Examples[] | symbol = [];
+ const options: { value: Examples; label: string; hint: string }[] = [
{
value: "todo" as const,
label: "Todo App",
@@ -73,7 +67,7 @@ export async function getExamplesChoice(
});
}
- response = await multiselect({
+ response = await multiselect({
message: "Include examples",
options: options,
required: false,
diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend.ts
similarity index 88%
rename from apps/cli/src/prompts/frontend-option.ts
rename to apps/cli/src/prompts/frontend.ts
index 32ac209..4ae7dfe 100644
--- a/apps/cli/src/prompts/frontend-option.ts
+++ b/apps/cli/src/prompts/frontend.ts
@@ -1,12 +1,12 @@
import { cancel, isCancel, multiselect, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend, ProjectFrontend } from "../types";
+import type { Backend, Frontend } from "../types";
export async function getFrontendChoice(
- frontendOptions?: ProjectFrontend[],
- backend?: ProjectBackend,
-): Promise {
+ frontendOptions?: Frontend[],
+ backend?: Backend,
+): Promise {
if (frontendOptions !== undefined) return frontendOptions;
const frontendTypes = await multiselect({
@@ -32,7 +32,7 @@ export async function getFrontendChoice(
process.exit(0);
}
- const result: ProjectFrontend[] = [];
+ const result: Frontend[] = [];
if (frontendTypes.includes("web")) {
const allWebOptions = [
@@ -80,8 +80,8 @@ export async function getFrontendChoice(
return true;
});
- const webFramework = await select({
- message: "Choose frontend framework",
+ const webFramework = await select({
+ message: "Choose frontend",
options: webOptions,
initialValue: DEFAULT_CONFIG.frontend[0],
});
@@ -95,7 +95,7 @@ export async function getFrontendChoice(
}
if (frontendTypes.includes("native")) {
- const nativeFramework = await select({
+ const nativeFramework = await select({
message: "Choose native framework",
options: [
{
diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts
index 189012e..b5614f1 100644
--- a/apps/cli/src/prompts/orm.ts
+++ b/apps/cli/src/prompts/orm.ts
@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend, ProjectDatabase, ProjectOrm } from "../types";
+import type { Backend, Database, ORM } from "../types";
const ormOptions = {
prisma: {
@@ -22,11 +22,11 @@ const ormOptions = {
};
export async function getORMChoice(
- orm: ProjectOrm | undefined,
+ orm: ORM | undefined,
hasDatabase: boolean,
- database?: ProjectDatabase,
- backend?: ProjectBackend,
-): Promise {
+ database?: Database,
+ backend?: Backend,
+): Promise {
if (backend === "convex") {
return "none";
}
@@ -40,7 +40,7 @@ export async function getORMChoice(
: [ormOptions.drizzle, ormOptions.prisma]),
];
- const response = await select({
+ const response = await select({
message: "Select ORM",
options,
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
diff --git a/apps/cli/src/prompts/package-manager.ts b/apps/cli/src/prompts/package-manager.ts
index 52d3b99..25dcbb9 100644
--- a/apps/cli/src/prompts/package-manager.ts
+++ b/apps/cli/src/prompts/package-manager.ts
@@ -1,16 +1,16 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
-import type { ProjectPackageManager } from "../types";
+import type { PackageManager } from "../types";
import { getUserPkgManager } from "../utils/get-package-manager";
export async function getPackageManagerChoice(
- packageManager?: ProjectPackageManager,
-): Promise {
+ packageManager?: PackageManager,
+): Promise {
if (packageManager !== undefined) return packageManager;
const detectedPackageManager = getUserPkgManager();
- const response = await select({
+ const response = await select({
message: "Choose package manager",
options: [
{ value: "npm", label: "npm", hint: "Node Package Manager" },
diff --git a/apps/cli/src/prompts/runtime.ts b/apps/cli/src/prompts/runtime.ts
index 50dfe0e..4c691a2 100644
--- a/apps/cli/src/prompts/runtime.ts
+++ b/apps/cli/src/prompts/runtime.ts
@@ -1,12 +1,12 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
-import type { ProjectBackend, ProjectRuntime } from "../types";
+import type { Backend, Runtime } from "../types";
export async function getRuntimeChoice(
- runtime?: ProjectRuntime,
- backend?: ProjectBackend,
-): Promise {
+ runtime?: Runtime,
+ backend?: Backend,
+): Promise {
if (backend === "convex" || backend === "none") {
return "none";
}
@@ -17,7 +17,7 @@ export async function getRuntimeChoice(
return "node";
}
- const response = await select({
+ const response = await select({
message: "Select runtime",
options: [
{
diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts
index 574468d..687092f 100644
--- a/apps/cli/src/types.ts
+++ b/apps/cli/src/types.ts
@@ -1,12 +1,7 @@
-export type ProjectDatabase =
- | "sqlite"
- | "postgres"
- | "mongodb"
- | "mysql"
- | "none";
-export type ProjectOrm = "drizzle" | "prisma" | "mongoose" | "none";
-export type ProjectPackageManager = "npm" | "pnpm" | "bun";
-export type ProjectAddons =
+export type Database = "sqlite" | "postgres" | "mongodb" | "mysql" | "none";
+export type ORM = "drizzle" | "prisma" | "mongoose" | "none";
+export type PackageManager = "npm" | "pnpm" | "bun";
+export type Addons =
| "pwa"
| "biome"
| "tauri"
@@ -14,7 +9,7 @@ export type ProjectAddons =
| "starlight"
| "turborepo"
| "none";
-export type ProjectBackend =
+export type Backend =
| "hono"
| "express"
| "fastify"
@@ -22,9 +17,9 @@ export type ProjectBackend =
| "elysia"
| "convex"
| "none";
-export type ProjectRuntime = "node" | "bun" | "none";
-export type ProjectExamples = "todo" | "ai" | "none";
-export type ProjectFrontend =
+export type Runtime = "node" | "bun" | "none";
+export type Examples = "todo" | "ai" | "none";
+export type Frontend =
| "react-router"
| "tanstack-router"
| "tanstack-start"
@@ -35,51 +30,51 @@ export type ProjectFrontend =
| "svelte"
| "solid"
| "none";
-export type ProjectDBSetup =
+export type DatabaseSetup =
| "turso"
| "prisma-postgres"
| "mongodb-atlas"
| "neon"
| "supabase"
| "none";
-export type ProjectApi = "trpc" | "orpc" | "none";
+export type API = "trpc" | "orpc" | "none";
export interface ProjectConfig {
projectName: string;
projectDir: string;
relativePath: string;
- backend: ProjectBackend;
- runtime: ProjectRuntime;
- database: ProjectDatabase;
- orm: ProjectOrm;
+ backend: Backend;
+ runtime: Runtime;
+ database: Database;
+ orm: ORM;
auth: boolean;
- addons: ProjectAddons[];
- examples: ProjectExamples[];
+ addons: Addons[];
+ examples: Examples[];
git: boolean;
- packageManager: ProjectPackageManager;
+ packageManager: PackageManager;
install: boolean;
- dbSetup: ProjectDBSetup;
- frontend: ProjectFrontend[];
- api: ProjectApi;
+ dbSetup: DatabaseSetup;
+ frontend: Frontend[];
+ api: API;
}
export type YargsArgv = {
projectDirectory?: string;
yes?: boolean;
- database?: ProjectDatabase;
- orm?: ProjectOrm;
+ database?: Database;
+ orm?: ORM;
auth?: boolean;
- frontend?: ProjectFrontend[];
- addons?: ProjectAddons[];
- examples?: ProjectExamples[];
+ frontend?: Frontend[];
+ addons?: Addons[];
+ examples?: Examples[];
git?: boolean;
- packageManager?: ProjectPackageManager;
+ packageManager?: PackageManager;
install?: boolean;
- dbSetup?: ProjectDBSetup;
- backend?: ProjectBackend;
- runtime?: ProjectRuntime;
- api?: ProjectApi;
+ dbSetup?: DatabaseSetup;
+ backend?: Backend;
+ runtime?: Runtime;
+ api?: API;
_: (string | number)[];
$0: string;
diff --git a/apps/cli/src/utils/get-package-execution-command.ts b/apps/cli/src/utils/get-package-execution-command.ts
index 9501bfc..4898e50 100644
--- a/apps/cli/src/utils/get-package-execution-command.ts
+++ b/apps/cli/src/utils/get-package-execution-command.ts
@@ -1,4 +1,4 @@
-import type { ProjectPackageManager } from "../types";
+import type { PackageManager } from "../types";
/**
* Returns the appropriate command for running a package without installing it globally,
@@ -9,7 +9,7 @@ import type { ProjectPackageManager } from "../types";
* @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
*/
export function getPackageExecutionCommand(
- packageManager: ProjectPackageManager | null | undefined,
+ packageManager: PackageManager | null | undefined,
commandWithArgs: string,
): string {
switch (packageManager) {
diff --git a/apps/cli/src/utils/get-package-manager.ts b/apps/cli/src/utils/get-package-manager.ts
index 259271a..26f739a 100644
--- a/apps/cli/src/utils/get-package-manager.ts
+++ b/apps/cli/src/utils/get-package-manager.ts
@@ -1,6 +1,6 @@
-import type { ProjectPackageManager } from "../types";
+import type { PackageManager } from "../types";
-export const getUserPkgManager: () => ProjectPackageManager = () => {
+export const getUserPkgManager: () => PackageManager = () => {
const userAgent = process.env.npm_config_user_agent;
if (userAgent?.startsWith("pnpm")) {
diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts
new file mode 100644
index 0000000..d54d624
--- /dev/null
+++ b/apps/cli/src/validation.ts
@@ -0,0 +1,519 @@
+import path from "node:path";
+import { log } from "@clack/prompts";
+import { consola } from "consola";
+import type {
+ API,
+ Addons,
+ Backend,
+ Database,
+ DatabaseSetup,
+ Examples,
+ Frontend,
+ ORM,
+ PackageManager,
+ ProjectConfig,
+ Runtime,
+ YargsArgv,
+} from "./types";
+
+export function processAndValidateFlags(
+ options: YargsArgv,
+ projectName?: string,
+): Partial {
+ const config: Partial = {};
+ const providedFlags: Set = new Set(
+ Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
+ );
+
+ if (options.api) {
+ config.api = options.api as API;
+ if (options.api === "none") {
+ if (
+ options.examples &&
+ !(options.examples.length === 1 && options.examples[0] === "none")
+ ) {
+ consola.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 as Backend;
+ }
+
+ if (
+ providedFlags.has("backend") &&
+ config.backend &&
+ config.backend !== "convex" &&
+ config.backend !== "none"
+ ) {
+ if (providedFlags.has("runtime") && options.runtime === "none") {
+ consola.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 as Database;
+ }
+ if (options.orm) {
+ config.orm = options.orm as ORM;
+ }
+ if (options.auth !== undefined) {
+ config.auth = options.auth;
+ }
+ if (options.git !== undefined) {
+ config.git = options.git;
+ }
+ if (options.install !== undefined) {
+ config.install = options.install;
+ }
+ if (options.runtime) {
+ config.runtime = options.runtime as Runtime;
+ }
+ if (options.dbSetup) {
+ config.dbSetup = options.dbSetup as DatabaseSetup;
+ }
+ if (options.packageManager) {
+ config.packageManager = options.packageManager as PackageManager;
+ }
+
+ if (projectName) {
+ config.projectName = projectName;
+ } else if (options.projectDirectory) {
+ config.projectName = path.basename(
+ path.resolve(process.cwd(), options.projectDirectory),
+ );
+ }
+
+ if (options.frontend && options.frontend.length > 0) {
+ if (options.frontend.includes("none")) {
+ if (options.frontend.length > 1) {
+ consola.fatal(`Cannot combine 'none' with other frontend options.`);
+ process.exit(1);
+ }
+ config.frontend = [];
+ } else {
+ const validOptions = options.frontend.filter(
+ (f): f is Frontend => f !== "none",
+ );
+ const webFrontends = validOptions.filter(
+ (f) =>
+ f === "tanstack-router" ||
+ f === "react-router" ||
+ f === "tanstack-start" ||
+ f === "next" ||
+ f === "nuxt" ||
+ f === "svelte" ||
+ f === "solid",
+ );
+ const nativeFrontends = validOptions.filter(
+ (f) => f === "native-nativewind" || f === "native-unistyles",
+ );
+
+ if (webFrontends.length > 1) {
+ consola.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.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.fatal(`Cannot combine 'none' with other addons.`);
+ process.exit(1);
+ }
+ config.addons = [];
+ } else {
+ config.addons = options.addons.filter(
+ (addon): addon is Addons => addon !== "none",
+ );
+ }
+ }
+ if (options.examples && options.examples.length > 0) {
+ if (options.examples.includes("none")) {
+ if (options.examples.length > 1) {
+ consola.fatal("Cannot combine 'none' with other examples.");
+ process.exit(1);
+ }
+ config.examples = [];
+ } else {
+ config.examples = options.examples.filter(
+ (ex): ex is Examples => ex !== "none",
+ );
+ if (options.examples.includes("none") && config.backend !== "convex") {
+ config.examples = [];
+ }
+ }
+ }
+
+ if (config.backend === "convex") {
+ const incompatibleFlags: string[] = [];
+
+ 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.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 === "nuxt" || f === "solid",
+ );
+ if (incompatibleFrontends.length > 0) {
+ consola.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: string[] = [];
+
+ 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.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 = [];
+ } else {
+ if (config.database === "none") {
+ if (providedFlags.has("orm") && options.orm !== "none") {
+ consola.fatal(
+ `'--orm ${options.orm}' is incompatible with '--database none'. Please use '--orm none' or choose a database.`,
+ );
+ process.exit(1);
+ }
+ if (providedFlags.has("auth") && options.auth === true) {
+ consola.fatal(
+ `'--auth' requires a database. Cannot use '--auth' with '--database none'.`,
+ );
+ process.exit(1);
+ }
+ if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
+ consola.fatal(
+ `'--db-setup ${options.dbSetup}' requires a database. Cannot use with '--database none'.`,
+ );
+ process.exit(1);
+ }
+
+ config.orm = "none";
+ config.auth = false;
+ config.dbSetup = "none";
+
+ log.info(
+ "Due to '--database none', '--orm' has been automatically set to 'none'.",
+ );
+ log.info(
+ "Due to '--database none', '--auth' has been automatically set to 'false'.",
+ );
+ log.info(
+ "Due to '--database none', '--db-setup' has been automatically set to 'none'.",
+ );
+ }
+
+ if (config.orm === "mongoose") {
+ if (!providedFlags.has("database")) {
+ config.database = "mongodb";
+ log.info(
+ "Due to '--orm mongoose', '--database' has been automatically set to 'mongodb'.",
+ );
+ } else if (config.database !== "mongodb") {
+ consola.fatal(
+ `'--orm mongoose' requires '--database mongodb'. Cannot use '--orm mongoose' with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ }
+
+ if (config.dbSetup) {
+ if (config.dbSetup === "turso") {
+ if (!providedFlags.has("database")) {
+ config.database = "sqlite";
+ log.info(
+ "Due to '--db-setup turso', '--database' has been automatically set to 'sqlite'.",
+ );
+ } else if (config.database !== "sqlite") {
+ consola.fatal(
+ `'--db-setup turso' requires '--database sqlite'. Cannot use with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ if (!providedFlags.has("orm")) {
+ config.orm = "drizzle";
+ log.info(
+ "Due to '--db-setup turso', '--orm' has been automatically set to 'drizzle'.",
+ );
+ } else if (config.orm !== "drizzle") {
+ consola.fatal(
+ `'--db-setup turso' requires '--orm drizzle'. Cannot use with '--orm ${config.orm}'.`,
+ );
+ process.exit(1);
+ }
+ } else if (config.dbSetup === "prisma-postgres") {
+ if (!providedFlags.has("database")) {
+ config.database = "postgres";
+ log.info(
+ "Due to '--db-setup prisma-postgres', '--database' has been automatically set to 'postgres'.",
+ );
+ } else if (config.database !== "postgres") {
+ consola.fatal(
+ `'--db-setup prisma-postgres' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ if (!providedFlags.has("orm")) {
+ config.orm = "prisma";
+ log.info(
+ "Due to '--db-setup prisma-postgres', '--orm' has been automatically set to 'prisma'.",
+ );
+ } else if (config.orm !== "prisma") {
+ consola.fatal(
+ `'--db-setup prisma-postgres' requires '--orm prisma'. Cannot use with '--orm ${config.orm}'.`,
+ );
+ process.exit(1);
+ }
+ } else if (config.dbSetup === "supabase") {
+ if (!providedFlags.has("database")) {
+ config.database = "postgres";
+ log.info(
+ "Due to '--db-setup supabase', '--database' has been automatically set to 'postgres'.",
+ );
+ } else if (config.database !== "postgres") {
+ consola.fatal(
+ `'--db-setup supabase' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ } else if (config.dbSetup === "neon") {
+ if (!providedFlags.has("database")) {
+ config.database = "postgres";
+ log.info(
+ "Due to '--db-setup neon', '--database' has been automatically set to 'postgres'.",
+ );
+ } else if (config.database !== "postgres") {
+ consola.fatal(
+ `'--db-setup neon' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ } else if (config.dbSetup === "mongodb-atlas") {
+ if (!providedFlags.has("database")) {
+ config.database = "mongodb";
+ log.info(
+ "Due to '--db-setup mongodb-atlas', '--database' has been automatically set to 'mongodb'.",
+ );
+ } else if (config.database !== "mongodb") {
+ consola.fatal(
+ `'--db-setup mongodb-atlas' requires '--database mongodb'. Cannot use with '--database ${config.database}'.`,
+ );
+ process.exit(1);
+ }
+ }
+ }
+
+ if (config.database === "mongodb" && config.orm === "drizzle") {
+ consola.fatal(
+ `'--database mongodb' is incompatible with '--orm drizzle'. Use '--orm mongoose' or '--orm prisma' with MongoDB.`,
+ );
+ process.exit(1);
+ }
+ }
+
+ return config;
+}
+
+export function validateConfigCompatibility(
+ config: Partial,
+): void {
+ const effectiveDatabase = config.database;
+ const effectiveBackend = config.backend;
+ const effectiveFrontend = config.frontend;
+ const effectiveApi = config.api;
+
+ const includesNuxt = effectiveFrontend?.includes("nuxt");
+ const includesSvelte = effectiveFrontend?.includes("svelte");
+ const includesSolid = effectiveFrontend?.includes("solid");
+
+ if (
+ (includesNuxt || includesSvelte || includesSolid) &&
+ effectiveApi === "trpc"
+ ) {
+ consola.fatal(
+ `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.`,
+ );
+ process.exit(1);
+ }
+
+ if (config.addons && config.addons.length > 0) {
+ const webSpecificAddons = ["pwa", "tauri"];
+ const hasWebSpecificAddons = config.addons.some((addon) =>
+ webSpecificAddons.includes(addon),
+ );
+ const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
+ const isPwaCompatible =
+ f === "tanstack-router" ||
+ f === "react-router" ||
+ f === "solid" ||
+ f === "next";
+ const isTauriCompatible =
+ f === "tanstack-router" ||
+ f === "react-router" ||
+ f === "nuxt" ||
+ f === "svelte" ||
+ f === "solid" ||
+ f === "next";
+
+ if (config.addons?.includes("pwa") && config.addons?.includes("tauri")) {
+ return isPwaCompatible && isTauriCompatible;
+ }
+ if (config.addons?.includes("pwa")) {
+ return isPwaCompatible;
+ }
+ if (config.addons?.includes("tauri")) {
+ return isTauriCompatible;
+ }
+ return true;
+ });
+
+ if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
+ let incompatibleReason = "Selected frontend is not compatible.";
+ if (config.addons.includes("pwa")) {
+ incompatibleReason =
+ "PWA requires tanstack-router, react-router, next, or solid.";
+ }
+ if (config.addons.includes("tauri")) {
+ incompatibleReason =
+ "Tauri requires tanstack-router, react-router, nuxt, svelte, solid, or next.";
+ }
+ consola.fatal(
+ `Incompatible addon/frontend combination: ${incompatibleReason}`,
+ );
+ process.exit(1);
+ }
+
+ if (config.addons.includes("husky") && !config.addons.includes("biome")) {
+ consola.warn(
+ "Husky addon is recommended to be used with Biome for lint-staged configuration.",
+ );
+ }
+ config.addons = [...new Set(config.addons)];
+ }
+
+ const onlyNativeFrontend =
+ effectiveFrontend &&
+ effectiveFrontend.length === 1 &&
+ (effectiveFrontend[0] === "native-nativewind" ||
+ effectiveFrontend[0] === "native-unistyles");
+
+ if (
+ onlyNativeFrontend &&
+ config.examples &&
+ config.examples.length > 0 &&
+ !config.examples.includes("none")
+ ) {
+ consola.fatal(
+ "Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
+ );
+ process.exit(1);
+ }
+
+ if (
+ config.examples &&
+ config.examples.length > 0 &&
+ !config.examples.includes("none")
+ ) {
+ if (
+ config.examples.includes("todo") &&
+ effectiveBackend !== "convex" &&
+ effectiveBackend !== "none" &&
+ effectiveDatabase === "none"
+ ) {
+ consola.fatal(
+ "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.",
+ );
+ process.exit(1);
+ }
+
+ if (config.examples.includes("ai") && effectiveBackend === "elysia") {
+ consola.fatal(
+ "The 'ai' example is not compatible with the Elysia backend.",
+ );
+ process.exit(1);
+ }
+
+ if (config.examples.includes("ai") && includesSolid) {
+ consola.fatal(
+ "The 'ai' example is not compatible with the Solid frontend.",
+ );
+ process.exit(1);
+ }
+ }
+}