Spaces:
Build error
Build error
| 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); | |
| } | |
| } |