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); + } + } +}