import fs from 'fs-extra'; import path from 'path'; import { execSync } from 'child_process'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { getEnvPrompts } from './prompts.js'; export async function installer(config) { const { name, sandbox, path: installPath, skipInstall, dryRun, templatesDir } = config; const projectPath = path.join(installPath, name); if (dryRun) { console.log(chalk.blue('\nšŸ“‹ Dry run - would perform these actions:')); console.log(chalk.gray(` - Create directory: ${projectPath}`)); console.log(chalk.gray(` - Copy base template files`)); console.log(chalk.gray(` - Copy ${sandbox}-specific files`)); console.log(chalk.gray(` - Create .env file`)); if (!skipInstall) { console.log(chalk.gray(` - Run npm install`)); } return; } // Check if directory exists if (await fs.pathExists(projectPath)) { const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: `Directory ${name} already exists. Overwrite?`, default: false }]); if (!overwrite) { throw new Error('Installation cancelled'); } await fs.remove(projectPath); } // Create project directory await fs.ensureDir(projectPath); // Copy base template (shared files) const baseTemplatePath = path.join(templatesDir, 'base'); if (await fs.pathExists(baseTemplatePath)) { await copyTemplate(baseTemplatePath, projectPath); } else { // If no base template exists yet, copy from the main project await copyMainProject(path.dirname(templatesDir), projectPath, sandbox); } // Copy provider-specific template const providerTemplatePath = path.join(templatesDir, sandbox); if (await fs.pathExists(providerTemplatePath)) { await copyTemplate(providerTemplatePath, projectPath); } // Configure environment variables if (config.configureEnv) { const envAnswers = await inquirer.prompt(getEnvPrompts(sandbox)); await createEnvFile(projectPath, sandbox, envAnswers); } else { // Create .env.example copy await createEnvExample(projectPath, sandbox); } // Update package.json with project name await updatePackageJson(projectPath, name); // Update configuration to use the selected sandbox provider await updateAppConfig(projectPath, sandbox); // Install dependencies if (!skipInstall) { console.log(chalk.cyan('\nšŸ“¦ Installing dependencies...')); execSync('npm install', { cwd: projectPath, stdio: 'inherit' }); } } async function copyTemplate(src, dest) { const files = await fs.readdir(src); for (const file of files) { const srcPath = path.join(src, file); const destPath = path.join(dest, file); const stat = await fs.stat(srcPath); if (stat.isDirectory()) { await fs.ensureDir(destPath); await copyTemplate(srcPath, destPath); } else { await fs.copy(srcPath, destPath, { overwrite: true }); } } } async function copyMainProject(mainProjectPath, projectPath, sandbox) { // Copy essential directories and files from the main project const itemsToCopy = [ 'app', 'components', 'config', 'lib', 'types', 'public', 'styles', '.eslintrc.json', '.gitignore', 'next.config.js', 'package.json', 'tailwind.config.ts', 'tsconfig.json', 'postcss.config.mjs' ]; for (const item of itemsToCopy) { const srcPath = path.join(mainProjectPath, '..', item); const destPath = path.join(projectPath, item); if (await fs.pathExists(srcPath)) { await fs.copy(srcPath, destPath, { overwrite: true, filter: (src) => { // Skip node_modules and .next if (src.includes('node_modules') || src.includes('.next')) { return false; } return true; } }); } } } async function createEnvFile(projectPath, sandbox, answers) { let envContent = '# Open Lovable Configuration\n\n'; // Sandbox provider envContent += `# Sandbox Provider\n`; envContent += `SANDBOX_PROVIDER=${sandbox}\n\n`; // Required keys envContent += `# REQUIRED - Web scraping for cloning websites\n`; envContent += `FIRECRAWL_API_KEY=${answers.firecrawlApiKey || 'your_firecrawl_api_key_here'}\n\n`; if (sandbox === 'e2b') { envContent += `# REQUIRED - E2B Sandboxes\n`; envContent += `E2B_API_KEY=${answers.e2bApiKey || 'your_e2b_api_key_here'}\n\n`; } else if (sandbox === 'vercel') { envContent += `# REQUIRED - Vercel Sandboxes\n`; if (answers.vercelAuthMethod === 'oidc') { envContent += `# Using OIDC authentication (automatic in Vercel environment)\n`; } else { envContent += `VERCEL_TEAM_ID=${answers.vercelTeamId || 'your_team_id'}\n`; envContent += `VERCEL_PROJECT_ID=${answers.vercelProjectId || 'your_project_id'}\n`; envContent += `VERCEL_TOKEN=${answers.vercelToken || 'your_access_token'}\n`; } envContent += '\n'; } // Optional AI provider keys envContent += `# OPTIONAL - AI Providers\n`; if (answers.anthropicApiKey) { envContent += `ANTHROPIC_API_KEY=${answers.anthropicApiKey}\n`; } else { envContent += `# ANTHROPIC_API_KEY=your_anthropic_api_key_here\n`; } if (answers.openaiApiKey) { envContent += `OPENAI_API_KEY=${answers.openaiApiKey}\n`; } else { envContent += `# OPENAI_API_KEY=your_openai_api_key_here\n`; } if (answers.geminiApiKey) { envContent += `GEMINI_API_KEY=${answers.geminiApiKey}\n`; } else { envContent += `# GEMINI_API_KEY=your_gemini_api_key_here\n`; } if (answers.groqApiKey) { envContent += `GROQ_API_KEY=${answers.groqApiKey}\n`; } else { envContent += `# GROQ_API_KEY=your_groq_api_key_here\n`; } await fs.writeFile(path.join(projectPath, '.env'), envContent); await fs.writeFile(path.join(projectPath, '.env.example'), envContent.replace(/=.+/g, '=your_key_here')); } async function createEnvExample(projectPath, sandbox) { let envContent = '# Open Lovable Configuration\n\n'; envContent += `# Sandbox Provider\n`; envContent += `SANDBOX_PROVIDER=${sandbox}\n\n`; envContent += `# REQUIRED - Web scraping for cloning websites\n`; envContent += `# Get yours at https://firecrawl.dev\n`; envContent += `FIRECRAWL_API_KEY=your_firecrawl_api_key_here\n\n`; if (sandbox === 'e2b') { envContent += `# REQUIRED - Sandboxes for code execution\n`; envContent += `# Get yours at https://e2b.dev\n`; envContent += `E2B_API_KEY=your_e2b_api_key_here\n\n`; } else if (sandbox === 'vercel') { envContent += `# REQUIRED - Vercel Sandboxes\n`; envContent += `# Option 1: OIDC (automatic in Vercel environment)\n`; envContent += `# Option 2: Personal Access Token\n`; envContent += `VERCEL_TEAM_ID=your_team_id\n`; envContent += `VERCEL_PROJECT_ID=your_project_id\n`; envContent += `VERCEL_TOKEN=your_access_token\n\n`; } envContent += `# OPTIONAL - AI Providers (need at least one)\n`; envContent += `# Get yours at https://console.anthropic.com\n`; envContent += `ANTHROPIC_API_KEY=your_anthropic_api_key_here\n\n`; envContent += `# Get yours at https://platform.openai.com\n`; envContent += `OPENAI_API_KEY=your_openai_api_key_here\n\n`; envContent += `# Get yours at https://aistudio.google.com/app/apikey\n`; envContent += `GEMINI_API_KEY=your_gemini_api_key_here\n\n`; envContent += `# Get yours at https://console.groq.com\n`; envContent += `GROQ_API_KEY=your_groq_api_key_here\n`; await fs.writeFile(path.join(projectPath, '.env.example'), envContent); } async function updatePackageJson(projectPath, name) { const packageJsonPath = path.join(projectPath, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); packageJson.name = name; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } } async function updateAppConfig(projectPath, sandbox) { const configPath = path.join(projectPath, 'config', 'app.config.ts'); if (await fs.pathExists(configPath)) { let content = await fs.readFile(configPath, 'utf-8'); // Add sandbox provider configuration const sandboxConfig = ` // Sandbox Provider Configuration sandboxProvider: process.env.SANDBOX_PROVIDER || '${sandbox}', `; // Insert after the opening of appConfig content = content.replace( 'export const appConfig = {', `export const appConfig = {${sandboxConfig}` ); await fs.writeFile(configPath, content); } }