File size: 8,633 Bytes
75fefa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
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);
  }
}