UNPKG

launch-express

Version:

CLI tool to setup a new Launch Express project

819 lines (812 loc) 46 kB
import chalk from 'chalk'; import { intro, outro, select, text, multiselect, spinner, isCancel, log } from '@clack/prompts'; import fs from 'fs'; import path from 'path'; import { REPO_URL, execAsync } from '../utils.js'; const AUTH_PROVIDERS = { supabase: [ 'Apple', 'Azure', 'Bitbucket', 'Discord', 'Facebook', 'Figma', 'Github', 'Gitlab', 'Google', 'Keycloak', 'LinkedIn', 'Notion', 'Slack', 'Spotify', 'Twitch', 'Twitter', ], 'better-auth': [ 'Apple', 'Discord', 'Facebook', 'Github', 'Google', 'Microsoft', 'Twitch', 'Twitter', 'Dropbox', 'LinkedIn', 'Gitlab', 'Reddit', ], }; const DEFAULT_OPTIONS = { authFramework: 'better-auth', authProviders: ['Google'], paymentProvider: 'Stripe', database: 'PostgreSQL', analytics: 'None', aiProvider: [], }; // Map of provider names for better-auth const BETTER_AUTH_PROVIDER_CONFIG = { Apple: { envVars: ['APPLE_CLIENT_ID', 'APPLE_CLIENT_SECRET'], importName: 'apple', }, Discord: { envVars: ['DISCORD_CLIENT_ID', 'DISCORD_CLIENT_SECRET'], importName: 'discord', }, Facebook: { envVars: ['FACEBOOK_CLIENT_ID', 'FACEBOOK_CLIENT_SECRET'], importName: 'facebook', }, Github: { envVars: ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'], importName: 'github', }, Google: { envVars: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'], importName: 'google', }, Microsoft: { envVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'], importName: 'microsoft', }, Twitch: { envVars: ['TWITCH_CLIENT_ID', 'TWITCH_CLIENT_SECRET'], importName: 'twitch', }, Twitter: { envVars: ['TWITTER_CLIENT_ID', 'TWITTER_CLIENT_SECRET'], importName: 'twitter', }, Dropbox: { envVars: ['DROPBOX_CLIENT_ID', 'DROPBOX_CLIENT_SECRET'], importName: 'dropbox', }, LinkedIn: { envVars: ['LINKEDIN_CLIENT_ID', 'LINKEDIN_CLIENT_SECRET'], importName: 'linkedin', }, Gitlab: { envVars: ['GITLAB_CLIENT_ID', 'GITLAB_CLIENT_SECRET', 'GITLAB_ISSUER'], importName: 'gitlab', }, Reddit: { envVars: ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET'], importName: 'reddit', }, Spotify: { envVars: ['SPOTIFY_CLIENT_ID', 'SPOTIFY_CLIENT_SECRET'], importName: 'spotify', }, }; // Function to generate a better-auth style secret function generateBetterAuthSecret(length = 32) { const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let secret = ''; const randomValues = new Uint8Array(length); crypto.getRandomValues(randomValues); for (let i = 0; i < length; i++) { secret += charset[randomValues[i] % charset.length]; } return secret; } export function showHelp() { console.log(` ${chalk.bold('Launch Express CLI - New Project')} ${chalk.dim('Create a new Launch Express Boilerplate project with custom configuration.')} ${chalk.yellow('Usage:')} npx launch-express new <project-name> [options] ${chalk.yellow('Arguments:')} project-name Name of your project directory ${chalk.yellow('Options:')} --default, -d Use default configuration: - Better Auth with Google - Stripe payments - PostgreSQL database - No analytics - Emails with Resend ${chalk.yellow('Interactive Prompts:')} 2. Auth Framework Choose your authentication framework 3. Auth Providers Select OAuth providers for your chosen framework 4. Payment Provider Choose Stripe or LemonSqueezy integration 5. Database Select from MongoDB, MySQL, PostgreSQL, or SQLite 6. Database URL Enter your database connection string 7. Analytics Choose your analytics provider 8. Email Provider Choose your email provider ${chalk.yellow('Example:')} npx launch-express new my-awesome-app npx launch-express new my-awesome-app --default `); process.exit(0); } export async function execute(projectName) { intro(chalk.inverse(' Create a project ')); // Project name handling if (!projectName) { projectName = (await text({ message: 'Enter your project name:', placeholder: 'my-awesome-app', validate: (value) => { if (!value) return 'Project name is required'; return; }, })); if (isCancel(projectName)) { outro(chalk.red('Operation cancelled')); process.exit(0); } } projectName = projectName.toLowerCase().replace(/[^a-z0-9]/g, '-'); // Check for default flag const useDefaults = process.argv.includes('--default') || process.argv.includes('-d'); let answers; if (useDefaults) { answers = { ...DEFAULT_OPTIONS, databaseUrl: '', }; console.log(chalk.magenta('\nUsing default configuration:')); console.log(chalk.yellow('- Better Auth with Google provider')); console.log(chalk.yellow('- Stripe payments')); console.log(chalk.yellow('- PostgreSQL database')); console.log(chalk.yellow('- No analytics')); console.log(chalk.dim("\nNote: You'll need to set up your database URL and the OAuth credentials later in the .env file")); } else { // Auth Framework const authFramework = (await select({ message: 'Select your authentication framework:', options: [{ value: 'better-auth', label: 'Better Auth', hint: 'Recommended' }], })); if (isCancel(authFramework)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // Auth Providers const authProviders = (await multiselect({ message: 'Select OAuth providers:', options: AUTH_PROVIDERS[authFramework].map((provider) => ({ value: provider.toLowerCase(), label: provider, })), cursorAt: 'Google', required: true, })); if (isCancel(authProviders)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // Payment Provider const paymentProvider = (await select({ message: 'Select a payment provider:', options: [ { value: 'Stripe', label: 'Stripe' }, { value: 'LemonSqueezy', label: 'LemonSqueezy' }, ], })); if (isCancel(paymentProvider)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // Database const database = (await select({ message: 'Select a database provider:', options: [ { value: 'PostgreSQL', label: 'PostgreSQL', hint: 'Recommended' }, { value: 'MongoDB', label: 'MongoDB' }, { value: 'MySQL', label: 'MySQL' }, { value: 'SQLite', label: 'SQLite' }, ], })); if (isCancel(database)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // Database URL const databaseUrl = (await text({ message: 'Enter your database connection string:', placeholder: database === 'MongoDB' ? 'mongodb://localhost:27017/mydatabase' : database === 'MySQL' ? 'mysql://user:password@localhost:3306/mydatabase' : database === 'PostgreSQL' ? 'postgresql://user:password@localhost:5432/mydatabase' : database === 'SQLite' ? 'file:./mydatabase.db' : '', validate: (value) => { if (!value) return 'Database URL is required'; return; }, })); if (isCancel(databaseUrl)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // Analytics const analytics = (await select({ message: 'Select your analytics provider: (Optional)', options: [ { value: 'None', label: 'None' }, { value: 'Google Analytics', label: 'Google Analytics' }, { value: 'Pirsch Analytics', label: 'Pirsch Analytics' }, { value: 'Plausible Analytics', label: 'Plausible Analytics' }, { value: 'PostHog Analytics', label: 'PostHog Analytics' }, { value: 'Simple Analytics', label: 'Simple Analytics' }, { value: 'Vercel Analytics', label: 'Vercel Analytics' }, ], })); if (isCancel(analytics)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // AI Provider const aiProvider = (await multiselect({ message: 'Select your AI provider: (Optional)', options: [ { value: 'Vercel AI SDK', label: 'Vercel AI SDK' }, { value: 'Hugging Face', label: 'Hugging Face' }, { value: 'Replicate', label: 'Replicate' }, { value: 'Langchain', label: 'Langchain' }, { value: 'Pinecone', label: 'Pinecone' }, ], required: false, })); if (isCancel(aiProvider)) { outro(chalk.red('Operation cancelled')); process.exit(0); } // GitHub Repository Setup const shouldSetupGithub = await select({ message: 'Would you like to initialize your project on a GitHub repository?', options: [ { value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }, ], }); if (isCancel(shouldSetupGithub)) { outro(chalk.red('Operation cancelled')); process.exit(0); } let githubUrl; if (shouldSetupGithub === 'yes') { githubUrl = (await text({ message: 'Enter your GitHub repository URL:', placeholder: 'https://github.com/username/repo', validate: (value) => { if (!value) return 'GitHub repository URL is required'; if (!value.startsWith('https://github.com/')) { return 'Please enter a valid GitHub repository URL'; } return; }, })); if (isCancel(githubUrl)) { outro(chalk.red('Operation cancelled')); process.exit(0); } } answers = { authFramework, authProviders, paymentProvider, database, databaseUrl, analytics, aiProvider, githubUrl, }; } const s = await spinner({ indicator: 'dots', }); try { // Clone the repository based on kit type and auth framework const branchFlag = answers.authFramework === 'better-auth' ? '-b main' : '-b supabase'; try { // Start spinner with initial message s.start(chalk.magenta('Creating your awesome project')); // Suppress output from git clone await execAsync(`git clone ${branchFlag} ${REPO_URL} ${projectName}`, { stdio: 'ignore' }); s.message(chalk.magenta('📦 Template downloaded successfully')); // Verify the clone was successful by checking if the directory exists and contains .git const gitDir = path.join(process.cwd(), projectName, '.git'); if (!fs.existsSync(gitDir)) { throw new Error('Git clone seems to have failed - .git directory not found'); } // Change to the project directory for git operations process.chdir(projectName); // Remove all git history and create a fresh repository s.message(chalk.magenta('🔄 Initializing fresh Git repository')); fs.rmSync('.git', { recursive: true, force: true }); await execAsync('git init', { stdio: 'ignore' }); await execAsync('git add .', { stdio: 'ignore' }); await execAsync('git commit -m "Initial commit: Project created with Launch Express CLI"', { stdio: 'ignore' }); // If GitHub URL is provided, set it up as remote and push if (answers.githubUrl) { s.message(chalk.magenta('🚀 Connecting to GitHub')); try { await execAsync(`git remote add origin ${answers.githubUrl}`, { stdio: 'ignore' }); await execAsync('git branch -M main', { stdio: 'ignore' }); s.message(chalk.magenta('⬆️ Pushing to GitHub')); await execAsync('git push -u origin main', { stdio: 'ignore' }); s.message(chalk.green('✨ GitHub repository configured successfully')); } catch (error) { s.stop(); log.warning('\nFailed to push to GitHub. You can push manually later with:'); log.message('git push -u origin main'); s.start(chalk.magenta('Continuing setup')); } } // Add upstream remote (suppress output) s.message(chalk.magenta('🔗 Configuring template updates')); try { await execAsync(`git remote add upstream ${REPO_URL}`, { stdio: 'ignore' }); } catch (error) { s.stop(); log.warning('Failed to add upstream remote'); s.start(chalk.magenta('Continuing setup')); } // Change back to original directory process.chdir('..'); s.message(chalk.magenta('⚙️ Configuring project files')); // Copy .env.example to .env await execAsync(`cp ${projectName}/.env.example ${projectName}/.env`); // get the env content let envContent = fs.readFileSync(path.join(process.cwd(), projectName, '.env'), 'utf8'); // Generate appropriate auth secret based on framework const authSecret = answers.authFramework === 'better-auth' ? generateBetterAuthSecret() : Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64'); // Remove payment provider specific variables based on selection if (answers.paymentProvider === 'LemonSqueezy') { // Remove Stripe variables if LemonSqueezy is selected envContent = envContent .replace(/## Stripe\n/g, '') .replace(/STRIPE_SECRET_KEY=.*\n/g, '') .replace(/STRIPE_WEBHOOK_SECRET=.*\n/g, ''); } else { // Remove LemonSqueezy variables if Stripe is selected envContent = envContent .replace(/## Lemonsqueezy\n/g, '') .replace(/LEMON_SQUEEZY_API_KEY=.*\n/g, '') .replace(/LEMON_SQUEEZY_WEBHOOK_SECRET=.*\n/g, ''); } // Change the .env file if (answers.authFramework === 'better-auth') { envContent = envContent.replace(/DATABASE_URL=/g, `DATABASE_URL="${answers.databaseUrl}"\n`); envContent = envContent.replace(/BETTER_AUTH_SECRET=/g, `BETTER_AUTH_SECRET="${authSecret}"\n`); // Handle Better-Auth providers const selectedProviders = answers.authProviders; // Add selected providers' environment variables let providersEnvContent = ''; // Remove google provider from envContent envContent = envContent.replace(/## Google\n(?:.*?\n)+?(?=##|$)/g, ''); selectedProviders.forEach((provider) => { const config = BETTER_AUTH_PROVIDER_CONFIG[(provider.substring(0, 1).toUpperCase() + provider.substring(1).toLowerCase())]; providersEnvContent += `## ${provider.charAt(0).toUpperCase() + provider.slice(1)}\n`; config.envVars.forEach((envVar) => { providersEnvContent += `${envVar}=\n`; }); providersEnvContent += '\n'; }); // First, remove all existing provider sections envContent = envContent.replace(/# ----------------- Better-Auth Provider -----------------[\s\S]*?(?=##)/g, '# ----------------- Better-Auth Provider -----------------\n\n' + providersEnvContent); // Update auth.ts with selected providers const authPath = path.join(process.cwd(), projectName, 'src', 'lib', 'auth', 'index.ts'); let authContent = fs.readFileSync(authPath, 'utf8'); // Remove existing OAuth provider from SocialProviders authContent = authContent.replace(/export const socialProviders: SocialProviders = \{([\s\S]*?)\};/, 'export const socialProviders: SocialProviders = {};'); // Add selected providers to the socialProviders object const providersArray = selectedProviders .map((provider) => `\n ${provider.toLowerCase()}: {\n clientId: process.env.${provider.toUpperCase()}_CLIENT_ID as string,\n clientSecret: process.env.${provider.toUpperCase()}_CLIENT_SECRET as string,\n },\n`) .join('\n '); authContent = authContent.replace(/export const socialProviders: SocialProviders = {};/, `export const socialProviders: SocialProviders = {${providersArray}};`); fs.writeFileSync(authPath, authContent); } else if (answers.authFramework === 'supabase') { // TODO: Add supabase auth log.error('Supabase auth is not yet supported'); process.exit(1); // envContent = envContent.replace(/DATABASE_URL=/g, `DATABASE_URL="${answers.databaseUrl}"\n`); } else { throw new Error('Invalid auth framework'); } // change the prisma file to the correct provider if (answers.database !== 'PostgreSQL') { fs.renameSync(path.join(process.cwd(), projectName, 'prisma', 'schema.prisma'), path.join(process.cwd(), projectName, 'prisma', 'schema.postgresql.prisma')); } if (answers.database === 'MongoDB') { fs.renameSync(path.join(process.cwd(), projectName, 'prisma', 'schema.mongodb.prisma'), path.join(process.cwd(), projectName, 'prisma', 'schema.prisma')); } else if (answers.database === 'MySQL') { fs.renameSync(path.join(process.cwd(), projectName, 'prisma', 'schema.mysql.prisma'), path.join(process.cwd(), projectName, 'prisma', 'schema.prisma')); } else if (answers.database === 'SQLite') { fs.renameSync(path.join(process.cwd(), projectName, 'prisma', 'schema.sqlite.prisma'), path.join(process.cwd(), projectName, 'prisma', 'schema.prisma')); } // Clean up any empty lines that might have been created envContent = envContent.replace(/\n\n+/g, '\n\n'); fs.writeFileSync(path.join(process.cwd(), projectName, '.env'), envContent); // Update config.ts with project name and SEO settings const configPath = path.join(process.cwd(), projectName, 'src', 'config', 'config.ts'); const configContent = fs.readFileSync(configPath, 'utf8'); // Create a title-cased version of the project name for SEO const titleCaseName = projectName .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); let updatedConfigContent = configContent // Update app name .replace(/name: ['"]My Saas['"],/g, `name: '${titleCaseName}',`) // Update SEO title .replace(/title: ['"]My Saas['"],/g, `title: '${titleCaseName}',`) // Update SEO description .replace(/description: ['"]The best Saas in the world['"],/g, `description: '${titleCaseName} - A modern web application built with Launch Express',`) // Update payment provider .replace(/paymentService: ['"]stripe['"],/g, `paymentService: '${answers.paymentProvider.toLowerCase()}',`); fs.writeFileSync(configPath, updatedConfigContent); // Update package.json with project name const projectPackageJsonPath = path.join(process.cwd(), projectName, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(projectPackageJsonPath, 'utf8')); packageJson.name = projectName; // Analytics let additionalInstallCommands = []; switch (answers.analytics) { case 'Google Analytics': // Update providers.tsx to include the analytics script const googleAnalyticsProvidersPath = path.join(process.cwd(), projectName, 'src', 'components', 'global', 'providers.tsx'); let googleAnalyticsProvidersContent = fs.readFileSync(googleAnalyticsProvidersPath, 'utf8'); // add the import statement after the last import statement googleAnalyticsProvidersContent = googleAnalyticsProvidersContent.replace(/import { QueryClient, QueryClientProvider } from '@tanstack\/react-query';/, "import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { GoogleAnalytics } from '@next/third-parties/google';\n"); // Add script to the end of the body googleAnalyticsProvidersContent = googleAnalyticsProvidersContent.replace(' {/* Analytics */}', ' {/* Analytics */} \n <GoogleAnalytics gaId={process.env.GOOGLE_ANALYTICS_ID!} />'); fs.writeFileSync(googleAnalyticsProvidersPath, googleAnalyticsProvidersContent); // Add @next/third-parties as a dependency additionalInstallCommands.push('pnpm add @next/third-parties@latest'); // Add Google Analytics environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- Analytics -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'GOOGLE_ANALYTICS_ID=\n'); // add to env.d.ts const envDtsPath = path.join(process.cwd(), projectName, 'env.d.ts'); if (fs.existsSync(envDtsPath)) { let envDtsContent = fs.readFileSync(envDtsPath, 'utf8'); // Add GOOGLE_ANALYTICS_ID to ProcessEnv interface envDtsContent = envDtsContent.replace(/interface ProcessEnv \{([\s\S]*?)\}/, 'interface ProcessEnv {$1 GOOGLE_ANALYTICS_ID?: string;\n }'); fs.writeFileSync(envDtsPath, envDtsContent); } break; case 'Pirsch Analytics': // Add async rewrites() const rewritesConfigPath = path.join(process.cwd(), projectName, 'next.config.ts'); let rewritesConfigContent = fs.readFileSync(rewritesConfigPath, 'utf8'); rewritesConfigContent = rewritesConfigContent.replace(/(\s*)(headers\s*:\s*async\s*\(\)\s*=>\s*\{)/, '$1async rewrites() {$1 return [$1 {$1 source: "/pa.js",$1 destination: "https://api.pirsch.io/pa.js",$1 },$1 ];$1},$1$2'); fs.writeFileSync(rewritesConfigPath, rewritesConfigContent); // Add pirsch script to the head inside the layout.tsx const layoutPath = path.join(process.cwd(), projectName, 'src', 'app', 'layout.tsx'); let layoutContent = fs.readFileSync(layoutPath, 'utf8'); // Add script to the end of the head layoutContent = layoutContent.replace(/<head>\s*{\s*\/\*\s*Place here your Analytics script\s*\*\/\s*}<\/head>/, '<head>\n {/* Place here your Analytics script */}\n <Script defer src="/pa.js" id="pianjs" data-code={process.env.PIRSCH_ID!}></Script>\n </head>'); fs.writeFileSync(layoutPath, layoutContent); // Add Pirsch environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- Analytics -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'PIRSCH_ID=\n'); // add to env.d.ts const pirschEnvDtsPath = path.join(process.cwd(), projectName, 'env.d.ts'); if (fs.existsSync(pirschEnvDtsPath)) { let pirschEnvDtsContent = fs.readFileSync(pirschEnvDtsPath, 'utf8'); // Add PIRSCH_ID to ProcessEnv interface pirschEnvDtsContent = pirschEnvDtsContent.replace(/interface ProcessEnv \{([\s\S]*?)\}/, 'interface ProcessEnv {$1 PIRSCH_ID?: string;\n }'); fs.writeFileSync(pirschEnvDtsPath, pirschEnvDtsContent); } break; case 'Plausible Analytics': // Add Plausible environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- Analytics -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'NEXT_PUBLIC_PLAUSIBLE_DOMAIN=\n'); // Update providers.tsx to include Plausible const plausibleProvidersPath = path.join(process.cwd(), projectName, 'src', 'components', 'global', 'providers.tsx'); let plausibleProvidersContent = fs.readFileSync(plausibleProvidersPath, 'utf8'); // Add imports plausibleProvidersContent = plausibleProvidersContent.replace(/import { QueryClient, QueryClientProvider } from '@tanstack\/react-query';/, 'import { QueryClient, QueryClientProvider } from "@tanstack/react-query";\nimport PlausibleProvider from "next-plausible";\n'); // Add PlausibleProvider wrapper plausibleProvidersContent = plausibleProvidersContent.replace('<QueryClientProvider client={queryClient}>', '<PlausibleProvider domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN!}>\n <QueryClientProvider client={queryClient}>'); plausibleProvidersContent = plausibleProvidersContent.replace('</QueryClientProvider>', ' </QueryClientProvider>\n </PlausibleProvider>'); fs.writeFileSync(plausibleProvidersPath, plausibleProvidersContent); // Add Plausible dependencies additionalInstallCommands.push('pnpm add next-plausible@latest'); // add to env.d.ts const plausibleEnvDtsPath = path.join(process.cwd(), projectName, 'env.d.ts'); if (fs.existsSync(plausibleEnvDtsPath)) { let plausibleEnvDtsContent = fs.readFileSync(plausibleEnvDtsPath, 'utf8'); plausibleEnvDtsContent = plausibleEnvDtsContent.replace(/interface ProcessEnv \{([\s\S]*?)\}/, 'interface ProcessEnv {$1 NEXT_PUBLIC_PLAUSIBLE_DOMAIN?: string;\n }'); fs.writeFileSync(plausibleEnvDtsPath, plausibleEnvDtsContent); } break; case 'PostHog Analytics': // Add PostHog environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- Analytics -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'NEXT_PUBLIC_POSTHOG_KEY=\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'NEXT_PUBLIC_POSTHOG_HOST=\n'); // Update providers.tsx to include PostHog const posthogProvidersPath = path.join(process.cwd(), projectName, 'src', 'components', 'global', 'providers.tsx'); let posthogProvidersContent = fs.readFileSync(posthogProvidersPath, 'utf8'); // Add imports posthogProvidersContent = posthogProvidersContent.replace(/import { QueryClient, QueryClientProvider } from '@tanstack\/react-query';/, 'import { QueryClient, QueryClientProvider } from "@tanstack/react-query";\nimport posthog from "posthog-js";\nimport { PostHogProvider } from "posthog-js/react";\nimport { useEffect } from "react";\n'); // Add the useEffect hook after the useTheme hook posthogProvidersContent = posthogProvidersContent.replace('const { theme } = useTheme();', 'const { theme } = useTheme();\n\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,\n capture_pageview: false // Disable automatic pageview capture, as we capture manually\n })\n }, [])'); // Add PostHogProvider wrapper posthogProvidersContent = posthogProvidersContent.replace('<QueryClientProvider client={queryClient}>', '<PostHogProvider client={posthog}>\n <QueryClientProvider client={queryClient}>'); posthogProvidersContent = posthogProvidersContent.replace('</QueryClientProvider>', ' </QueryClientProvider>\n </PostHogProvider>'); fs.writeFileSync(posthogProvidersPath, posthogProvidersContent); // Add PostHog dependencies additionalInstallCommands.push('pnpm add posthog-js@latest'); // add to env.d.ts const posthogEnvDtsPath = path.join(process.cwd(), projectName, 'env.d.ts'); if (fs.existsSync(posthogEnvDtsPath)) { let posthogEnvDtsContent = fs.readFileSync(posthogEnvDtsPath, 'utf8'); posthogEnvDtsContent = posthogEnvDtsContent.replace(/interface ProcessEnv \{([\s\S]*?)\}/, 'interface ProcessEnv {$1 NEXT_PUBLIC_POSTHOG_KEY?: string;\n NEXT_PUBLIC_POSTHOG_HOST?: string;\n }'); fs.writeFileSync(posthogEnvDtsPath, posthogEnvDtsContent); } break; case 'Simple Analytics': // Add Simple Analytics script const simpleAnalyticsProvidersPath = path.join(process.cwd(), projectName, 'next.config.ts'); let simpleAnalyticsProvidersContent = fs.readFileSync(simpleAnalyticsProvidersPath, 'utf8'); simpleAnalyticsProvidersContent = simpleAnalyticsProvidersContent.replace(/(\s*)(headers\s*:\s*async\s*\(\)\s*=>\s*\{)/, '$1async rewrites() {$1 return [$1 {$1 source: "/latest.js",$1 destination: "https://scripts.simpleanalyticscdn.com/latest.js",$1 },$1 ];$1},$1$2'); fs.writeFileSync(simpleAnalyticsProvidersPath, simpleAnalyticsProvidersContent); // Add script to the end of the body const providersPath = path.join(process.cwd(), projectName, 'src', 'components', 'global', 'providers.tsx'); let providersContent = fs.readFileSync(providersPath, 'utf8'); // add import statement providersContent = providersContent.replace(/import { QueryClient, QueryClientProvider } from '@tanstack\/react-query';/, 'import { QueryClient, QueryClientProvider } from "@tanstack/react-query";\nimport Script from "next/script";\n'); providersContent = providersContent.replace(' {/* Analytics */}', ' {/* Analytics */} \n <Script data-collect-dnt="true" async src="/latest.js"></Script>'); fs.writeFileSync(providersPath, providersContent); break; case 'Vercel Analytics': // Update providers.tsx to include the analytics script const vercelProvidersPath = path.join(process.cwd(), projectName, 'src', 'components', 'global', 'providers.tsx'); let vercelProvidersContent = fs.readFileSync(vercelProvidersPath, 'utf8'); // add import statement vercelProvidersContent = vercelProvidersContent.replace(/import { QueryClient, QueryClientProvider } from '@tanstack\/react-query';/, 'import { QueryClient, QueryClientProvider } from "@tanstack/react-query";\nimport { Analytics } from "@vercel/analytics/next";\n'); // Add script to the end of the body vercelProvidersContent = vercelProvidersContent.replace(' {/* Analytics */}', ' {/* Analytics */} \n <Analytics />'); fs.writeFileSync(vercelProvidersPath, vercelProvidersContent); // Add @vercel/analytics as a dependency additionalInstallCommands.push('pnpm add @vercel/analytics@latest'); break; default: break; } // AI Provider if (answers.aiProvider.length > 0) { answers.aiProvider.forEach((provider) => { switch (provider) { case 'Vercel AI SDK': // Add Vercel AI SDK with openai additionalInstallCommands.push('pnpm add ai@latest @ai-sdk/openai@latest'); // Add Vercel AI SDK environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- AI -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'OPENAI_API_KEY=\n'); // Create new lib file const aiDirPath = path.join(process.cwd(), projectName, 'src', 'lib', 'ai'); const libPath = path.join(aiDirPath, 'sdk.ts'); // Create the directory structure if it doesn't exist if (!fs.existsSync(aiDirPath)) { fs.mkdirSync(aiDirPath, { recursive: true }); } // Create the file if it doesn't exist if (!fs.existsSync(libPath)) { const fileContent = 'import { createOpenAI } from "@ai-sdk/openai";\n\nconst openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });\n\nexport default openai;'; fs.writeFileSync(libPath, fileContent); } break; case 'Hugging Face': // Add Hugging Face additionalInstallCommands.push('pnpm add @huggingface/inference@latest'); // Add Vercel AI SDK environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- AI -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'HUGGINGFACE_TOKEN=\n'); // Create new lib file const hfDirPath = path.join(process.cwd(), projectName, 'src', 'lib', 'ai'); const hfLibPath = path.join(hfDirPath, 'huggingface.ts'); // Create the directory structure if it doesn't exist if (!fs.existsSync(hfDirPath)) { fs.mkdirSync(hfDirPath, { recursive: true }); } // Create the file if it doesn't exist if (!fs.existsSync(hfLibPath)) { const fileContent = 'import { HfInference } from "@huggingface/inference";\n\nexport const hf = new HfInference(process.env.HUGGINGFACE_TOKEN!);'; fs.writeFileSync(hfLibPath, fileContent); } break; case 'Replicate': additionalInstallCommands.push('pnpm add replicate@latest'); // Add Vercel AI SDK environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- AI -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'REPLICATE_API_TOKEN=\n'); // Create new lib file const replicateDirPath = path.join(process.cwd(), projectName, 'src', 'lib', 'ai'); const replicateLibPath = path.join(replicateDirPath, 'replicate.ts'); // Create the directory structure if it doesn't exist if (!fs.existsSync(replicateDirPath)) { fs.mkdirSync(replicateDirPath, { recursive: true }); } // Create the file if it doesn't exist if (!fs.existsSync(replicateLibPath)) { const fileContent = 'import Replicate from "replicate";\n\nexport const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN! });'; fs.writeFileSync(replicateLibPath, fileContent); } break; case 'Langchain': additionalInstallCommands.push('pnpm add @langchain/openai@latest @langchain/core@latest'); // Add Vercel AI SDK environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- AI -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'OPENAI_API_KEY=\n'); // Create new lib file const langchainDirPath = path.join(process.cwd(), projectName, 'src', 'lib', 'ai'); const langchainLibPath = path.join(langchainDirPath, 'langchain.ts'); // Create the directory structure if it doesn't exist if (!fs.existsSync(langchainDirPath)) { fs.mkdirSync(langchainDirPath, { recursive: true }); } // Create the file if it doesn't exist if (!fs.existsSync(langchainLibPath)) { const fileContent = 'import { OpenAI } from "@langchain/openai";\n\nconst llm = new OpenAI({\n model: "gpt-3.5-turbo",\n apiKey: process.env.OPENAI_API_KEY!,\n});\n\nexport default llm;'; fs.writeFileSync(langchainLibPath, fileContent); } break; case 'Pinecone': additionalInstallCommands.push('pnpm add @pinecone-database/pinecone@latest'); // Add Pinecone environment variables fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), '\n\n# ----------------- AI -----------------\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'PINECONE_API_KEY=\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'PINECONE_ENVIRONMENT=\n'); fs.appendFileSync(path.join(process.cwd(), projectName, '.env'), 'PINECONE_INDEX=\n'); // Create new lib file const pineconeDirPath = path.join(process.cwd(), projectName, 'src', 'lib', 'ai'); const pineconeLibPath = path.join(pineconeDirPath, 'pinecone.ts'); // Create the directory structure if it doesn't exist if (!fs.existsSync(pineconeDirPath)) { fs.mkdirSync(pineconeDirPath, { recursive: true }); } // Create the file if it doesn't exist if (!fs.existsSync(pineconeLibPath)) { const fileContent = 'import { Pinecone } from "@pinecone-database/pinecone";\n\nexport const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });'; fs.writeFileSync(pineconeLibPath, fileContent); } break; default: break; } }); } // Write the updated package.json fs.writeFileSync(projectPackageJsonPath, JSON.stringify(packageJson, null, 2)); // Install dependencies s.message(chalk.magenta('📦 Checking package manager')); // Check if pnpm is installed try { await execAsync('pnpm --version', { stdio: 'ignore' }); } catch (e) { s.message(chalk.magenta('📥 Installing pnpm package manager')); await execAsync('npm install -g pnpm', { stdio: 'inherit' }); } try { // Try with pnpm first with more verbose output s.message(chalk.magenta('📦 Installing project dependencies')); try { await execAsync(`cd ${projectName} && pnpm install`); } catch (pnpmError) { s.stop(); log.error('pnpm install failed'); if (pnpmError instanceof Error) { log.error(pnpmError.message); } else { log.error(String(pnpmError)); } process.exit(1); } // Install additional packages if any if (additionalInstallCommands.length > 0) { s.message(chalk.magenta('📦 Installing additional dependencies')); for (const command of additionalInstallCommands) { try { await execAsync(`cd ${projectName} && ${command}`, { stdio: 'ignore' }); } catch (error) { if (error instanceof Error) { log.warning(error.message); } else { log.warning(String(error)); } } } } s.stop('✨ Project setup completed successfully! 🚀'); } catch (error) { s.stop(chalk.red('Installation failed.')); if (error instanceof Error) { log.error(error.message); } else { log.error(String(error)); } log.message('\nYou can try installing dependencies manually:'); log.message(`1. cd ${projectName}`); log.message('2. pnpm install'); log.message('3. pnpm dev'); process.exit(1); } // End of installation log.info(chalk.magenta('Next steps:')); log.message(`1. cd ${projectName}`); if (useDefaults) { log.message('2. Set up your database URL in .env'); log.message('3. pnpm dev'); log.message('4. Configure your Google OAuth credentials'); } else { log.message('2. pnpm dev'); log.message('3. Configure your selected OAuth providers'); } } catch (error) { s.stop(); log.error('Failed to create project'); if (error instanceof Error) { log.error(error.message); } else { log.error(String(error)); } process.exit(1); } } catch (error) { s.stop(); log.error('Failed to create project'); if (error instanceof Error) { log.error(error.message); } else { log.error(String(error)); } process.exit(1); } outro('Happy coding! 🚀'); }