UNPKG

@codeandcreed/create-titan

Version:

CLI tool to create new Titan projects with options for Individual or B2B SaaS

746 lines (721 loc) 31.4 kB
#!/usr/bin/env node "use strict"; // src/index.ts var { Command } = require("commander"); var chalk = require("chalk"); var execa = require("execa"); var ora = require("ora"); var prompts = require("prompts"); var fs = require("fs/promises"); var path = require("path"); var os = require("os"); var isWindows = os.platform() === "win32"; var is64Bit = os.arch() === "x64"; var rmrf = process.platform === "win32" ? ["cmd", ["/c", "rmdir", "/s", "/q"]] : ["rm", ["-rf"]]; var gitInit = process.platform === "win32" ? ["cmd", ["/c", "git", "init"]] : ["git", ["init"]]; var program = new Command().name("create-titan").description("Create a new Titan project").version("0.1.0").parse(); async function checkGitHubSSH() { try { await execa("git", ["ls-remote", "git@github.com:ObaidUr-Rahmaan/titan.git", "HEAD"], { timeout: 1e4 // 10 seconds timeout }); return true; } catch (error) { return false; } } async function main() { const spinner = ora(); try { const titanLogo = [ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557", "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551", " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551", " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551", " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551", " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D" ].join("\n"); console.log(chalk.cyan(titanLogo)); console.log(chalk.yellow("Pre-requisites check:")); console.log(chalk.yellow("1. The following connection info is ready:")); console.log(chalk.yellow(" - Clerk (Publishable Key & Secret Key)")); console.log(chalk.yellow(" - Stripe (Public Key, Secret Key & Price ID)")); console.log(chalk.yellow(" - Plunk API Key")); console.log(chalk.yellow(" - Supabase:")); console.log(chalk.yellow(" * NEXT_PUBLIC_SUPABASE_URL")); console.log(chalk.yellow(" * SUPABASE_SERVICE_ROLE_KEY")); console.log(chalk.yellow(" * DATABASE_URL (with pgbouncer)")); console.log(chalk.yellow(" * DIRECT_URL (without pgbouncer)\n")); console.log(chalk.cyan("\u{1F4A1} Important Note:")); console.log( chalk.cyan( ' We recommend creating a dedicated project in Supabase called "[Project Name] Dev DB"' ) ); console.log( chalk.cyan( " for development purposes. Supabase offers 2 free projects in their free tier, so you can" ) ); console.log( chalk.cyan(" use one for development and one for production later on.\n") ); spinner.text = "Checking GitHub SSH authentication..."; const hasGitHubSSH = await checkGitHubSSH(); if (!hasGitHubSSH) { spinner.fail("GitHub SSH authentication failed"); console.log(chalk.red("\nError: Unable to authenticate with GitHub via SSH.")); console.log(chalk.yellow("\nPlease set up SSH authentication with GitHub:")); console.log(chalk.cyan("1. Generate an SSH key if you don't have one:")); console.log(chalk.cyan(' ssh-keygen -t ed25519 -C "your_email@example.com"')); console.log(chalk.cyan("2. Add your SSH key to the ssh-agent:")); console.log(chalk.cyan(' eval "$(ssh-agent -s)"')); console.log(chalk.cyan(" ssh-add ~/.ssh/id_ed25519")); console.log(chalk.cyan("3. Add your SSH key to your GitHub account:")); console.log(chalk.cyan(" - Copy your public key to clipboard:")); console.log(chalk.cyan(" cat ~/.ssh/id_ed25519.pub | pbcopy")); console.log(chalk.cyan(" - Go to GitHub > Settings > SSH and GPG keys > New SSH key")); console.log(chalk.cyan(" - Paste your key and save")); console.log(chalk.cyan("4. Test your connection:")); console.log(chalk.cyan(" ssh -T git@github.com")); console.log(chalk.cyan("5. Run this CLI again")); process.exit(1); } spinner.succeed("GitHub SSH authentication successful"); const { proceed } = await prompts({ type: "confirm", name: "proceed", message: "Do you have all pre-requisites ready?", initial: false }); if (!proceed) { console.log(chalk.cyan("\nPlease set up the pre-requisites and try again.")); console.log( chalk.cyan( "For detailed setup instructions, visit: https://github.com/ObaidUr-Rahmaan/titan#prerequisites" ) ); process.exit(0); } const { setupEnvNow } = await prompts({ type: "select", name: "setupEnvNow", message: "When would you like to set up environment variables?", choices: [ { title: "Now - I have all my API keys and credentials ready", value: true }, { title: "Later - Skip env setup for now and add them manually later", value: false } ], initial: 0 }); const { projectType } = await prompts({ type: "select", name: "projectType", message: "What type of application are you building?", choices: [ { title: "Individual SaaS - Personal user accounts only", description: "Simple SaaS with individual user authentication (Clerk personal accounts)", value: "individual" }, { title: "B2B SaaS - Organizations + Teams", description: "Business SaaS with organization management, team features, and seat-based billing", value: "b2b" } ], initial: 1 // Default to B2B since it's the full-featured option }, { onCancel: () => { console.log("\nSetup cancelled"); process.exit(1); } }); const { projectName, projectDescription, githubRepo } = await prompts( [ { type: "text", name: "projectName", message: "What is your project name?", initial: "my-titan-app" }, { type: "text", name: "projectDescription", message: "Describe your project in a few words:" }, { type: "text", name: "githubRepo", message: "Enter your GitHub repository URL (SSH format: git@github.com:username/repo.git):", validate: (value) => { const sshFormat = /^git@github\.com:.+\/.+\.git$/; const httpsFormat = /^https:\/\/github\.com\/.+\/.+\.git$/; if (sshFormat.test(value)) return true; if (httpsFormat.test(value)) { const sshUrl = value.replace("https://github.com/", "git@github.com:").replace(/\.git$/, ".git"); return `Please use the SSH URL format instead: ${sshUrl}`; } return "Please enter a valid GitHub SSH URL (format: git@github.com:username/repo.git)"; } } ], { onCancel: () => { console.log("\nSetup cancelled"); process.exit(1); } } ); const projectDir = path.join(process.cwd(), projectName); try { await fs.access(projectDir); console.error( chalk.red( ` Error: Directory ${projectName} already exists. Please choose a different name or delete the existing directory.` ) ); process.exit(1); } catch { await fs.mkdir(projectDir); } spinner.start("Creating your project..."); const maxRetries = 3; let retryCount = 0; while (retryCount < maxRetries) { try { spinner.text = "Cloning template repository..."; await execa("git", [ "clone", "--depth=1", "--single-branch", "git@github.com:ObaidUr-Rahmaan/titan.git", projectDir ]); spinner.succeed("Project cloned successfully!"); break; } catch (error) { retryCount++; if (retryCount === maxRetries) { spinner.fail("Failed to clone repository"); console.error(chalk.red("\nError cloning repository. Please check:")); console.log(chalk.cyan("1. Your SSH key is set up correctly:")); console.log(chalk.cyan(" Run: ssh -T git@github.com")); console.log( chalk.cyan( " If it fails, follow: https://docs.github.com/en/authentication/connecting-to-github-with-ssh" ) ); console.log(chalk.cyan("\n2. The repository exists on GitHub:")); console.log(chalk.cyan(" - Go to GitHub")); console.log(chalk.cyan(' - Create repository named "your-repo-name"')); console.log(chalk.cyan(" - Don't initialize with any files")); console.log(chalk.cyan("\n3. Try cloning manually to verify:")); console.log( chalk.cyan( ` git clone --depth=1 git@github.com:ObaidUr-Rahmaan/titan.git ${projectDir}` ) ); process.exit(1); } spinner.text = `Retrying clone (${retryCount}/${maxRetries})...`; await new Promise((resolve) => setTimeout(resolve, 2e3)); } } let envContent = ""; spinner.start("Writing configuration files..."); if (setupEnvNow) { spinner.stop(); const promptWithConfirmation = async (message, type = "text") => { let confirmed = false; let value = ""; while (!confirmed) { const result = await prompts({ type, name: "value", message }, { onCancel: () => { console.log("\nSetup cancelled"); process.exit(1); } }); value = result.value; if (!value) { console.log(chalk.red("This value is required")); continue; } const confirmation = await prompts({ type: "text", name: "confirmed", message: `Are you sure you've inputted that env var correctly? (Type "yes" to proceed)` }, { onCancel: () => { console.log("\nSetup cancelled"); process.exit(1); } }); confirmed = confirmation.confirmed?.toLowerCase() === "yes"; if (!confirmed) { console.log(chalk.yellow("Let's try again...")); } } return value; }; const clerkPublishableKey = await promptWithConfirmation("Enter your Clerk Public Key:", "password"); const clerkSecretKey = await promptWithConfirmation("Enter your Clerk Secret Key:", "password"); const authConfig = { clerkPublishableKey, clerkSecretKey }; if (!authConfig.clerkPublishableKey || !authConfig.clerkSecretKey) { console.log(chalk.red("Clerk keys are required")); process.exit(1); } spinner.start("Configuring authentication..."); envContent += `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${authConfig.clerkPublishableKey} `; envContent += `CLERK_SECRET_KEY=${authConfig.clerkSecretKey} `; envContent += `NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in `; envContent += `NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up `; envContent += `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ `; envContent += `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ `; spinner.succeed("Authentication configured"); spinner.stop(); const { dbSetup } = await prompts({ type: "select", name: "dbSetup", message: "Choose your database setup:", choices: [ { title: 'Development Database (create a dedicated project in Supabase called "[Project Name] Dev DB")', value: "dev" }, { title: "Production Database (NOT RECOMMENDED FOR NEW PROJECTS)", value: "prod" } ] }); if (dbSetup === "dev") { spinner.stop(); console.log( chalk.green( "\n\u2705 Good choice! Using a dedicated Supabase Dev DB is reliable and consistent." ) ); console.log( chalk.green( ` If you haven't already, create a project called "[Project Name] Dev DB" in Supabase.` ) ); console.log( chalk.green( " You can find your database credentials in the project settings.\n" ) ); const supabaseUrl = await promptWithConfirmation("Enter your Supabase Project URL:"); if (!supabaseUrl.startsWith("https://")) { console.log(chalk.red("URL must start with https://")); process.exit(1); } const supabaseAnonKey = await promptWithConfirmation("Enter your Supabase Anon Key:", "password"); const supabaseServiceKey = await promptWithConfirmation("Enter your Supabase Service Role Key:", "password"); const databaseUrl = await promptWithConfirmation("Enter your Database URL (with pgbouncer):"); if (!databaseUrl.includes("?pgbouncer=true")) { console.log(chalk.red("URL must include ?pgbouncer=true")); process.exit(1); } const directUrl = await promptWithConfirmation("Enter your Direct URL (without pgbouncer):"); const dbConfig = { supabaseUrl, supabaseAnonKey, supabaseServiceKey, databaseUrl, directUrl }; if (!dbConfig.supabaseUrl || !dbConfig.supabaseAnonKey || !dbConfig.supabaseServiceKey || !dbConfig.databaseUrl || !dbConfig.directUrl) { console.log(chalk.red("All database configuration values are required")); process.exit(1); } envContent += `NEXT_PUBLIC_SUPABASE_URL=${dbConfig.supabaseUrl} `; envContent += `NEXT_PUBLIC_SUPABASE_ANON_KEY=${dbConfig.supabaseAnonKey} `; envContent += `SUPABASE_SERVICE_ROLE_KEY=${dbConfig.supabaseServiceKey} `; envContent += `DATABASE_URL=${dbConfig.databaseUrl} `; envContent += `DIRECT_URL=${dbConfig.directUrl} `; envContent += `FRONTEND_URL=http://localhost:3000 `; await fs.writeFile(path.join(projectDir, ".env"), envContent); console.log(chalk.yellow("\nSupabase env variables are set. We're skipping database setup during project creation.")); console.log(chalk.cyan("After installation, you can set up the database with:")); console.log(chalk.cyan(" bun run db:init")); } else { spinner.stop(); console.log( chalk.green( "\n\u2705 Good choice! Using a dedicated Supabase Dev DB is reliable and consistent." ) ); console.log( chalk.green( ` If you haven't already, create a project called "[Project Name] Dev DB" in Supabase.` ) ); console.log( chalk.green( " You can find your database credentials in the project settings.\n" ) ); const supabaseUrl = await promptWithConfirmation("Enter your Supabase Project URL:"); if (!supabaseUrl.startsWith("https://")) { console.log(chalk.red("URL must start with https://")); process.exit(1); } const supabaseAnonKey = await promptWithConfirmation("Enter your Supabase Anon Key:", "password"); const supabaseServiceKey = await promptWithConfirmation("Enter your Supabase Service Role Key:", "password"); const databaseUrl = await promptWithConfirmation("Enter your Database URL (with pgbouncer):"); if (!databaseUrl.includes("?pgbouncer=true")) { console.log(chalk.red("URL must include ?pgbouncer=true")); process.exit(1); } const directUrl = await promptWithConfirmation("Enter your Direct URL (without pgbouncer):"); const dbConfig = { supabaseUrl, supabaseAnonKey, supabaseServiceKey, databaseUrl, directUrl }; if (!dbConfig.supabaseUrl || !dbConfig.supabaseAnonKey || !dbConfig.supabaseServiceKey || !dbConfig.databaseUrl || !dbConfig.directUrl) { console.log(chalk.red("All database configuration values are required")); process.exit(1); } envContent += `NEXT_PUBLIC_SUPABASE_URL=${dbConfig.supabaseUrl} `; envContent += `NEXT_PUBLIC_SUPABASE_ANON_KEY=${dbConfig.supabaseAnonKey} `; envContent += `SUPABASE_SERVICE_ROLE_KEY=${dbConfig.supabaseServiceKey} `; envContent += `DATABASE_URL=${dbConfig.databaseUrl} `; envContent += `DIRECT_URL=${dbConfig.directUrl} `; envContent += `FRONTEND_URL=http://localhost:3000 `; await fs.writeFile(path.join(projectDir, ".env"), envContent); console.log(chalk.yellow("\nDatabase setup is skipped during project creation.")); console.log(chalk.cyan("After installation, once you are happy with your database schema, you can update db/schema/ and then run:")); console.log(chalk.cyan(" bun run db:push")); } spinner.stop(); const stripePublicKey = await promptWithConfirmation("Enter your Stripe Publishable Key:"); const stripeSecretKey = await promptWithConfirmation("Enter your Stripe Secret Key:", "password"); const stripePriceId = await promptWithConfirmation("Enter your Stripe Price ID:"); const paymentConfig = { stripePublicKey, stripeSecretKey, stripePriceId }; if (!paymentConfig.stripeSecretKey || !paymentConfig.stripePublicKey || !paymentConfig.stripePriceId) { console.log(chalk.red("All Stripe configuration values are required")); process.exit(1); } spinner.start("Configuring payments..."); envContent += `STRIPE_SECRET_KEY=${paymentConfig.stripeSecretKey} `; envContent += `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${paymentConfig.stripePublicKey} `; envContent += `NEXT_PUBLIC_STRIPE_PRODUCT_1_PRICE_ID=${paymentConfig.stripePriceId} `; spinner.succeed("Payments configured"); spinner.stop(); const plunkApiKey = await promptWithConfirmation("Enter your Plunk Secret API Key:"); const emailConfig = { plunkApiKey }; if (!emailConfig.plunkApiKey) { console.log(chalk.red("Plunk API Key is required")); process.exit(1); } spinner.start("Configuring email..."); envContent += `PLUNK_API_KEY=${emailConfig.plunkApiKey} `; spinner.succeed("Email configured"); } else { spinner.start("Creating placeholder environment file..."); envContent = `# Authentication - Clerk (https://clerk.dev) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key CLERK_SECRET_KEY=your_clerk_secret_key CLERK_WEBHOOK_SECRET=your_clerk_webhook_secret # Clerk redirect URLs NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ # Database - Supabase (https://supabase.com) NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key # Database connection URLs DATABASE_URL=your_database_url_with_pgbouncer DIRECT_URL=your_direct_url_without_pgbouncer FRONTEND_URL=http://localhost:3000 # Payments - Stripe (https://stripe.com) STRIPE_SECRET_KEY=your_stripe_secret_key NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key NEXT_PUBLIC_STRIPE_PRODUCT_1_PRICE_ID=your_stripe_price_id STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret # Email - Plunk (https://useplunk.com) PLUNK_API_KEY=your_plunk_api_key # NOTE: You need to replace the placeholder values with your actual credentials # before running the application. This file was created by create-titan CLI # with the "setup environment variables later" option.`; spinner.succeed("Created placeholder .env file with instructions"); } await fs.writeFile(path.join(projectDir, ".env"), envContent); await fs.rm(path.join(projectDir, ".env.template")); const configPath = path.join(projectDir, "config.ts"); const configContent = `const config = { auth: { enabled: true, }, payments: { enabled: true, }, email: { enabled: true, }, }; export default config; `; await fs.writeFile(configPath, configContent); spinner.succeed(chalk.green("Project configured successfully! \u{1F680}")); spinner.start("Installing dependencies..."); try { await execa("bun", ["install"], { stdio: "inherit", cwd: projectDir }); spinner.succeed("Dependencies installed"); } catch (error) { spinner.fail("Failed to install dependencies"); console.error(chalk.red("Error installing dependencies:"), error); throw error; } spinner.start("Setting up git repository..."); try { await execa("rm", ["-rf", path.join(projectDir, ".git")]); const originalDir = process.cwd(); process.chdir(projectDir); await execa("git", ["init"]); await execa("git", ["add", "."]); await execa("git", ["commit", "-m", "Initial commit from Titan CLI"]); await execa("git", ["branch", "-M", "main"]); try { await execa("git", ["remote", "remove", "origin"]); } catch (error) { } await execa("git", ["remote", "add", "origin", githubRepo]); try { await execa("git", ["push", "-u", "origin", "main", "--force"]); } catch (pushError) { await execa("git", ["branch", "-M", "master"]); await execa("git", ["push", "-u", "origin", "master", "--force"]); } process.chdir(originalDir); spinner.succeed("Git repository setup complete"); } catch (error) { try { const originalDir = process.cwd(); if (originalDir !== projectDir) { process.chdir(originalDir); } } catch (e) { } spinner.warn("Git setup had some issues"); console.log(chalk.yellow("\nTo push your code to GitHub manually:")); console.log(chalk.cyan(`1. cd ${projectName}`)); console.log(chalk.cyan("2. git remote add origin " + githubRepo)); console.log(chalk.cyan("3. git branch -M main")); console.log(chalk.cyan("4. git push -u origin main --force")); } const readmeContent = `# ${projectName} ${projectDescription} # ToDos - Add todos here... `; await fs.writeFile(path.join(projectDir, "README.md"), readmeContent); try { await fs.rm(path.join(projectDir, "packages"), { recursive: true, force: true }); } catch (error) { } if (projectType === "individual") { spinner.start("Configuring for individual SaaS setup..."); try { await fs.rm(path.join(projectDir, "app/org"), { recursive: true, force: true }); await fs.rm(path.join(projectDir, "components/organizations"), { recursive: true, force: true }); await fs.rm(path.join(projectDir, "db/schema/organizations.ts"), { force: true }); await fs.rm(path.join(projectDir, "db/schema/organization-memberships.ts"), { force: true }); await fs.rm(path.join(projectDir, "db/schema/organization-invitations.ts"), { force: true }); await fs.rm(path.join(projectDir, "app/api/organizations"), { recursive: true, force: true }); await fs.rm(path.join(projectDir, "utils/auth/organization-helpers.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/actions/organizations.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/hooks/use-organization-data.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/hooks/use-organization-guard.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/hooks/use-organization-state.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/hooks/useOrganizationSubscription.ts"), { force: true }); await fs.rm(path.join(projectDir, "utils/data/shared"), { recursive: true, force: true }); await fs.rm(path.join(projectDir, "utils/api/validation/organization.ts"), { force: true }); await fs.rm(path.join(projectDir, "add-teams-orgs-for-b2b-apps.md"), { force: true }); await fs.rm(path.join(projectDir, "test-organization-routing.md"), { force: true }); const middlewarePath = path.join(projectDir, "middleware.ts"); const middlewareContent = await fs.readFile(middlewarePath, "utf-8"); const updatedMiddleware = middlewareContent.replace(/\/org\/.*?\)\]\*/, "").replace(/organizationId.*?\n.*?\n/g, "").replace(/\/\/ Organization routes[\s\S]*?\/\/ End organization routes/g, ""); await fs.writeFile(middlewarePath, updatedMiddleware); const schemaIndexPath = path.join(projectDir, "db/schema/index.ts"); const schemaContent = await fs.readFile(schemaIndexPath, "utf-8"); const updatedSchema = schemaContent.replace(/export \* from ['"]\.\/organizations['"];?\n?/g, "").replace(/export \* from ['"]\.\/organization-.*?['"];?\n?/g, ""); await fs.writeFile(schemaIndexPath, updatedSchema); const subscriptionPath = path.join(projectDir, "utils/subscription-management.ts"); const subscriptionContent = await fs.readFile(subscriptionPath, "utf-8"); const updatedSubscription = subscriptionContent.replace(/SubscriptionContext[\s\S]*?= 'individual' \| 'organization'/g, "SubscriptionContext = 'individual'").replace(/context === 'organization'[\s\S]*?}/g, "// Organization context removed for individual SaaS"); await fs.writeFile(subscriptionPath, updatedSubscription); spinner.succeed("Individual SaaS configuration complete"); } catch (error) { spinner.warn("Some organization files could not be removed (this is usually fine)"); } } spinner.start("Writing final configurations..."); await fs.writeFile(path.join(projectDir, ".env"), envContent); spinner.succeed("Final configurations written"); spinner.start("Customizing application layout..."); const layoutPath = path.join(projectDir, "app", "layout.tsx"); const formatProjectName = (name) => { return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); }; const formattedProjectName = formatProjectName(projectName); const layoutContent = `import Provider from '@/app/provider'; import { ThemeProvider } from '@/components/theme-provider'; import { Toaster } from '@/components/ui/sonner'; import AuthWrapper from '@/components/wrapper/auth-wrapper'; import { Analytics } from '@vercel/analytics/react'; import { GeistSans } from 'geist/font/sans'; import type { Metadata } from 'next'; import './globals.css'; import { validateConfig } from '@/lib/config-validator'; // Validate config on app initialization validateConfig(); export const metadata: Metadata = { metadataBase: new URL('http://localhost:3000'), title: { default: '${formattedProjectName}', template: \`%s | ${formattedProjectName}\`, }, description: '${projectDescription}', icons: [ { rel: 'icon', url: '/favicon.ico' }, { rel: 'icon', url: '/favicon.png', type: 'image/png' }, { rel: 'apple-touch-icon', url: '/favicon.png' }, ], openGraph: { description: '${projectDescription}', images: [''], url: '', }, twitter: { card: 'summary_large_image', title: '${formattedProjectName}', description: '${projectDescription}', siteId: '', creator: '', creatorId: '', images: [''], }, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <AuthWrapper> <html lang="en" suppressHydrationWarning> <head> <link rel="icon" type="image/png" href="/favicon.png" /> </head> <body className={GeistSans.className}> <Provider> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} <Toaster /> </ThemeProvider> </Provider> <Analytics /> </body> </html> </AuthWrapper> ); }`; await fs.writeFile(layoutPath, layoutContent); spinner.succeed("Application layout customized"); console.log(chalk.green("\n\u2728 Project created and pushed to GitHub successfully! \u2728")); if (projectType === "individual") { console.log(chalk.yellow("\n\u{1F3AF} Individual SaaS Project Configuration:")); console.log(chalk.yellow(" \u2022 Personal user accounts with Clerk authentication")); console.log(chalk.yellow(" \u2022 Individual subscription management")); console.log(chalk.yellow(" \u2022 Organization features have been removed")); console.log(chalk.yellow(" \u2022 Optimized for simple, personal-use SaaS applications")); } else { console.log(chalk.yellow("\n\u{1F3E2} B2B SaaS Project Configuration:")); console.log(chalk.yellow(" \u2022 Organization management with team features")); console.log(chalk.yellow(" \u2022 Clerk Organizations integration")); console.log(chalk.yellow(" \u2022 Seat-based billing and subscription management")); console.log(chalk.yellow(" \u2022 Member invitations and role management")); console.log(chalk.yellow(" \u2022 Full-featured B2B SaaS boilerplate")); } console.log(chalk.cyan("\nNext steps:")); console.log(chalk.cyan("1. cd into your project")); console.log(chalk.cyan(` cd ${projectName}`)); if (!setupEnvNow) { console.log(chalk.cyan("2. Set up your environment variables in .env file")); console.log(chalk.cyan(" You'll need to replace the placeholder values with your actual credentials")); console.log(chalk.cyan("3. Run `bun run dev` to start the development server")); } else { console.log(chalk.cyan("2. Run `bun run dev` to start the development server")); } if (projectType === "b2b") { console.log(chalk.cyan("\n\u{1F4DA} B2B Features Documentation:")); console.log(chalk.cyan(" \u2022 Organization setup: Check the included documentation")); console.log(chalk.cyan(" \u2022 Billing configuration: Configure Stripe for seat-based billing")); console.log(chalk.cyan(" \u2022 Member management: Set up organization roles and permissions")); } } catch (error) { if (spinner) spinner.fail("Failed to create project"); console.error(chalk.red("Error:"), error); process.exit(1); } } process.on("SIGINT", () => { console.log("\nSetup cancelled"); process.exit(1); }); main().catch((error) => { console.error(error); process.exit(1); });