From 357dfbbbf9171440afa947e5a07a85ef2bf44b8c Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 5 May 2025 18:54:23 +0530 Subject: [PATCH] Prompt to overwrite non-empty dirs before config --- .changeset/fluffy-bears-pick.md | 5 + apps/cli/src/constants.ts | 2 + apps/cli/src/helpers/addons-setup.ts | 3 +- apps/cli/src/helpers/api-setup.ts | 4 +- apps/cli/src/helpers/auth-setup.ts | 4 +- .../src/helpers/backend-framework-setup.ts | 3 +- apps/cli/src/helpers/create-project.ts | 5 +- apps/cli/src/helpers/db-setup.ts | 4 +- apps/cli/src/helpers/env-setup.ts | 2 +- apps/cli/src/helpers/examples-setup.ts | 4 +- apps/cli/src/helpers/mongodb-atlas-setup.ts | 3 +- apps/cli/src/helpers/neon-setup.ts | 3 +- apps/cli/src/helpers/post-installation.ts | 5 +- apps/cli/src/helpers/prisma-postgres-setup.ts | 3 +- apps/cli/src/helpers/runtime-setup.ts | 3 +- apps/cli/src/helpers/starlight-setup.ts | 3 +- apps/cli/src/helpers/tauri-setup.ts | 3 +- apps/cli/src/helpers/turso-setup.ts | 3 +- apps/cli/src/index.ts | 211 +++++++++++++----- apps/cli/src/prompts/config-prompts.ts | 12 +- apps/cli/src/prompts/project-name.ts | 55 ++--- apps/cli/src/types.ts | 2 + .../utils/generate-reproducible-command.ts | 4 +- .../auth/server/base/src/lib/auth.ts.hbs | 2 +- .../web/solid/src/components/sign-in-form.tsx | 6 +- .../backend/server/express/src/index.ts.hbs | 4 +- .../app/(home)/_components/Testimonials.tsx | 2 +- apps/web/src/components/theme-toggle.tsx | 2 +- bun.lock | 2 +- 29 files changed, 223 insertions(+), 141 deletions(-) create mode 100644 .changeset/fluffy-bears-pick.md diff --git a/.changeset/fluffy-bears-pick.md b/.changeset/fluffy-bears-pick.md new file mode 100644 index 0000000..7b970c3 --- /dev/null +++ b/.changeset/fluffy-bears-pick.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +Prompt to overwrite non-empty dirs before config diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index c55f001..0909ce6 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -9,6 +9,8 @@ export const PKG_ROOT = path.join(distPath, "../"); export const DEFAULT_CONFIG: ProjectConfig = { projectName: "my-better-t-app", + projectDir: path.resolve(process.cwd(), "my-better-t-app"), + relativePath: "my-better-t-app", frontend: ["tanstack-router"], database: "sqlite", orm: "drizzle", diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 3cd4b46..4205003 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -8,8 +8,7 @@ import { setupTauri } from "./tauri-setup"; import type { ProjectConfig } from "../types"; export async function setupAddons(config: ProjectConfig) { - const { projectName, addons, frontend } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, addons, frontend, projectDir } = config; const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router"); const hasNuxtFrontend = frontend.includes("nuxt"); diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/api-setup.ts index 212f8a2..15791e1 100644 --- a/apps/cli/src/helpers/api-setup.ts +++ b/apps/cli/src/helpers/api-setup.ts @@ -5,8 +5,8 @@ import type { ProjectConfig, ProjectFrontend } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; export async function setupApi(config: ProjectConfig): Promise { - const { api, projectName, frontend, backend, packageManager } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { api, projectName, frontend, backend, packageManager, projectDir } = + config; const isConvex = backend === "convex"; const webDir = path.join(projectDir, "apps/web"); const nativeDir = path.join(projectDir, "apps/native"); diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index db2edb0..cccecdc 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -6,13 +6,11 @@ import type { ProjectConfig } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; export async function setupAuth(config: ProjectConfig): Promise { - const { projectName, auth, frontend, backend } = config; - + const { projectName, auth, frontend, backend, projectDir } = config; if (backend === "convex" || !auth) { return; } - const projectDir = path.resolve(process.cwd(), projectName); const serverDir = path.join(projectDir, "apps/server"); const clientDir = path.join(projectDir, "apps/web"); const nativeDir = path.join(projectDir, "apps/native"); diff --git a/apps/cli/src/helpers/backend-framework-setup.ts b/apps/cli/src/helpers/backend-framework-setup.ts index 6bdf33f..421a178 100644 --- a/apps/cli/src/helpers/backend-framework-setup.ts +++ b/apps/cli/src/helpers/backend-framework-setup.ts @@ -7,13 +7,12 @@ import type { ProjectConfig } from "../types"; export async function setupBackendDependencies( config: ProjectConfig, ): Promise { - const { projectName, backend, runtime, api } = config; + const { projectName, backend, runtime, api, projectDir } = config; if (backend === "convex") { return; } - const projectDir = path.resolve(process.cwd(), projectName); const framework = backend; const serverDir = path.join(projectDir, "apps/server"); diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index 5c62516..db55021 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -1,5 +1,4 @@ -import path from "node:path"; -import { cancel, log, spinner } from "@clack/prompts"; +import { cancel, log } from "@clack/prompts"; import fs from "fs-extra"; import pc from "picocolors"; import type { ProjectConfig } from "../types"; @@ -27,7 +26,7 @@ import { } from "./template-manager"; export async function createProject(options: ProjectConfig) { - const projectDir = path.resolve(process.cwd(), options.projectName); + const projectDir = options.projectDir; const isConvex = options.backend === "convex"; try { diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index e4d6288..4bb055d 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -13,11 +13,10 @@ import { setupNeonPostgres } from "./neon-setup"; import type { ProjectConfig } from "../types"; export async function setupDatabase(config: ProjectConfig): Promise { - const { projectName, database, orm, dbSetup, backend } = config; + const { projectName, database, orm, dbSetup, backend, projectDir } = config; if (backend === "convex" || database === "none") { if (backend !== "convex") { - const projectDir = path.resolve(process.cwd(), projectName); const serverDir = path.join(projectDir, "apps/server"); const serverDbDir = path.join(serverDir, "src/db"); if (await fs.pathExists(serverDbDir)) { @@ -27,7 +26,6 @@ export async function setupDatabase(config: ProjectConfig): Promise { return; } - const projectDir = path.resolve(process.cwd(), projectName); const s = spinner(); const serverDir = path.join(projectDir, "apps/server"); diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 3d50f81..2e09569 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -65,8 +65,8 @@ export async function setupEnvironmentVariables( auth, examples, dbSetup, + projectDir, } = config; - const projectDir = path.resolve(process.cwd(), projectName); const hasReactRouter = frontend.includes("react-router"); const hasTanStackRouter = frontend.includes("tanstack-router"); diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index f22f91a..1c39076 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -5,7 +5,7 @@ import type { ProjectConfig } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; export async function setupExamples(config: ProjectConfig): Promise { - const { projectName, examples, frontend, backend } = config; + const { projectName, examples, frontend, backend, projectDir } = config; if ( backend === "convex" || @@ -16,8 +16,6 @@ export async function setupExamples(config: ProjectConfig): Promise { return; } - const projectDir = path.resolve(process.cwd(), projectName); - if (examples.includes("ai")) { const clientDir = path.join(projectDir, "apps/web"); const serverDir = path.join(projectDir, "apps/server"); diff --git a/apps/cli/src/helpers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/mongodb-atlas-setup.ts index 11da7f8..bb228e0 100644 --- a/apps/cli/src/helpers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/mongodb-atlas-setup.ts @@ -130,8 +130,7 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")} } export async function setupMongoDBAtlas(config: ProjectConfig) { - const { projectName } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, projectDir } = config; const mainSpinner = spinner(); mainSpinner.start("Setting up MongoDB Atlas"); diff --git a/apps/cli/src/helpers/neon-setup.ts b/apps/cli/src/helpers/neon-setup.ts index d6f5fa2..b7425e6 100644 --- a/apps/cli/src/helpers/neon-setup.ts +++ b/apps/cli/src/helpers/neon-setup.ts @@ -128,8 +128,7 @@ DATABASE_URL="your_connection_string"`); import type { ProjectConfig } from "../types"; export async function setupNeonPostgres(config: ProjectConfig): Promise { - const { projectName, packageManager } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, packageManager, projectDir } = config; const setupSpinner = spinner(); setupSpinner.start("Setting up Neon PostgreSQL"); diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 3c0aff9..1ef36bb 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -16,6 +16,7 @@ export function displayPostInstallInstructions( const { database, projectName, + relativePath, packageManager, depsInstalled, orm, @@ -27,7 +28,7 @@ export function displayPostInstallInstructions( const isConvex = backend === "convex"; const runCmd = packageManager === "npm" ? "npm run" : packageManager; - const cdCmd = `cd ${projectName}`; + const cdCmd = `cd ${relativePath}`; const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome"); @@ -76,7 +77,7 @@ export function displayPostInstallInstructions( !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : ""; const hasReactRouter = frontend?.includes("react-router"); - const hasSvelte = frontend?.includes("svelte"); // Keep separate for port logic + const hasSvelte = frontend?.includes("svelte"); const webPort = hasReactRouter || hasSvelte ? "5173" : "3001"; const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r"); diff --git a/apps/cli/src/helpers/prisma-postgres-setup.ts b/apps/cli/src/helpers/prisma-postgres-setup.ts index 9cefccd..b5858e3 100644 --- a/apps/cli/src/helpers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/prisma-postgres-setup.ts @@ -154,8 +154,7 @@ export default prisma; import type { ProjectConfig } from "../types"; export async function setupPrismaPostgres(config: ProjectConfig) { - const { projectName, packageManager } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, packageManager, projectDir } = config; const serverDir = path.join(projectDir, "apps/server"); const s = spinner(); s.start("Setting up Prisma PostgreSQL"); diff --git a/apps/cli/src/helpers/runtime-setup.ts b/apps/cli/src/helpers/runtime-setup.ts index 5aa1c40..7ef1846 100644 --- a/apps/cli/src/helpers/runtime-setup.ts +++ b/apps/cli/src/helpers/runtime-setup.ts @@ -4,13 +4,12 @@ import type { ProjectBackend, ProjectConfig } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; export async function setupRuntime(config: ProjectConfig): Promise { - const { projectName, runtime, backend } = config; + const { projectName, runtime, backend, projectDir } = config; if (backend === "convex" || backend === "next" || runtime === "none") { return; } - const projectDir = path.resolve(process.cwd(), projectName); const serverDir = path.join(projectDir, "apps/server"); if (!(await fs.pathExists(serverDir))) { diff --git a/apps/cli/src/helpers/starlight-setup.ts b/apps/cli/src/helpers/starlight-setup.ts index 8044396..f9f5bfc 100644 --- a/apps/cli/src/helpers/starlight-setup.ts +++ b/apps/cli/src/helpers/starlight-setup.ts @@ -7,8 +7,7 @@ import type { ProjectConfig } from "../types"; import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; export async function setupStarlight(config: ProjectConfig): Promise { - const { projectName, packageManager } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, packageManager, projectDir } = config; const s = spinner(); try { diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index 93eaecb..edcb3ba 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -10,8 +10,7 @@ import { getPackageExecutionCommand } from "../utils/get-package-execution-comma import type { ProjectConfig } from "../types"; export async function setupTauri(config: ProjectConfig): Promise { - const { projectName, packageManager, frontend } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, packageManager, frontend, projectDir } = config; const s = spinner(); const clientPackageDir = path.join(projectDir, "apps/web"); diff --git a/apps/cli/src/helpers/turso-setup.ts b/apps/cli/src/helpers/turso-setup.ts index 708d456..4be3890 100644 --- a/apps/cli/src/helpers/turso-setup.ts +++ b/apps/cli/src/helpers/turso-setup.ts @@ -198,8 +198,7 @@ DATABASE_AUTH_TOKEN=your_auth_token`); import type { ProjectConfig } from "../types"; export async function setupTurso(config: ProjectConfig): Promise { - const { projectName, orm } = config; - const projectDir = path.resolve(process.cwd(), projectName); + const { projectName, orm, projectDir } = config; const isDrizzle = orm === "drizzle"; const setupSpinner = spinner(); setupSpinner.start("Setting up Turso database"); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index b687261..e810977 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,5 +1,13 @@ import path from "node:path"; -import { cancel, intro, log, outro } from "@clack/prompts"; +import { + cancel, + confirm, + intro, + isCancel, + log, + outro, + spinner, +} from "@clack/prompts"; import { consola } from "consola"; import fs from "fs-extra"; import pc from "picocolors"; @@ -147,17 +155,96 @@ async function main() { .parse(); const options = argv as YargsArgv; - const projectDirectory = options.projectDirectory; + const cliProjectNameArg = options.projectDirectory; renderTitle(); - - const flagConfig = processAndValidateFlags(options, projectDirectory); - intro(pc.magenta("Creating a new Better-T-Stack project")); - if (!options.yes && Object.keys(flagConfig).length > 0) { + let currentPathInput: string; + let finalPathInput: string; + let finalResolvedPath: string; + let finalBaseName: string; + let shouldClearDirectory = false; + + if (options.yes && cliProjectNameArg) { + currentPathInput = cliProjectNameArg; + } else if (options.yes) { + let defaultName = DEFAULT_CONFIG.relativePath; + let counter = 1; + while ( + fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && + fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0 + ) { + defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`; + counter++; + } + currentPathInput = defaultName; + } else { + currentPathInput = await getProjectName(cliProjectNameArg); + } + + while (true) { + const resolvedPath = path.resolve(process.cwd(), currentPathInput); + const dirExists = fs.pathExistsSync(resolvedPath); + const dirIsNotEmpty = + dirExists && fs.readdirSync(resolvedPath).length > 0; + + if (!dirIsNotEmpty) { + finalPathInput = currentPathInput; + shouldClearDirectory = false; + break; + } + + const shouldOverwrite = await confirm({ + message: `Directory "${pc.yellow( + currentPathInput, + )}" already exists and is not empty. Overwrite and replace all existing files?`, + initialValue: false, + }); + + if (isCancel(shouldOverwrite)) { + cancel(pc.red("Operation cancelled.")); + process.exit(0); + } + + if (shouldOverwrite) { + finalPathInput = currentPathInput; + shouldClearDirectory = true; + break; + } + + log.info("Please choose a different project name or path."); + currentPathInput = await getProjectName(undefined); + } + + if (finalPathInput === ".") { + finalResolvedPath = process.cwd(); + finalBaseName = path.basename(finalResolvedPath); + } else { + finalResolvedPath = path.resolve(process.cwd(), finalPathInput); + finalBaseName = path.basename(finalResolvedPath); + } + + if (shouldClearDirectory) { + const s = spinner(); + s.start(`Clearing directory "${finalResolvedPath}"...`); + try { + await fs.emptyDir(finalResolvedPath); + s.stop(`Directory "${finalResolvedPath}" cleared.`); + } catch (error) { + s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`)); + consola.error(error); + process.exit(1); + } + } + + const flagConfig = processAndValidateFlags(options, finalBaseName); + + const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig; + + if (!options.yes && Object.keys(otherFlags).length > 0) { log.info(pc.yellow("Using these pre-selected options:")); - log.message(displayConfig(flagConfig)); + log.message(displayConfig(otherFlags)); log.message(""); } @@ -165,8 +252,10 @@ async function main() { if (options.yes) { config = { ...DEFAULT_CONFIG, - projectName: projectDirectory ?? DEFAULT_CONFIG.projectName, ...flagConfig, + projectName: finalBaseName, + projectDir: finalResolvedPath, + relativePath: finalPathInput, }; if (config.backend === "convex") { @@ -176,27 +265,25 @@ async function main() { config.api = "none"; config.runtime = "none"; config.dbSetup = "none"; + config.examples = ["todo"]; } else if (config.database === "none") { config.orm = "none"; config.auth = false; config.dbSetup = "none"; } - log.info(pc.yellow("Using these default/flag options:")); + log.info( + pc.yellow("Using default/flag options (config prompts skipped):"), + ); log.message(displayConfig(config)); log.message(""); } else { - config = await gatherConfig(flagConfig); - } - - const projectDir = path.resolve(process.cwd(), config.projectName); - - if ( - fs.pathExistsSync(projectDir) && - fs.readdirSync(projectDir).length > 0 - ) { - const newProjectName = await getProjectName(); - config.projectName = newProjectName; + config = await gatherConfig( + flagConfig, + finalBaseName, + finalResolvedPath, + finalPathInput, + ); } await createProject(config); @@ -238,7 +325,7 @@ async function main() { function processAndValidateFlags( options: YargsArgv, - projectDirectory?: string, + projectName?: string, ): Partial { const config: Partial = {}; const providedFlags: Set = new Set( @@ -305,8 +392,13 @@ function processAndValidateFlags( if (options.packageManager) { config.packageManager = options.packageManager as ProjectPackageManager; } - if (projectDirectory) { - config.projectName = projectDirectory; + + 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) { @@ -363,7 +455,7 @@ function processAndValidateFlags( config.examples = options.examples.filter( (ex): ex is ProjectExamples => ex !== "none", ); - if (config.backend !== "convex" && options.examples.includes("none")) { + if (options.examples.includes("none") && config.backend !== "convex") { config.examples = []; } } @@ -384,15 +476,12 @@ function processAndValidateFlags( incompatibleFlags.push(`--runtime ${options.runtime}`); if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`); - if (providedFlags.has("examples")) { - incompatibleFlags.push("--examples"); - } if (incompatibleFlags.length > 0) { consola.fatal( `The following flags are incompatible with '--backend convex': ${incompatibleFlags.join( ", ", - )}. Please remove them. The 'todo' example is included automatically with Convex.`, + )}. Please remove them.`, ); process.exit(1); } @@ -463,6 +552,12 @@ function processAndValidateFlags( } 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"; } @@ -564,7 +659,7 @@ function processAndValidateFlags( if ( (includesNuxt || includesSvelte || includesSolid) && effectiveApi !== "orpc" && - (!options.api || (options.yes && options.api !== "trpc")) + (!options.api || (options.yes && options.api === "trpc")) ) { if (config.api !== "none") { config.api = "orpc"; @@ -576,32 +671,43 @@ function processAndValidateFlags( const hasWebSpecificAddons = config.addons.some((addon) => webSpecificAddons.includes(addon), ); - const hasCompatibleWebFrontend = effectiveFrontend?.some( - (f) => + const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => { + const isPwaCompatible = + f === "tanstack-router" || f === "react-router" || f === "solid"; + const isTauriCompatible = f === "tanstack-router" || f === "react-router" || - f === "solid" || - (f === "nuxt" && - config.addons?.includes("tauri") && - !config.addons?.includes("pwa")) || - (f === "svelte" && - config.addons?.includes("tauri") && - !config.addons?.includes("pwa")), - ); + f === "nuxt" || + f === "svelte" || + f === "solid"; + + 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 incompatibleAddon = ""; - if (config.addons.includes("pwa") && includesNuxt) { - incompatibleAddon = "PWA addon is not compatible with Nuxt."; - } else if ( - config.addons.includes("pwa") || - config.addons.includes("tauri") - ) { - incompatibleAddon = - "PWA requires tanstack-router/react-router/solid. Tauri requires tanstack-router/react-router/Nuxt/Svelte/Solid."; + let incompatibleReason = "Selected frontend is not compatible."; + if (config.addons.includes("pwa")) { + incompatibleReason = + "PWA requires tanstack-router, react-router, or solid."; + } + if (config.addons.includes("tauri")) { + incompatibleReason = + "Tauri requires tanstack-router, react-router, nuxt, svelte, or solid."; } consola.fatal( - `${incompatibleAddon} Cannot use these addons with your frontend selection.`, + `Incompatible addon/frontend combination: ${incompatibleReason}`, ); process.exit(1); } @@ -671,7 +777,12 @@ main().catch((err) => { if (err instanceof Error) { if ( !err.message.includes("is only supported with") && - !err.message.includes("incompatible with") + !err.message.includes("incompatible with") && + !err.message.includes("requires") && + !err.message.includes("Cannot use") && + !err.message.includes("Cannot select multiple") && + !err.message.includes("Cannot combine") && + !err.message.includes("not supported") ) { consola.error(err.message); consola.error(err.stack); diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index cbbcbbd..df98904 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -25,11 +25,9 @@ import { getGitChoice } from "./git"; import { getinstallChoice } from "./install"; import { getORMChoice } from "./orm"; import { getPackageManagerChoice } from "./package-manager"; -import { getProjectName } from "./project-name"; import { getRuntimeChoice } from "./runtime"; type PromptGroupResults = { - projectName: string; frontend: ProjectFrontend[]; backend: ProjectBackend; runtime: ProjectRuntime; @@ -47,12 +45,12 @@ type PromptGroupResults = { export async function gatherConfig( flags: Partial, + projectName: string, + projectDir: string, + relativePath: string, ): Promise { const result = await group( { - projectName: async () => { - return getProjectName(flags.projectName); - }, frontend: ({ results }) => getFrontendChoice(flags.frontend, flags.backend), backend: ({ results }) => @@ -109,7 +107,9 @@ export async function gatherConfig( } return { - projectName: result.projectName, + projectName: projectName, + projectDir: projectDir, + relativePath: relativePath, frontend: result.frontend, backend: result.backend, runtime: result.runtime, diff --git a/apps/cli/src/prompts/project-name.ts b/apps/cli/src/prompts/project-name.ts index 0058ca6..aefe204 100644 --- a/apps/cli/src/prompts/project-name.ts +++ b/apps/cli/src/prompts/project-name.ts @@ -8,7 +8,6 @@ const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; const MAX_LENGTH = 255; function validateDirectoryName(name: string): string | undefined { - // Allow "." as it represents current directory if (name === ".") return undefined; if (!name) return "Project name cannot be empty"; @@ -30,22 +29,12 @@ function validateDirectoryName(name: string): string | undefined { export async function getProjectName(initialName?: string): Promise { if (initialName) { if (initialName === ".") { - const projectDir = process.cwd(); - if (fs.readdirSync(projectDir).length === 0) { - return initialName; - } - } else { - const finalDirName = path.basename(initialName); - const validationError = validateDirectoryName(finalDirName); - if (!validationError) { - const projectDir = path.resolve(process.cwd(), initialName); - if ( - !fs.pathExistsSync(projectDir) || - fs.readdirSync(projectDir).length === 0 - ) { - return initialName; - } - } + return initialName; + } + const finalDirName = path.basename(initialName); + const validationError = validateDirectoryName(finalDirName); + if (!validationError) { + return initialName; } } @@ -54,7 +43,10 @@ export async function getProjectName(initialName?: string): Promise { let defaultName = DEFAULT_CONFIG.projectName; let counter = 1; - while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName))) { + while ( + fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && + fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0 + ) { defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`; counter++; } @@ -69,33 +61,17 @@ export async function getProjectName(initialName?: string): Promise { validate: (value) => { const nameToUse = value.trim() || defaultName; - if (nameToUse === ".") { - const dirContents = fs.readdirSync(process.cwd()); - if (dirContents.length > 0) { - return "Current directory is not empty. Please choose a different directory."; - } - isValid = true; - return undefined; - } - - const projectDir = path.resolve(process.cwd(), nameToUse); - const finalDirName = path.basename(projectDir); - + const finalDirName = path.basename(nameToUse); const validationError = validateDirectoryName(finalDirName); if (validationError) return validationError; - if (!projectDir.startsWith(process.cwd())) { - return "Project path must be within current directory"; - } - - if (fs.pathExistsSync(projectDir)) { - const dirContents = fs.readdirSync(projectDir); - if (dirContents.length > 0) { - return `Directory "${nameToUse}" already exists and is not empty. Please choose a different name or path.`; + if (nameToUse !== ".") { + const projectDir = path.resolve(process.cwd(), nameToUse); + if (!projectDir.startsWith(process.cwd())) { + return "Project path must be within current directory"; } } - isValid = true; return undefined; }, }); @@ -106,6 +82,7 @@ export async function getProjectName(initialName?: string): Promise { } projectPath = response || defaultName; + isValid = true; } return projectPath; diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 6521982..1a3f325 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -37,6 +37,8 @@ export type ProjectApi = "trpc" | "orpc" | "none"; export interface ProjectConfig { projectName: string; + projectDir: string; + relativePath: string; backend: ProjectBackend; runtime: ProjectRuntime; database: ProjectDatabase; diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 13c9cd2..38a9ad5 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -64,7 +64,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string { baseCommand = "bun create better-t-stack@latest"; } - const projectName = config.projectName ? ` ${config.projectName}` : ""; + const projectPathArg = config.relativePath ? ` ${config.relativePath}` : ""; - return `${baseCommand}${projectName} ${flags.join(" ")}`; + return `${baseCommand}${projectPathArg} ${flags.join(" ")}`; } diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs index 815f996..6ac7733 100644 --- a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -94,7 +94,7 @@ export const auth = betterAuth({ database: "", // Invalid configuration trustedOrigins: [ process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} - "my-better-t-app://", // Use hardcoded scheme{{/if}} + "my-better-t-app://",{{/if}} ], emailAndPassword: { enabled: true, diff --git a/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx index e8d5311..de4ab82 100644 --- a/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx +++ b/apps/cli/templates/auth/web/solid/src/components/sign-in-form.tsx @@ -68,8 +68,8 @@ export default function SignInForm({ type="email" value={field().state.value} onBlur={field().handleBlur} - onInput={(e) => field().handleChange(e.currentTarget.value)} // Use onInput and currentTarget - class="w-full rounded border p-2" // Example basic styling + onInput={(e) => field().handleChange(e.currentTarget.value)} + class="w-full rounded border p-2" /> {(error) => ( @@ -122,7 +122,7 @@ export default function SignInForm({ diff --git a/apps/cli/templates/backend/server/express/src/index.ts.hbs b/apps/cli/templates/backend/server/express/src/index.ts.hbs index 2eca13d..da6fd12 100644 --- a/apps/cli/templates/backend/server/express/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/express/src/index.ts.hbs @@ -39,8 +39,6 @@ app.use( app.all("/api/auth{/*path}", toNodeHandler(auth)); {{/if}} -app.use(express.json()) - {{#if (eq api "trpc")}} app.use( "/trpc", @@ -67,6 +65,8 @@ app.use('/rpc{*path}', async (req, res, next) => { }); {{/if}} +app.use(express.json()) + {{#if (includes examples "ai")}} app.post("/ai", async (req, res) => { const { messages = [] } = req.body || {}; diff --git a/apps/web/src/app/(home)/_components/Testimonials.tsx b/apps/web/src/app/(home)/_components/Testimonials.tsx index 79a008f..42cd575 100644 --- a/apps/web/src/app/(home)/_components/Testimonials.tsx +++ b/apps/web/src/app/(home)/_components/Testimonials.tsx @@ -45,7 +45,7 @@ const MAX_VISIBLE_PAGES = 5; export default function Testimonials() { const [startIndex, setStartIndex] = useState(0); - const [tweetsPerPage] = useState(6); // Show 6 tweets per page + const [tweetsPerPage] = useState(6); const totalPages = useMemo( () => Math.ceil(TWEET_IDS.length / tweetsPerPage), diff --git a/apps/web/src/components/theme-toggle.tsx b/apps/web/src/components/theme-toggle.tsx index 9de33a7..d5f65fb 100644 --- a/apps/web/src/components/theme-toggle.tsx +++ b/apps/web/src/components/theme-toggle.tsx @@ -1,6 +1,6 @@ "use client"; -import { cn } from "@/lib/utils"; // Make sure you have this utility +import { cn } from "@/lib/utils"; import * as SwitchPrimitives from "@radix-ui/react-switch"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; diff --git a/bun.lock b/bun.lock index ef8662f..beb770a 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.6.1", + "version": "2.8.0", "bin": { "create-better-t-stack": "dist/index.js", },