Add v2 sandbox implementation with new API routes and sandbox library

This commit is contained in:
Developers Digest
2025-09-02 19:14:27 -04:00
parent d7ae41ba9d
commit dbf34e2d63
15 changed files with 1978 additions and 0 deletions
@@ -0,0 +1,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);
}
}
+190
View File
@@ -0,0 +1,190 @@
export function getPrompts(config) {
const prompts = [];
if (!config.name) {
prompts.push({
type: 'input',
name: 'name',
message: 'Project name:',
default: 'my-open-lovable',
validate: (input) => {
if (!input || input.trim() === '') {
return 'Project name is required';
}
if (!/^[a-z0-9-_]+$/i.test(input)) {
return 'Project name can only contain letters, numbers, hyphens, and underscores';
}
return true;
}
});
}
if (!config.sandbox) {
prompts.push({
type: 'list',
name: 'sandbox',
message: 'Choose your sandbox provider:',
choices: [
{
name: 'E2B - Full-featured development sandboxes',
value: 'e2b',
short: 'E2B'
},
{
name: 'Vercel - Lightweight ephemeral VMs',
value: 'vercel',
short: 'Vercel'
}
],
default: 'e2b'
});
}
prompts.push({
type: 'confirm',
name: 'configureEnv',
message: 'Would you like to configure API keys now?',
default: true
});
return prompts;
}
export function getEnvPrompts(provider) {
const prompts = [];
// Always include Firecrawl API key
prompts.push({
type: 'input',
name: 'firecrawlApiKey',
message: 'Firecrawl API key (for web scraping):',
validate: (input) => {
if (!input || input.trim() === '') {
return 'Firecrawl API key is required for web scraping functionality';
}
return true;
}
});
if (provider === 'e2b') {
prompts.push({
type: 'input',
name: 'e2bApiKey',
message: 'E2B API key:',
validate: (input) => {
if (!input || input.trim() === '') {
return 'E2B API key is required';
}
return true;
}
});
} else if (provider === 'vercel') {
prompts.push({
type: 'list',
name: 'vercelAuthMethod',
message: 'Vercel authentication method:',
choices: [
{
name: 'OIDC Token (automatic in Vercel environment)',
value: 'oidc',
short: 'OIDC'
},
{
name: 'Personal Access Token',
value: 'pat',
short: 'PAT'
}
]
});
prompts.push({
type: 'input',
name: 'vercelTeamId',
message: 'Vercel Team ID:',
when: (answers) => answers.vercelAuthMethod === 'pat',
validate: (input) => {
if (!input || input.trim() === '') {
return 'Team ID is required for PAT authentication';
}
return true;
}
});
prompts.push({
type: 'input',
name: 'vercelProjectId',
message: 'Vercel Project ID:',
when: (answers) => answers.vercelAuthMethod === 'pat',
validate: (input) => {
if (!input || input.trim() === '') {
return 'Project ID is required for PAT authentication';
}
return true;
}
});
prompts.push({
type: 'input',
name: 'vercelToken',
message: 'Vercel Access Token:',
when: (answers) => answers.vercelAuthMethod === 'pat',
validate: (input) => {
if (!input || input.trim() === '') {
return 'Access token is required for PAT authentication';
}
return true;
}
});
}
// Optional AI provider keys
prompts.push({
type: 'confirm',
name: 'addAiKeys',
message: 'Would you like to add AI provider API keys?',
default: true
});
prompts.push({
type: 'checkbox',
name: 'aiProviders',
message: 'Select AI providers to configure:',
when: (answers) => answers.addAiKeys,
choices: [
{ name: 'Anthropic (Claude)', value: 'anthropic' },
{ name: 'OpenAI (GPT)', value: 'openai' },
{ name: 'Google (Gemini)', value: 'gemini' },
{ name: 'Groq', value: 'groq' }
]
});
prompts.push({
type: 'input',
name: 'anthropicApiKey',
message: 'Anthropic API key:',
when: (answers) => answers.aiProviders && answers.aiProviders.includes('anthropic')
});
prompts.push({
type: 'input',
name: 'openaiApiKey',
message: 'OpenAI API key:',
when: (answers) => answers.aiProviders && answers.aiProviders.includes('openai')
});
prompts.push({
type: 'input',
name: 'geminiApiKey',
message: 'Gemini API key:',
when: (answers) => answers.aiProviders && answers.aiProviders.includes('gemini')
});
prompts.push({
type: 'input',
name: 'groqApiKey',
message: 'Groq API key:',
when: (answers) => answers.aiProviders && answers.aiProviders.includes('groq')
});
return prompts;
}