launch-express
Version:
CLI tool to setup a new Launch Express project
819 lines (812 loc) • 46 kB
JavaScript
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! 🚀');
}