261 lines
8.4 KiB
JavaScript
261 lines
8.4 KiB
JavaScript
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);
|
|
}
|
|
} |