From 30efd64fc1002bf3e4cd698312022f63af806133 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 11 Apr 2025 15:45:41 +0530 Subject: [PATCH] Add spinner feedback to database setup workflows --- .changeset/weak-results-film.md | 5 + apps/cli/src/helpers/mongodb-atlas-setup.ts | 120 +++++++++++----- apps/cli/src/helpers/neon-setup.ts | 25 +++- apps/cli/src/helpers/prisma-postgres-setup.ts | 128 +++++++++++------- apps/cli/src/helpers/turso-setup.ts | 101 +++++++++----- apps/cli/src/prompts/frontend-option.ts | 4 +- 6 files changed, 258 insertions(+), 125 deletions(-) create mode 100644 .changeset/weak-results-film.md diff --git a/.changeset/weak-results-film.md b/.changeset/weak-results-film.md new file mode 100644 index 0000000..297a777 --- /dev/null +++ b/.changeset/weak-results-film.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +Add spinner feedback to database setup workflows diff --git a/apps/cli/src/helpers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/mongodb-atlas-setup.ts index 712f06a..cabaf74 100644 --- a/apps/cli/src/helpers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/mongodb-atlas-setup.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { cancel, isCancel, log, text } from "@clack/prompts"; +import { cancel, isCancel, log, spinner, text } from "@clack/prompts"; import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; @@ -10,7 +10,21 @@ type MongoDBConfig = { }; async function checkAtlasCLI(): Promise { - return commandExists("atlas"); + const s = spinner(); + s.start("Checking for MongoDB Atlas CLI"); + + try { + const exists = await commandExists("atlas"); + s.stop( + exists + ? "MongoDB Atlas CLI found" + : pc.yellow("MongoDB Atlas CLI not found"), + ); + return exists; + } catch (error) { + s.stop(pc.red("Error checking for MongoDB Atlas CLI")); + return false; + } } async function initMongoDBAtlas( @@ -29,20 +43,23 @@ async function initMongoDBAtlas( return null; } - log.info(pc.yellow("Setting up MongoDB Atlas...")); + log.info(pc.blue("Running MongoDB Atlas setup...")); await execa("atlas", ["deployments", "setup"], { cwd: serverDir, stdio: "inherit", }); + log.info(pc.green("Atlas setup complete!")); + const connectionString = await text({ - message: "Paste your complete MongoDB connection string:", - placeholder: "mongodb://USERNAME:PASSWORD@HOST/DATABASE", + message: "Enter your MongoDB connection string:", + placeholder: + "mongodb+srv://username:password@cluster.mongodb.net/database", validate(value) { if (!value) return "Please enter a connection string"; if (!value.startsWith("mongodb")) { - return "URL should start with mongodb"; + return "URL should start with mongodb:// or mongodb+srv://"; } }, }); @@ -64,57 +81,88 @@ async function initMongoDBAtlas( } async function writeEnvFile(projectDir: string, config?: MongoDBConfig) { - const envPath = path.join(projectDir, "apps/server", ".env"); - let envContent = ""; + try { + const envPath = path.join(projectDir, "apps/server", ".env"); + await fs.ensureDir(path.dirname(envPath)); - if (await fs.pathExists(envPath)) { - envContent = await fs.readFile(envPath, "utf8"); + let envContent = ""; + if (await fs.pathExists(envPath)) { + envContent = await fs.readFile(envPath, "utf8"); + } + + const mongoUrlLine = config + ? `DATABASE_URL="${config.connectionString}"` + : `DATABASE_URL="mongodb://localhost:27017/mydb"`; + + if (!envContent.includes("DATABASE_URL=")) { + envContent += `\n${mongoUrlLine}`; + } else { + envContent = envContent.replace( + /DATABASE_URL=.*(\r?\n|$)/, + `${mongoUrlLine}$1`, + ); + } + + await fs.writeFile(envPath, envContent.trim()); + } catch (error) { + log.error("Failed to update environment configuration"); + throw error; } - - const mongoUrlLine = config - ? `DATABASE_URL="${config.connectionString}"` - : `DATABASE_URL="mongodb://localhost:27017/mydb"`; - - if (!envContent.includes("DATABASE_URL=")) { - envContent += `\n${mongoUrlLine}`; - } else { - envContent = envContent.replace( - /DATABASE_URL=.*(\r?\n|$)/, - `${mongoUrlLine}$1`, - ); - } - - await fs.writeFile(envPath, envContent.trim()); } function displayManualSetupInstructions() { - log.info(`MongoDB Atlas Setup: + log.info(` +${pc.green("MongoDB Atlas Manual Setup Instructions:")} -1. Install Atlas CLI: https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/ -2. Run 'atlas deployments setup' and follow prompts -3. Get your connection string from the output -4. Format: mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME -5. Add to .env as DATABASE_URL="your_connection_string"`); +1. Install Atlas CLI: + ${pc.blue("https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/")} + +2. Run the following command and follow the prompts: + ${pc.blue("atlas deployments setup")} + +3. Get your connection string from the Atlas dashboard: + Format: ${pc.dim("mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME")} + +4. Add the connection string to your .env file: + ${pc.dim('DATABASE_URL="your_connection_string"')} +`); } export async function setupMongoDBAtlas(projectDir: string) { - const serverDir = path.join(projectDir, "apps/server"); + const mainSpinner = spinner(); + mainSpinner.start("Setting up MongoDB Atlas"); + const serverDir = path.join(projectDir, "apps/server"); try { + await fs.ensureDir(serverDir); + + mainSpinner.stop("Starting MongoDB Atlas setup"); + const config = await initMongoDBAtlas(serverDir); if (config) { await writeEnvFile(projectDir, config); log.success( - pc.green("MongoDB Atlas connection string saved to .env file!"), + pc.green( + "MongoDB Atlas setup complete! Connection saved to .env file.", + ), ); } else { + log.warn(pc.yellow("Falling back to local MongoDB configuration")); await writeEnvFile(projectDir); displayManualSetupInstructions(); } } catch (error) { - log.error(pc.red(`Error during MongoDB Atlas setup: ${error}`)); - await writeEnvFile(projectDir); - displayManualSetupInstructions(); + mainSpinner.stop(pc.red("MongoDB Atlas setup failed")); + log.error( + pc.red( + `Error during MongoDB Atlas setup: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + + try { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } catch {} } } diff --git a/apps/cli/src/helpers/neon-setup.ts b/apps/cli/src/helpers/neon-setup.ts index a3ee85e..5c36cda 100644 --- a/apps/cli/src/helpers/neon-setup.ts +++ b/apps/cli/src/helpers/neon-setup.ts @@ -47,7 +47,7 @@ async function executeNeonCommand( if (s) s.start(spinnerText); const result = await execa(cmd, cmdArgs); - if (s) s.stop(); + if (s) s.stop(spinnerText); return result; } catch (error) { @@ -58,11 +58,15 @@ async function executeNeonCommand( async function isNeonAuthenticated(packageManager: string) { try { - const { stdout } = await executeNeonCommand(packageManager, [ + const { cmd, cmdArgs } = buildNeonCommand(packageManager, [ "projects", "list", ]); - return !stdout.includes("not authenticated") && !stdout.includes("error"); + const result = await execa(cmd, cmdArgs); + return ( + !result.stdout.includes("not authenticated") && + !result.stdout.includes("error") + ); } catch { return false; } @@ -147,11 +151,14 @@ export async function setupNeonPostgres( projectDir: string, packageManager: ProjectPackageManager, ) { - const s = spinner(); + const setupSpinner = spinner(); + setupSpinner.start("Setting up Neon PostgreSQL"); try { const isAuthenticated = await isNeonAuthenticated(packageManager); + setupSpinner.stop("Setting up Neon PostgreSQL"); + if (!isAuthenticated) { log.info("Please authenticate with Neon to continue:"); await authenticateWithNeon(packageManager); @@ -180,14 +187,20 @@ export async function setupNeonPostgres( ); } + const finalSpinner = spinner(); + finalSpinner.start("Configuring database connection"); + await fs.ensureDir(path.join(projectDir, "apps/server")); await writeEnvFile(projectDir, config); - log.success("Neon database configured successfully!"); + + finalSpinner.stop("Neon database configured successfully!"); } catch (error) { - s.stop(pc.red("Neon PostgreSQL setup failed")); + setupSpinner.stop(pc.red("Neon PostgreSQL setup failed")); + if (error instanceof Error) { log.error(pc.red(error.message)); } + await writeEnvFile(projectDir); displayManualSetupInstructions(); } diff --git a/apps/cli/src/helpers/prisma-postgres-setup.ts b/apps/cli/src/helpers/prisma-postgres-setup.ts index fc9efbc..79d1008 100644 --- a/apps/cli/src/helpers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/prisma-postgres-setup.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { cancel, isCancel, log, password } from "@clack/prompts"; +import { cancel, isCancel, log, password, spinner } from "@clack/prompts"; import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; @@ -14,8 +14,9 @@ async function initPrismaDatabase( serverDir: string, packageManager: ProjectPackageManager, ): Promise { + const s = spinner(); try { - log.info(pc.blue("Initializing Prisma PostgreSQL")); + s.start("Initializing Prisma PostgreSQL"); const prismaDir = path.join(serverDir, "prisma"); await fs.ensureDir(prismaDir); @@ -27,6 +28,8 @@ async function initPrismaDatabase( ? "pnpm dlx" : "bunx"; + s.stop("Initializing Prisma. Follow the prompts below:"); + await execa(initCmd, ["prisma", "init", "--db"], { cwd: serverDir, stdio: "inherit", @@ -57,35 +60,42 @@ async function initPrismaDatabase( databaseUrl: databaseUrl as string, }; } catch (error) { + s.stop(pc.red("Failed to initialize Prisma PostgreSQL")); if (error instanceof Error) { - log.error(pc.red(error.message)); + log.error(error.message); } return null; } } async function writeEnvFile(projectDir: string, config?: PrismaConfig) { - const envPath = path.join(projectDir, "apps/server", ".env"); - let envContent = ""; + try { + const envPath = path.join(projectDir, "apps/server", ".env"); + await fs.ensureDir(path.dirname(envPath)); - if (await fs.pathExists(envPath)) { - envContent = await fs.readFile(envPath, "utf8"); + let envContent = ""; + if (await fs.pathExists(envPath)) { + envContent = await fs.readFile(envPath, "utf8"); + } + + const databaseUrlLine = config + ? `DATABASE_URL="${config.databaseUrl}"` + : `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; + + if (!envContent.includes("DATABASE_URL=")) { + envContent += `\n${databaseUrlLine}`; + } else { + envContent = envContent.replace( + /DATABASE_URL=.*(\r?\n|$)/, + `${databaseUrlLine}$1`, + ); + } + + await fs.writeFile(envPath, envContent.trim()); + } catch (error) { + log.error("Failed to update environment configuration"); + throw error; } - - const databaseUrlLine = config - ? `DATABASE_URL="${config.databaseUrl}"` - : `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; - - if (!envContent.includes("DATABASE_URL=")) { - envContent += `\n${databaseUrlLine}`; - } else { - envContent = envContent.replace( - /DATABASE_URL=.*(\r?\n|$)/, - `${databaseUrlLine}$1`, - ); - } - - await fs.writeFile(envPath, envContent.trim()); } function displayManualSetupInstructions() { @@ -99,33 +109,6 @@ function displayManualSetupInstructions() { DATABASE_URL="your_database_url"`); } -export async function setupPrismaPostgres( - projectDir: string, - packageManager: ProjectPackageManager = "npm", -) { - const serverDir = path.join(projectDir, "apps/server"); - - try { - const config = await initPrismaDatabase(serverDir, packageManager); - - if (config) { - await writeEnvFile(projectDir, config); - await addPrismaAccelerateExtension(serverDir); - log.success( - pc.green("Prisma PostgreSQL database configured successfully!"), - ); - } else { - await writeEnvFile(projectDir); - displayManualSetupInstructions(); - } - } catch (error) { - log.error(pc.red(`Error during Prisma PostgreSQL setup: ${error}`)); - await writeEnvFile(projectDir); - displayManualSetupInstructions(); - log.info("Setup completed with manual configuration required."); - } -} - async function addPrismaAccelerateExtension(serverDir: string) { try { addPackageDependency({ @@ -159,9 +142,56 @@ export default prisma; await fs.writeFile(dbFilePath, dbFileContent); } } + return true; } catch (error) { log.warn( pc.yellow("Could not add Prisma Accelerate extension automatically"), ); + return false; + } +} + +export async function setupPrismaPostgres( + projectDir: string, + packageManager: ProjectPackageManager = "npm", +) { + const serverDir = path.join(projectDir, "apps/server"); + const s = spinner(); + s.start("Setting up Prisma PostgreSQL"); + + try { + await fs.ensureDir(serverDir); + + s.stop("Starting Prisma setup"); + + const config = await initPrismaDatabase(serverDir, packageManager); + + if (config) { + await writeEnvFile(projectDir, config); + await addPrismaAccelerateExtension(serverDir); + log.success( + pc.green("Prisma PostgreSQL database configured successfully!"), + ); + } else { + const fallbackSpinner = spinner(); + fallbackSpinner.start("Setting up fallback configuration"); + await writeEnvFile(projectDir); + fallbackSpinner.stop("Manual setup required"); + displayManualSetupInstructions(); + } + } catch (error) { + s.stop(pc.red("Prisma PostgreSQL setup failed")); + log.error( + pc.red( + `Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + + try { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } catch {} + + log.info("Setup completed with manual configuration required."); } } diff --git a/apps/cli/src/helpers/turso-setup.ts b/apps/cli/src/helpers/turso-setup.ts index 7b3f5f7..26202d3 100644 --- a/apps/cli/src/helpers/turso-setup.ts +++ b/apps/cli/src/helpers/turso-setup.ts @@ -69,7 +69,7 @@ async function installTursoCLI(isMac: boolean) { return true; } catch (error) { if (error instanceof Error && error.message.includes("User force closed")) { - s.stop(); + s.stop("Turso CLI installation cancelled"); log.warn(pc.yellow("Turso CLI installation cancelled by user")); throw new Error("Installation cancelled"); } @@ -79,11 +79,14 @@ async function installTursoCLI(isMac: boolean) { } async function getTursoGroups(): Promise { + const s = spinner(); try { + s.start("Fetching Turso groups..."); const { stdout } = await $`turso group list`; const lines = stdout.trim().split("\n"); if (lines.length <= 1) { + s.stop("No Turso groups found"); return []; } @@ -92,8 +95,10 @@ async function getTursoGroups(): Promise { return { name, locations, version, status }; }); + s.stop(`Found ${groups.length} Turso groups`); return groups; } catch (error) { + s.stop(pc.red("Error fetching Turso groups")); console.error("Error fetching Turso groups:", error); return []; } @@ -107,6 +112,7 @@ async function selectTursoGroup(): Promise { } if (groups.length === 1) { + log.info(`Using the only available group: ${pc.blue(groups[0].name)}`); return groups[0].name; } @@ -132,26 +138,43 @@ async function createTursoDatabase( dbName: string, groupName: string | null, ): Promise { + const s = spinner(); + try { + s.start( + `Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`, + ); + if (groupName) { await $`turso db create ${dbName} --group ${groupName}`; } else { await $`turso db create ${dbName}`; } + + s.stop(`Created database "${dbName}"`); } catch (error) { + s.stop(pc.red(`Failed to create database "${dbName}"`)); if (error instanceof Error && error.message.includes("already exists")) { throw new Error("DATABASE_EXISTS"); } throw error; } - const { stdout: dbUrl } = await $`turso db show ${dbName} --url`; - const { stdout: authToken } = await $`turso db tokens create ${dbName}`; + s.start("Retrieving database connection details..."); + try { + const { stdout: dbUrl } = await $`turso db show ${dbName} --url`; + const { stdout: authToken } = await $`turso db tokens create ${dbName}`; - return { - dbUrl: dbUrl.trim(), - authToken: authToken.trim(), - }; + s.stop("Retrieved database connection details"); + + return { + dbUrl: dbUrl.trim(), + authToken: authToken.trim(), + }; + } catch (error) { + s.stop(pc.red("Failed to retrieve database connection details")); + throw error; + } } async function writeEnvFile(projectDir: string, config?: TursoConfig) { @@ -162,6 +185,7 @@ DATABASE_AUTH_TOKEN="${config.authToken}"` : `DATABASE_URL= DATABASE_AUTH_TOKEN=`; + await fs.ensureDir(path.dirname(envPath)); await fs.writeFile(envPath, envContent); } @@ -181,25 +205,34 @@ export async function setupTurso( projectDir: string, shouldSetupTurso: boolean, ) { - if (!shouldSetupTurso) { - await writeEnvFile(projectDir); - log.info(pc.blue("Skipping Turso setup. Setting up empty configuration.")); - displayManualSetupInstructions(); - return; - } - - const platform = os.platform(); - const isMac = platform === "darwin"; - const canInstallCLI = platform !== "win32"; - - if (!canInstallCLI) { - log.warn(pc.yellow("Automatic Turso setup is not supported on Windows.")); - await writeEnvFile(projectDir); - displayManualSetupInstructions(); - return; - } + const setupSpinner = spinner(); + setupSpinner.start("Setting up Turso database"); try { + if (!shouldSetupTurso) { + setupSpinner.stop("Skipping Turso setup"); + await writeEnvFile(projectDir); + log.info( + pc.blue("Skipping Turso setup. Setting up empty configuration."), + ); + displayManualSetupInstructions(); + return; + } + + const platform = os.platform(); + const isMac = platform === "darwin"; + const canInstallCLI = platform !== "win32"; + + if (!canInstallCLI) { + setupSpinner.stop(pc.yellow("Turso setup not supported on Windows")); + log.warn(pc.yellow("Automatic Turso setup is not supported on Windows.")); + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + return; + } + + setupSpinner.stop("Checking Turso CLI"); + const isCliInstalled = await isTursoInstalled(); if (!isCliInstalled) { @@ -247,28 +280,32 @@ export async function setupTurso( } dbName = dbNameResponse as string; - const s = spinner(); try { - s.start( - `Creating Turso database "${dbName}"${selectedGroup ? ` in group "${selectedGroup}"` : ""}...`, - ); const config = await createTursoDatabase(dbName, selectedGroup); + + const finalSpinner = spinner(); + finalSpinner.start("Writing configuration to .env file"); await writeEnvFile(projectDir, config); - s.stop("Turso database configured successfully!"); + finalSpinner.stop("Turso database configured successfully!"); + success = true; } catch (error) { if (error instanceof Error && error.message === "DATABASE_EXISTS") { - s.stop(pc.yellow(`Database "${pc.red(dbName)}" already exists`)); + log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`)); suggestedName = `${dbName}-${Math.floor(Math.random() * 1000)}`; } else { - s.stop(pc.red("Failed to create Turso database")); throw error; } } } } catch (error) { - log.error(pc.red(`Error during Turso setup: ${error}`)); + setupSpinner.stop(pc.red("Failed to set up Turso database")); + log.error( + pc.red( + `Error during Turso setup: ${error instanceof Error ? error.message : String(error)}`, + ), + ); await writeEnvFile(projectDir); displayManualSetupInstructions(); log.success("Setup completed with manual configuration required."); diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts index eca3d91..5da30bc 100644 --- a/apps/cli/src/prompts/frontend-option.ts +++ b/apps/cli/src/prompts/frontend-option.ts @@ -52,12 +52,12 @@ export async function getFrontendChoice( { value: "react-router", label: "React Router", - hint: "A user‑obsessed, standards‑focused, multi‑strategy router you can deploy anywhere.", + hint: "A user‑obsessed, standards‑focused, multi‑strategy router", }, { value: "tanstack-start", label: "TanStack Start (beta)", - hint: "SSR, Streaming, Server Functions, API Routes, bundling and more powered by TanStack Router and Vite.", + hint: "SSR, Server Functions, API Routes and more with TanStack Router", }, ], initialValue: