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
+95
View File
@@ -0,0 +1,95 @@
import { NextResponse } from 'next/server';
import { SandboxFactory } from '@/lib/sandbox/factory';
import { SandboxProvider } from '@/lib/sandbox/types';
import type { SandboxState } from '@/types/sandbox';
// Store active sandbox globally
declare global {
var activeSandboxProvider: SandboxProvider | null;
var sandboxData: any;
var existingFiles: Set<string>;
var sandboxState: SandboxState;
}
export async function POST() {
try {
console.log('[create-ai-sandbox-v2] Creating sandbox...');
// Clean up existing sandbox if any
if (global.activeSandboxProvider) {
console.log('[create-ai-sandbox-v2] Terminating existing sandbox...');
try {
await global.activeSandboxProvider.terminate();
} catch (e) {
console.error('Failed to terminate existing sandbox:', e);
}
global.activeSandboxProvider = null;
}
// Clear existing files tracking
if (global.existingFiles) {
global.existingFiles.clear();
} else {
global.existingFiles = new Set<string>();
}
// Create new sandbox using factory
const provider = SandboxFactory.create();
const sandboxInfo = await provider.createSandbox();
console.log('[create-ai-sandbox-v2] Setting up Vite React app...');
await provider.setupViteApp();
// Store provider globally
global.activeSandboxProvider = provider;
global.sandboxData = {
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url
};
// Initialize sandbox state
global.sandboxState = {
fileCache: {
files: {},
lastSync: Date.now(),
sandboxId: sandboxInfo.sandboxId
},
sandbox: provider, // Store the provider instead of raw sandbox
sandboxData: {
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url
}
};
console.log('[create-ai-sandbox-v2] Sandbox ready at:', sandboxInfo.url);
return NextResponse.json({
success: true,
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url,
provider: sandboxInfo.provider,
message: 'Sandbox created and Vite React app initialized'
});
} catch (error) {
console.error('[create-ai-sandbox-v2] Error:', error);
// Clean up on error
if (global.activeSandboxProvider) {
try {
await global.activeSandboxProvider.terminate();
} catch (e) {
console.error('Failed to terminate sandbox on error:', e);
}
global.activeSandboxProvider = null;
}
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Failed to create sandbox',
details: error instanceof Error ? error.stack : undefined
},
{ status: 500 }
);
}
}
+44
View File
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types';
declare global {
var activeSandboxProvider: SandboxProvider | null;
}
export async function POST(request: NextRequest) {
try {
const { packages } = await request.json();
if (!packages || !Array.isArray(packages) || packages.length === 0) {
return NextResponse.json({
success: false,
error: 'Packages array is required'
}, { status: 400 });
}
if (!global.activeSandboxProvider) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`);
const result = await global.activeSandboxProvider.installPackages(packages);
return NextResponse.json({
success: result.success,
output: result.stdout,
error: result.stderr,
message: result.success ? 'Packages installed successfully' : 'Package installation failed'
});
} catch (error) {
console.error('[install-packages-v2] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+46
View File
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types';
// Get active sandbox provider from global state
declare global {
var activeSandboxProvider: SandboxProvider | null;
}
export async function POST(request: NextRequest) {
try {
const { command } = await request.json();
if (!command) {
return NextResponse.json({
success: false,
error: 'Command is required'
}, { status: 400 });
}
if (!global.activeSandboxProvider) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 400 });
}
console.log(`[run-command-v2] Executing: ${command}`);
const result = await global.activeSandboxProvider.runCommand(command);
return NextResponse.json({
success: result.success,
output: result.stdout,
error: result.stderr,
exitCode: result.exitCode,
message: result.success ? 'Command executed successfully' : 'Command failed'
});
} catch (error) {
console.error('[run-command-v2] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}
+42
View File
@@ -0,0 +1,42 @@
import { SandboxProvider, SandboxProviderConfig } from './types';
import { E2BProvider } from './providers/e2b-provider';
import { VercelProvider } from './providers/vercel-provider';
export class SandboxFactory {
static create(provider?: string, config?: SandboxProviderConfig): SandboxProvider {
// Use environment variable if provider not specified
const selectedProvider = provider || process.env.SANDBOX_PROVIDER || 'e2b';
console.log(`[SandboxFactory] Creating ${selectedProvider} provider`);
switch (selectedProvider.toLowerCase()) {
case 'e2b':
return new E2BProvider(config || {});
case 'vercel':
return new VercelProvider(config || {});
default:
throw new Error(`Unknown sandbox provider: ${selectedProvider}. Supported providers: e2b, vercel`);
}
}
static getAvailableProviders(): string[] {
return ['e2b', 'vercel'];
}
static isProviderAvailable(provider: string): boolean {
switch (provider.toLowerCase()) {
case 'e2b':
return !!process.env.E2B_API_KEY;
case 'vercel':
// Vercel can use OIDC (automatic) or PAT
return !!process.env.VERCEL_OIDC_TOKEN ||
(!!process.env.VERCEL_TOKEN && !!process.env.VERCEL_TEAM_ID && !!process.env.VERCEL_PROJECT_ID);
default:
return false;
}
}
}
+494
View File
@@ -0,0 +1,494 @@
import { Sandbox } from '@e2b/code-interpreter';
import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types';
import { appConfig } from '@/config/app.config';
export class E2BProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set();
async createSandbox(): Promise<SandboxInfo> {
try {
console.log('[E2BProvider] Creating sandbox...');
// Kill existing sandbox if any
if (this.sandbox) {
console.log('[E2BProvider] Killing existing sandbox...');
try {
await this.sandbox.kill();
} catch (e) {
console.error('Failed to close existing sandbox:', e);
}
this.sandbox = null;
}
// Clear existing files tracking
this.existingFiles.clear();
// Create base sandbox
console.log(`[E2BProvider] Creating E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
this.sandbox = await Sandbox.create({
apiKey: this.config.e2b?.apiKey || process.env.E2B_API_KEY,
timeoutMs: this.config.e2b?.timeoutMs || appConfig.e2b.timeoutMs
});
const sandboxId = (this.sandbox as any).sandboxId || Date.now().toString();
const host = (this.sandbox as any).getHost(appConfig.e2b.vitePort);
console.log(`[E2BProvider] Sandbox created: ${sandboxId}`);
console.log(`[E2BProvider] Sandbox host: ${host}`);
this.sandboxInfo = {
sandboxId,
url: `https://${host}`,
provider: 'e2b',
createdAt: new Date()
};
// Set extended timeout on the sandbox instance if method available
if (typeof this.sandbox.setTimeout === 'function') {
this.sandbox.setTimeout(appConfig.e2b.timeoutMs);
console.log(`[E2BProvider] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`);
}
return this.sandboxInfo;
} catch (error) {
console.error('[E2BProvider] Error creating sandbox:', error);
throw error;
}
}
async runCommand(command: string): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log(`[E2BProvider] Executing: ${command}`);
const result = await this.sandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
result = subprocess.run(${JSON.stringify(command.split(' '))},
capture_output=True,
text=True,
shell=False)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n');
const stderr = result.logs.stderr.join('\n');
return {
stdout: output,
stderr,
exitCode: result.error ? 1 : 0,
success: !result.error
};
}
async writeFile(path: string, content: string): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
await this.sandbox.runCode(`
import os
# Ensure directory exists
dir_path = os.path.dirname("${fullPath}")
os.makedirs(dir_path, exist_ok=True)
# Write file
with open("${fullPath}", 'w') as f:
f.write(${JSON.stringify(content)})
print(f"✓ Written: ${fullPath}")
`);
this.existingFiles.add(path);
}
async readFile(path: string): Promise<string> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
const result = await this.sandbox.runCode(`
with open("${fullPath}", 'r') as f:
content = f.read()
print(content)
`);
return result.logs.stdout.join('\n');
}
async listFiles(directory: string = '/home/user/app'): Promise<string[]> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCode(`
import os
import json
def list_files(path):
files = []
for root, dirs, filenames in os.walk(path):
# Skip node_modules and .git
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist', 'build']]
for filename in filenames:
rel_path = os.path.relpath(os.path.join(root, filename), path)
files.append(rel_path)
return files
files = list_files("${directory}")
print(json.dumps(files))
`);
try {
return JSON.parse(result.logs.stdout.join(''));
} catch {
return [];
}
}
async installPackages(packages: string[]): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const packageList = packages.join(' ');
const flags = appConfig.packages.useLegacyPeerDeps ? '--legacy-peer-deps' : '';
console.log(`[E2BProvider] Installing packages: ${packageList}`);
const result = await this.sandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
# Install packages
result = subprocess.run(
['npm', 'install', ${flags ? `'${flags}',` : ''} ${packages.map(p => `'${p}'`).join(', ')}],
capture_output=True,
text=True
)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n');
const stderr = result.logs.stderr.join('\n');
// Restart Vite if configured
if (appConfig.packages.autoRestartVite && !result.error) {
await this.restartViteServer();
}
return {
stdout: output,
stderr,
exitCode: result.error ? 1 : 0,
success: !result.error
};
}
async setupViteApp(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log('[E2BProvider] Setting up Vite React app...');
// Write all files in a single Python script
const setupScript = `
import os
import json
print('Setting up React app with Vite and Tailwind...')
# Create directory structure
os.makedirs('/home/user/app/src', exist_ok=True)
# Package.json
package_json = {
"name": "sandbox-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.3.9",
"tailwindcss": "^3.3.0",
"postcss": "^8.4.31",
"autoprefixer": "^10.4.16"
}
}
with open('/home/user/app/package.json', 'w') as f:
json.dump(package_json, f, indent=2)
print('✓ package.json')
# Vite config
vite_config = """import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: false,
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
}
})"""
with open('/home/user/app/vite.config.js', 'w') as f:
f.write(vite_config)
print('✓ vite.config.js')
# Tailwind config
tailwind_config = """/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}"""
with open('/home/user/app/tailwind.config.js', 'w') as f:
f.write(tailwind_config)
print('✓ tailwind.config.js')
# PostCSS config
postcss_config = """export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}"""
with open('/home/user/app/postcss.config.js', 'w') as f:
f.write(postcss_config)
print('✓ postcss.config.js')
# Index.html
index_html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>"""
with open('/home/user/app/index.html', 'w') as f:
f.write(index_html)
print('✓ index.html')
# Main.jsx
main_jsx = """import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)"""
with open('/home/user/app/src/main.jsx', 'w') as f:
f.write(main_jsx)
print('✓ src/main.jsx')
# App.jsx
app_jsx = """function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App"""
with open('/home/user/app/src/App.jsx', 'w') as f:
f.write(app_jsx)
print('✓ src/App.jsx')
# Index.css
index_css = """@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}"""
with open('/home/user/app/src/index.css', 'w') as f:
f.write(index_css)
print('✓ src/index.css')
print('\\nAll files created successfully!')
`;
await this.sandbox.runCode(setupScript);
// Install dependencies
console.log('[E2BProvider] Installing dependencies...');
await this.sandbox.runCode(`
import subprocess
print('Installing npm packages...')
result = subprocess.run(
['npm', 'install'],
cwd='/home/user/app',
capture_output=True,
text=True
)
if result.returncode == 0:
print('✓ Dependencies installed successfully')
else:
print(f'⚠ Warning: npm install had issues: {result.stderr}')
`);
// Start Vite dev server
console.log('[E2BProvider] Starting Vite dev server...');
await this.sandbox.runCode(`
import subprocess
import os
import time
os.chdir('/home/user/app')
# Kill any existing Vite processes
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(1)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite dev server started with PID: {process.pid}')
print('Waiting for server to be ready...')
`);
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
// Track initial files
this.existingFiles.add('src/App.jsx');
this.existingFiles.add('src/main.jsx');
this.existingFiles.add('src/index.css');
this.existingFiles.add('index.html');
this.existingFiles.add('package.json');
this.existingFiles.add('vite.config.js');
this.existingFiles.add('tailwind.config.js');
this.existingFiles.add('postcss.config.js');
}
async restartViteServer(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log('[E2BProvider] Restarting Vite server...');
await this.sandbox.runCode(`
import subprocess
import time
import os
os.chdir('/home/user/app')
# Kill existing Vite process
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(2)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite restarted with PID: {process.pid}')
`);
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
}
getSandboxUrl(): string | null {
return this.sandboxInfo?.url || null;
}
async terminate(): Promise<void> {
if (this.sandbox) {
console.log('[E2BProvider] Terminating sandbox...');
try {
await this.sandbox.kill();
} catch (e) {
console.error('Failed to terminate sandbox:', e);
}
this.sandbox = null;
this.sandboxInfo = null;
}
}
isAlive(): boolean {
return !!this.sandbox;
}
}
+469
View File
@@ -0,0 +1,469 @@
import { Sandbox } from '@vercel/sandbox';
import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types';
export class VercelProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set();
async createSandbox(): Promise<SandboxInfo> {
try {
console.log('[VercelProvider] Creating sandbox...');
// Kill existing sandbox if any
if (this.sandbox) {
console.log('[VercelProvider] Stopping existing sandbox...');
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to stop existing sandbox:', e);
}
this.sandbox = null;
}
// Clear existing files tracking
this.existingFiles.clear();
// Create Vercel sandbox
console.log('[VercelProvider] Creating Vercel sandbox...');
const sandboxConfig: any = {
timeout: 300000, // 5 minutes in ms
runtime: 'node22', // Use node22 runtime for Vercel sandboxes
ports: [5173] // Vite port
};
// Add authentication based on environment variables
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
console.log('[VercelProvider] Using personal access token authentication');
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
sandboxConfig.token = process.env.VERCEL_TOKEN;
} else if (process.env.VERCEL_OIDC_TOKEN) {
console.log('[VercelProvider] Using OIDC token authentication');
} else {
console.log('[VercelProvider] No authentication found - relying on default Vercel authentication');
}
this.sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = this.sandbox.sandboxId;
console.log(`[VercelProvider] Sandbox created: ${sandboxId}`);
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = this.sandbox.domain(5173);
console.log(`[VercelProvider] Sandbox URL: ${sandboxUrl}`);
this.sandboxInfo = {
sandboxId,
url: sandboxUrl,
provider: 'vercel',
createdAt: new Date()
};
return this.sandboxInfo;
} catch (error) {
console.error('[VercelProvider] Error creating sandbox:', error);
throw error;
}
}
async runCommand(command: string): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log(`[VercelProvider] Executing: ${command}`);
try {
// Parse command into cmd and args (matching PR syntax)
const parts = command.split(' ');
const cmd = parts[0];
const args = parts.slice(1);
// Vercel uses runCommand with cmd and args object (based on PR)
const result = await this.sandbox.runCommand({
cmd: cmd,
args: args,
cwd: '/app',
env: {}
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
} catch (error: any) {
return {
stdout: '',
stderr: error.message || 'Command failed',
exitCode: 1,
success: false
};
}
}
async writeFile(path: string, content: string): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/app/${path}`;
// Based on PR, Vercel SDK has a writeFiles method that takes an array
try {
await this.sandbox.writeFiles([{
path: fullPath,
content: Buffer.from(content)
}]);
console.log(`[VercelProvider] Written: ${fullPath}`);
this.existingFiles.add(path);
} catch (error) {
// Fallback to command-based approach if writeFiles is not available
console.log(`[VercelProvider] writeFiles failed, using command fallback`);
// Ensure directory exists
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', dir],
cwd: '/'
});
// Write file using echo and redirection
const escapedContent = content
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`')
.replace(/\n/g, '\\n');
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `echo "${escapedContent}" > ${fullPath}`],
cwd: '/'
});
console.log(`[VercelProvider] Written via command: ${fullPath}`);
this.existingFiles.add(path);
}
}
async readFile(path: string): Promise<string> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/app/${path}`;
const result = await this.sandbox.runCommand({
cmd: 'cat',
args: [fullPath],
cwd: '/'
});
if (result.exitCode !== 0) {
throw new Error(`Failed to read file: ${result.stderr}`);
}
return result.stdout || '';
}
async listFiles(directory: string = '/app'): Promise<string[]> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`],
cwd: '/'
});
if (result.exitCode !== 0) {
return [];
}
return (result.stdout || '').split('\n').filter((line: string) => line.trim() !== '');
}
async installPackages(packages: string[]): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const flags = process.env.NPM_FLAGS || '';
console.log(`[VercelProvider] Installing packages: ${packages.join(' ')}`);
// Build args array
const args = ['install'];
if (flags) {
args.push(...flags.split(' '));
}
args.push(...packages);
const result = await this.sandbox.runCommand({
cmd: 'npm',
args: args,
cwd: '/app'
});
// Restart Vite if configured and successful
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
await this.restartViteServer();
}
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
}
async setupViteApp(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log('[VercelProvider] Setting up Vite React app...');
// Create directory structure
await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', '/app/src'],
cwd: '/'
});
// Create package.json
const packageJson = {
name: "sandbox-app",
version: "1.0.0",
type: "module",
scripts: {
dev: "vite --host",
build: "vite build",
preview: "vite preview"
},
dependencies: {
react: "^18.2.0",
"react-dom": "^18.2.0"
},
devDependencies: {
"@vitejs/plugin-react": "^4.0.0",
vite: "^4.3.9",
tailwindcss: "^3.3.0",
postcss: "^8.4.31",
autoprefixer: "^10.4.16"
}
};
await this.writeFile('package.json', JSON.stringify(packageJson, null, 2));
// Create vite.config.js
const viteConfig = `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: {
clientPort: 443,
protocol: 'wss'
}
}
})`;
await this.writeFile('vite.config.js', viteConfig);
// Create tailwind.config.js
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}`;
await this.writeFile('tailwind.config.js', tailwindConfig);
// Create postcss.config.js
const postcssConfig = `export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}`;
await this.writeFile('postcss.config.js', postcssConfig);
// Create index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>`;
await this.writeFile('index.html', indexHtml);
// Create src/main.jsx
const mainJsx = `import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)`;
await this.writeFile('src/main.jsx', mainJsx);
// Create src/App.jsx
const appJsx = `function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Vercel Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App`;
await this.writeFile('src/App.jsx', appJsx);
// Create src/index.css
const indexCss = `@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}`;
await this.writeFile('src/index.css', indexCss);
console.log('[VercelProvider] All files created successfully!');
// Install dependencies
console.log('[VercelProvider] Installing dependencies...');
const installResult = await this.sandbox.runCommand({
cmd: 'npm',
args: ['install'],
cwd: '/app'
});
if (installResult.exitCode === 0) {
console.log('[VercelProvider] Dependencies installed successfully');
} else {
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
}
// Start Vite dev server
console.log('[VercelProvider] Starting Vite dev server...');
// Kill any existing Vite processes
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/app'
});
console.log('[VercelProvider] Vite dev server started');
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
// Track initial files
this.existingFiles.add('src/App.jsx');
this.existingFiles.add('src/main.jsx');
this.existingFiles.add('src/index.css');
this.existingFiles.add('index.html');
this.existingFiles.add('package.json');
this.existingFiles.add('vite.config.js');
this.existingFiles.add('tailwind.config.js');
this.existingFiles.add('postcss.config.js');
}
async restartViteServer(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
console.log('[VercelProvider] Restarting Vite server...');
// Kill existing Vite process
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/app'
});
console.log('[VercelProvider] Vite restarted');
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
}
getSandboxUrl(): string | null {
return this.sandboxInfo?.url || null;
}
async terminate(): Promise<void> {
if (this.sandbox) {
console.log('[VercelProvider] Terminating sandbox...');
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to terminate sandbox:', e);
}
this.sandbox = null;
this.sandboxInfo = null;
}
}
isAlive(): boolean {
return !!this.sandbox;
}
}
+64
View File
@@ -0,0 +1,64 @@
export interface SandboxFile {
path: string;
content: string;
lastModified?: number;
}
export interface SandboxInfo {
sandboxId: string;
url: string;
provider: 'e2b' | 'vercel';
createdAt: Date;
}
export interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
success: boolean;
}
export interface SandboxProviderConfig {
e2b?: {
apiKey: string;
timeoutMs?: number;
template?: string;
};
vercel?: {
teamId?: string;
projectId?: string;
token?: string;
authMethod?: 'oidc' | 'pat';
};
}
export abstract class SandboxProvider {
protected config: SandboxProviderConfig;
protected sandbox: any;
protected sandboxInfo: SandboxInfo | null = null;
constructor(config: SandboxProviderConfig) {
this.config = config;
}
abstract createSandbox(): Promise<SandboxInfo>;
abstract runCommand(command: string): Promise<CommandResult>;
abstract writeFile(path: string, content: string): Promise<void>;
abstract readFile(path: string): Promise<string>;
abstract listFiles(directory?: string): Promise<string[]>;
abstract installPackages(packages: string[]): Promise<CommandResult>;
abstract getSandboxUrl(): string | null;
abstract terminate(): Promise<void>;
abstract isAlive(): boolean;
// Optional methods that providers can override
async setupViteApp(): Promise<void> {
// Default implementation for setting up a Vite React app
throw new Error('setupViteApp not implemented for this provider');
}
async restartViteServer(): Promise<void> {
// Default implementation for restarting Vite
throw new Error('restartViteServer not implemented for this provider');
}
}
+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).