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
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import path from 'path';
import { fileURLToPath } from 'url';
import { installer } from './lib/installer.js';
import { getPrompts } from './lib/prompts.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const program = new Command();
program
.name('create-open-lovable')
.description('Create a new Open Lovable project with your choice of sandbox provider')
.version('1.0.0')
.option('-s, --sandbox <provider>', 'Sandbox provider (e2b or vercel)')
.option('-n, --name <name>', 'Project name')
.option('-p, --path <path>', 'Installation path (defaults to current directory)')
.option('--skip-install', 'Skip npm install')
.option('--dry-run', 'Run without making changes')
.parse(process.argv);
const options = program.opts();
async function main() {
console.log(chalk.cyan('\n🚀 Welcome to Open Lovable Setup!\n'));
let config = {
sandbox: options.sandbox,
name: options.name || 'my-open-lovable',
path: options.path || process.cwd(),
skipInstall: options.skipInstall || false,
dryRun: options.dryRun || false
};
// Interactive mode if sandbox not specified
if (!config.sandbox) {
const prompts = getPrompts(config);
const answers = await inquirer.prompt(prompts);
config = { ...config, ...answers };
}
// Validate sandbox provider
if (!['e2b', 'vercel'].includes(config.sandbox)) {
console.error(chalk.red(`\n❌ Invalid sandbox provider: ${config.sandbox}`));
console.log(chalk.yellow('Valid options: e2b, vercel\n'));
process.exit(1);
}
console.log(chalk.gray('\nConfiguration:'));
console.log(chalk.gray(` Project: ${config.name}`));
console.log(chalk.gray(` Sandbox: ${config.sandbox}`));
console.log(chalk.gray(` Path: ${path.resolve(config.path, config.name)}\n`));
if (config.dryRun) {
console.log(chalk.yellow('🔍 Dry run mode - no files will be created\n'));
}
const spinner = ora('Creating project...').start();
try {
await installer({
...config,
templatesDir: path.join(__dirname, 'templates')
});
spinner.succeed('Project created successfully!');
console.log(chalk.green('\n✅ Setup complete!\n'));
console.log(chalk.white('Next steps:'));
console.log(chalk.gray(` 1. cd ${config.name}`));
console.log(chalk.gray(` 2. Copy .env.example to .env and add your API keys`));
console.log(chalk.gray(` 3. npm run dev`));
console.log(chalk.gray('\nHappy coding! 🎉\n'));
} catch (error) {
spinner.fail('Setup failed');
console.error(chalk.red('\n❌ Error:'), error.message);
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
main().catch(error => {
console.error(chalk.red('Unexpected error:'), error);
process.exit(1);
});
@@ -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;
}
+36
View File
@@ -0,0 +1,36 @@
{
"name": "create-open-lovable",
"version": "1.0.0",
"description": "CLI to bootstrap Open Lovable with your choice of sandbox provider",
"type": "module",
"main": "index.js",
"bin": {
"create-open-lovable": "./index.js"
},
"scripts": {
"test": "node index.js --dry-run"
},
"keywords": [
"lovable",
"sandbox",
"e2b",
"vercel",
"ai",
"code-generation"
],
"author": "",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^11.1.0",
"fs-extra": "^11.2.0",
"inquirer": "^9.2.12",
"ora": "^7.0.1"
},
"engines": {
"node": ">=16.0.0"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,25 @@
# Open Lovable Configuration - E2B Provider
# Sandbox Provider
SANDBOX_PROVIDER=e2b
# REQUIRED - Sandboxes for code execution
# Get yours at https://e2b.dev
E2B_API_KEY=your_e2b_api_key_here
# REQUIRED - Web scraping for cloning websites
# Get yours at https://firecrawl.dev
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
# OPTIONAL - AI Providers (need at least one)
# Get yours at https://console.anthropic.com
ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Get yours at https://platform.openai.com
OPENAI_API_KEY=your_openai_api_key_here
# Get yours at https://aistudio.google.com/app/apikey
GEMINI_API_KEY=your_gemini_api_key_here
# Get yours at https://console.groq.com
GROQ_API_KEY=your_groq_api_key_here
@@ -0,0 +1,38 @@
# Open Lovable - E2B Sandbox
This project is configured to use E2B sandboxes for code execution.
## Setup
1. Get your E2B API key from [https://e2b.dev](https://e2b.dev)
2. Get your Firecrawl API key from [https://firecrawl.dev](https://firecrawl.dev)
3. Copy `.env.example` to `.env` and add your API keys
4. Run `npm install` to install dependencies
5. Run `npm run dev` to start the development server
## E2B Features
- Full-featured development sandboxes
- 15-minute default timeout (configurable)
- Persistent file system during session
- Support for complex package installations
- Built-in Python runtime for code execution
## Configuration
You can adjust E2B settings in `config/app.config.ts`:
- `timeoutMinutes`: Sandbox session timeout (default: 15)
- `vitePort`: Development server port (default: 5173)
- `viteStartupDelay`: Time to wait for Vite to start (default: 7000ms)
## Troubleshooting
If you encounter issues:
1. Verify your E2B API key is valid
2. Check the console for detailed error messages
3. Ensure you have a stable internet connection
4. Try refreshing the page and creating a new sandbox
For more help, visit the [E2B documentation](https://docs.e2b.dev).
@@ -0,0 +1,28 @@
# Open Lovable Configuration - Vercel Provider
# Sandbox Provider
SANDBOX_PROVIDER=vercel
# REQUIRED - Vercel Sandboxes
# Option 1: OIDC Token (automatic in Vercel environment)
# Option 2: Personal Access Token (configure below)
VERCEL_TEAM_ID=your_team_id
VERCEL_PROJECT_ID=your_project_id
VERCEL_TOKEN=your_access_token
# REQUIRED - Web scraping for cloning websites
# Get yours at https://firecrawl.dev
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
# OPTIONAL - AI Providers (need at least one)
# Get yours at https://console.anthropic.com
ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Get yours at https://platform.openai.com
OPENAI_API_KEY=your_openai_api_key_here
# Get yours at https://aistudio.google.com/app/apikey
GEMINI_API_KEY=your_gemini_api_key_here
# Get yours at https://console.groq.com
GROQ_API_KEY=your_groq_api_key_here
@@ -0,0 +1,52 @@
# Open Lovable - Vercel Sandbox
This project is configured to use Vercel Sandboxes for code execution.
## Setup
1. Configure Vercel authentication (see below)
2. Get your Firecrawl API key from [https://firecrawl.dev](https://firecrawl.dev)
3. Copy `.env.example` to `.env` and add your credentials
4. Run `npm install` to install dependencies
5. Run `npm run dev` to start the development server
## Vercel Authentication
### Option 1: OIDC Token (Recommended for Vercel deployments)
When running in a Vercel environment, authentication happens automatically via OIDC tokens. No configuration needed!
### Option 2: Personal Access Token (For local development)
1. Create a Personal Access Token in your [Vercel account settings](https://vercel.com/account/tokens)
2. Get your Team ID from your [team settings](https://vercel.com/teams)
3. Create a project and get the Project ID
4. Add these to your `.env` file:
- `VERCEL_TOKEN`
- `VERCEL_TEAM_ID`
- `VERCEL_PROJECT_ID`
## Vercel Sandbox Features
- Lightweight ephemeral Linux VMs
- Powered by Firecracker MicroVMs
- 5-minute default timeout (max 45 minutes)
- 8 vCPUs maximum
- Root access for package installation
- Node 22 runtime included
## Configuration
You can adjust Vercel settings in `config/app.config.ts`:
- `maxDuration`: Sandbox session timeout (default: 5 minutes)
- Authentication method (OIDC or PAT)
## Troubleshooting
If you encounter issues:
1. Verify your authentication credentials
2. Check if you're using the correct authentication method
3. Ensure your Vercel account has sandbox access
4. Check the console for detailed error messages
For more help, visit the [Vercel Sandbox documentation](https://vercel.com/docs/vercel-sandbox).