From dbf34e2d635b94d7507a7c38280e64f61ee3bb35 Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:14:27 -0400 Subject: [PATCH] Add v2 sandbox implementation with new API routes and sandbox library --- app/api/create-ai-sandbox-v2/route.ts | 95 ++++ app/api/install-packages-v2/route.ts | 44 ++ app/api/run-command-v2/route.ts | 46 ++ lib/sandbox/factory.ts | 42 ++ lib/sandbox/providers/e2b-provider.ts | 494 ++++++++++++++++++ lib/sandbox/providers/vercel-provider.ts | 469 +++++++++++++++++ lib/sandbox/types.ts | 64 +++ packages/create-open-lovable/index.js | 94 ++++ packages/create-open-lovable/lib/installer.js | 261 +++++++++ packages/create-open-lovable/lib/prompts.js | 190 +++++++ packages/create-open-lovable/package.json | 36 ++ .../templates/e2b/.env.example | 25 + .../templates/e2b/README.md | 38 ++ .../templates/vercel/.env.example | 28 + .../templates/vercel/README.md | 52 ++ 15 files changed, 1978 insertions(+) create mode 100644 app/api/create-ai-sandbox-v2/route.ts create mode 100644 app/api/install-packages-v2/route.ts create mode 100644 app/api/run-command-v2/route.ts create mode 100644 lib/sandbox/factory.ts create mode 100644 lib/sandbox/providers/e2b-provider.ts create mode 100644 lib/sandbox/providers/vercel-provider.ts create mode 100644 lib/sandbox/types.ts create mode 100644 packages/create-open-lovable/index.js create mode 100644 packages/create-open-lovable/lib/installer.js create mode 100644 packages/create-open-lovable/lib/prompts.js create mode 100644 packages/create-open-lovable/package.json create mode 100644 packages/create-open-lovable/templates/e2b/.env.example create mode 100644 packages/create-open-lovable/templates/e2b/README.md create mode 100644 packages/create-open-lovable/templates/vercel/.env.example create mode 100644 packages/create-open-lovable/templates/vercel/README.md diff --git a/app/api/create-ai-sandbox-v2/route.ts b/app/api/create-ai-sandbox-v2/route.ts new file mode 100644 index 0000000..93410a1 --- /dev/null +++ b/app/api/create-ai-sandbox-v2/route.ts @@ -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; + 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(); + } + + // 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 } + ); + } +} \ No newline at end of file diff --git a/app/api/install-packages-v2/route.ts b/app/api/install-packages-v2/route.ts new file mode 100644 index 0000000..646e44b --- /dev/null +++ b/app/api/install-packages-v2/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/app/api/run-command-v2/route.ts b/app/api/run-command-v2/route.ts new file mode 100644 index 0000000..6ea58b3 --- /dev/null +++ b/app/api/run-command-v2/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/lib/sandbox/factory.ts b/lib/sandbox/factory.ts new file mode 100644 index 0000000..97dd529 --- /dev/null +++ b/lib/sandbox/factory.ts @@ -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; + } + } +} \ No newline at end of file diff --git a/lib/sandbox/providers/e2b-provider.ts b/lib/sandbox/providers/e2b-provider.ts new file mode 100644 index 0000000..91fe458 --- /dev/null +++ b/lib/sandbox/providers/e2b-provider.ts @@ -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 = new Set(); + + async createSandbox(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 = """ + + + + + Sandbox App + + +
+ + +""" + +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( + + + , +)""" + +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 ( +
+
+

+ Sandbox Ready
+ Start building your React app with Vite and Tailwind CSS! +

+
+
+ ) +} + +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 { + 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 { + 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; + } +} \ No newline at end of file diff --git a/lib/sandbox/providers/vercel-provider.ts b/lib/sandbox/providers/vercel-provider.ts new file mode 100644 index 0000000..ed09dea --- /dev/null +++ b/lib/sandbox/providers/vercel-provider.ts @@ -0,0 +1,469 @@ +import { Sandbox } from '@vercel/sandbox'; +import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; + +export class VercelProvider extends SandboxProvider { + private existingFiles: Set = new Set(); + + async createSandbox(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 = ` + + + + + Sandbox App + + +
+ + +`; + + 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( + + + , +)`; + + await this.writeFile('src/main.jsx', mainJsx); + + // Create src/App.jsx + const appJsx = `function App() { + return ( +
+
+

+ Vercel Sandbox Ready
+ Start building your React app with Vite and Tailwind CSS! +

+
+
+ ) +} + +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 { + 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 { + 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; + } +} \ No newline at end of file diff --git a/lib/sandbox/types.ts b/lib/sandbox/types.ts new file mode 100644 index 0000000..9ab7a3c --- /dev/null +++ b/lib/sandbox/types.ts @@ -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; + abstract runCommand(command: string): Promise; + abstract writeFile(path: string, content: string): Promise; + abstract readFile(path: string): Promise; + abstract listFiles(directory?: string): Promise; + abstract installPackages(packages: string[]): Promise; + abstract getSandboxUrl(): string | null; + abstract terminate(): Promise; + abstract isAlive(): boolean; + + // Optional methods that providers can override + async setupViteApp(): Promise { + // Default implementation for setting up a Vite React app + throw new Error('setupViteApp not implemented for this provider'); + } + + async restartViteServer(): Promise { + // Default implementation for restarting Vite + throw new Error('restartViteServer not implemented for this provider'); + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/index.js b/packages/create-open-lovable/index.js new file mode 100644 index 0000000..1b5495f --- /dev/null +++ b/packages/create-open-lovable/index.js @@ -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 ', 'Sandbox provider (e2b or vercel)') + .option('-n, --name ', 'Project name') + .option('-p, --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); +}); \ No newline at end of file diff --git a/packages/create-open-lovable/lib/installer.js b/packages/create-open-lovable/lib/installer.js new file mode 100644 index 0000000..dcb7448 --- /dev/null +++ b/packages/create-open-lovable/lib/installer.js @@ -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); + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/lib/prompts.js b/packages/create-open-lovable/lib/prompts.js new file mode 100644 index 0000000..5008049 --- /dev/null +++ b/packages/create-open-lovable/lib/prompts.js @@ -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; +} \ No newline at end of file diff --git a/packages/create-open-lovable/package.json b/packages/create-open-lovable/package.json new file mode 100644 index 0000000..cbcc8dc --- /dev/null +++ b/packages/create-open-lovable/package.json @@ -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" + } +} \ No newline at end of file diff --git a/packages/create-open-lovable/templates/e2b/.env.example b/packages/create-open-lovable/templates/e2b/.env.example new file mode 100644 index 0000000..9d0b9a5 --- /dev/null +++ b/packages/create-open-lovable/templates/e2b/.env.example @@ -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 \ No newline at end of file diff --git a/packages/create-open-lovable/templates/e2b/README.md b/packages/create-open-lovable/templates/e2b/README.md new file mode 100644 index 0000000..05f0757 --- /dev/null +++ b/packages/create-open-lovable/templates/e2b/README.md @@ -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). \ No newline at end of file diff --git a/packages/create-open-lovable/templates/vercel/.env.example b/packages/create-open-lovable/templates/vercel/.env.example new file mode 100644 index 0000000..021f5af --- /dev/null +++ b/packages/create-open-lovable/templates/vercel/.env.example @@ -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 \ No newline at end of file diff --git a/packages/create-open-lovable/templates/vercel/README.md b/packages/create-open-lovable/templates/vercel/README.md new file mode 100644 index 0000000..5c2bb75 --- /dev/null +++ b/packages/create-open-lovable/templates/vercel/README.md @@ -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). \ No newline at end of file