Merge pull request #110 from firecrawl/revert-109-vercel-sandbox

This commit is contained in:
Developers Digest
2025-09-02 18:36:03 -04:00
committed by GitHub
22 changed files with 1662 additions and 1065 deletions
+16 -16
View File
@@ -1,20 +1,20 @@
# Required # REQUIRED - Sandboxes for code execution
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) # Get yours at https://e2b.dev
E2B_API_KEY=your_e2b_api_key_here
# Vercel Sandbox Authentication (choose one method) # REQUIRED - Web scraping for cloning websites
# See: https://vercel.com/docs/vercel-sandbox#authentication # Get yours at https://firecrawl.dev
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
# Method 1: OIDC Token (recommended for development) # OPTIONAL - AI Providers (need at least one)
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically # Get yours at https://console.anthropic.com
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Method 2: Personal Access Token (for production or when OIDC unavailable) # Get yours at https://platform.openai.com
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID OPENAI_API_KEY=your_openai_api_key_here
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
# Optional (need at least one AI provider) # Get yours at https://aistudio.google.com/app/apikey
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com GEMINI_API_KEY=your_gemini_api_key_here
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey # Get yours at https://console.groq.com
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) GROQ_API_KEY=your_groq_api_key_here
-1
View File
@@ -56,4 +56,3 @@ e2b-template-*
*.temp *.temp
repomix-output.txt repomix-output.txt
bun.lockb bun.lockb
.env*.local
+1 -13
View File
@@ -16,23 +16,11 @@ npm install
``` ```
2. **Add `.env.local`** 2. **Add `.env.local`**
```env ```env
# Required # Required
E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes)
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
# Vercel Sandbox Authentication (choose one method)
# See: https://vercel.com/docs/vercel-sandbox#authentication
# Method 1: OIDC Token (recommended for development)
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
# Method 2: Personal Access Token (for production or when OIDC unavailable)
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
# Optional (need at least one AI provider) # Optional (need at least one AI provider)
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
+34 -45
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@vercel/sandbox'; import { Sandbox } from '@e2b/code-interpreter';
import type { SandboxState } from '@/types/sandbox'; import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation'; import type { ConversationState } from '@/types/conversation';
@@ -525,6 +525,7 @@ export async function POST(request: NextRequest) {
normalizedPath = 'src/' + normalizedPath; normalizedPath = 'src/' + normalizedPath;
} }
const fullPath = `/home/user/app/${normalizedPath}`;
const isUpdate = global.existingFiles.has(normalizedPath); const isUpdate = global.existingFiles.has(normalizedPath);
// Remove any CSS imports from JSX/JS files (we're using Tailwind) // Remove any CSS imports from JSX/JS files (we're using Tailwind)
@@ -533,20 +534,19 @@ export async function POST(request: NextRequest) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
} }
// Create directory if needed // Write the file using Python (code-interpreter SDK)
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; const escapedContent = fileContent
if (dirPath) { .replace(/\\/g, '\\\\')
await sandboxInstance.runCommand({ .replace(/"""/g, '\\"\\"\\"')
cmd: 'mkdir', .replace(/\$/g, '\\$');
args: ['-p', dirPath]
});
}
// Write the file using Vercel Sandbox writeFiles await sandboxInstance.runCode(`
await sandboxInstance.writeFiles([{ import os
path: normalizedPath, os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
content: Buffer.from(fileContent) with open("${fullPath}", 'w') as f:
}]); f.write("""${escapedContent}""")
print(f"File written: ${fullPath}")
`);
// Update file cache // Update file cache
if (global.sandboxState?.fileCache) { if (global.sandboxState?.fileCache) {
@@ -599,39 +599,28 @@ export async function POST(request: NextRequest) {
action: 'executing' action: 'executing'
}); });
// Parse command and arguments for Vercel Sandbox // Use E2B commands.run() for cleaner execution
const commandParts = cmd.trim().split(/\s+/); const result = await sandboxInstance.commands.run(cmd, {
const cmdName = commandParts[0]; cwd: '/home/user/app',
const args = commandParts.slice(1); timeout: 60,
on_stdout: async (data: string) => {
// Use Vercel Sandbox runCommand await sendProgress({
const result = await sandboxInstance.runCommand({ type: 'command-output',
cmd: cmdName, command: cmd,
args output: data,
stream: 'stdout'
});
},
on_stderr: async (data: string) => {
await sendProgress({
type: 'command-output',
command: cmd,
output: data,
stream: 'stderr'
});
}
}); });
// Get command output
const stdout = await result.stdout();
const stderr = await result.stderr();
if (stdout) {
await sendProgress({
type: 'command-output',
command: cmd,
output: stdout,
stream: 'stdout'
});
}
if (stderr) {
await sendProgress({
type: 'command-output',
command: cmd,
output: stderr,
stream: 'stderr'
});
}
if (results.commandsExecuted) { if (results.commandsExecuted) {
results.commandsExecuted.push(cmd); results.commandsExecuted.push(cmd);
} }
+27 -32
View File
@@ -432,12 +432,15 @@ function App() {
export default App;`; export default App;`;
try { try {
await global.activeSandbox.writeFiles([{ await global.activeSandbox.runCode(`
path: 'src/App.jsx', file_path = "/home/user/app/src/App.jsx"
content: Buffer.from(appContent) file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
}]);
with open(file_path, 'w') as f:
console.log('Auto-generated: src/App.jsx'); f.write(file_content)
print(f"Auto-generated: {file_path}")
`);
results.filesCreated.push('src/App.jsx (auto-generated)'); results.filesCreated.push('src/App.jsx (auto-generated)');
} catch (error) { } catch (error) {
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`); results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
@@ -456,7 +459,9 @@ export default App;`;
if (!isEdit && !indexCssInParsed && !indexCssExists) { if (!isEdit && !indexCssInParsed && !indexCssExists) {
try { try {
const indexCssContent = `@tailwind base; await global.activeSandbox.runCode(`
file_path = "/home/user/app/src/index.css"
file_content = """@tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@@ -478,14 +483,13 @@ body {
margin: 0; margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
}`; }"""
await global.activeSandbox.writeFiles([{ with open(file_path, 'w') as f:
path: 'src/index.css', f.write(file_content)
content: Buffer.from(indexCssContent)
}]); print(f"Auto-generated: {file_path}")
`);
console.log('Auto-generated: src/index.css');
results.filesCreated.push('src/index.css (with Tailwind)'); results.filesCreated.push('src/index.css (with Tailwind)');
} catch (error) { } catch (error) {
results.errors.push('Failed to create index.css with Tailwind'); results.errors.push('Failed to create index.css with Tailwind');
@@ -496,24 +500,15 @@ body {
// Execute commands // Execute commands
for (const cmd of parsed.commands) { for (const cmd of parsed.commands) {
try { try {
// Parse command and arguments await global.activeSandbox.runCode(`
const commandParts = cmd.trim().split(/\s+/); import subprocess
const cmdName = commandParts[0]; os.chdir('/home/user/app')
const args = commandParts.slice(1); result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
print(f"Executed: ${cmd}")
// Execute command using Vercel Sandbox print(result.stdout)
const result = await global.activeSandbox.runCommand({ if result.stderr:
cmd: cmdName, print(f"Errors: {result.stderr}")
args `);
});
console.log(`Executed: ${cmd}`);
const stdout = await result.stdout();
const stderr = await result.stderr();
if (stdout) console.log(stdout);
if (stderr) console.log(`Errors: ${stderr}`);
results.commandsExecuted.push(cmd); results.commandsExecuted.push(cmd);
} catch (error) { } catch (error) {
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`); results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
+189 -151
View File
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { Sandbox } from '@vercel/sandbox'; import { Sandbox } from '@e2b/code-interpreter';
import type { SandboxState } from '@/types/sandbox'; import type { SandboxState } from '@/types/sandbox';
import { appConfig } from '@/config/app.config'; import { appConfig } from '@/config/app.config';
@@ -15,15 +15,15 @@ export async function POST() {
let sandbox: any = null; let sandbox: any = null;
try { try {
console.log('[create-ai-sandbox] Creating Vercel sandbox...'); console.log('[create-ai-sandbox] Creating base sandbox...');
// Kill existing sandbox if any // Kill existing sandbox if any
if (global.activeSandbox) { if (global.activeSandbox) {
console.log('[create-ai-sandbox] Stopping existing sandbox...'); console.log('[create-ai-sandbox] Killing existing sandbox...');
try { try {
await global.activeSandbox.stop(); await global.activeSandbox.kill();
} catch (e) { } catch (e) {
console.error('Failed to stop existing sandbox:', e); console.error('Failed to close existing sandbox:', e);
} }
global.activeSandbox = null; global.activeSandbox = null;
} }
@@ -35,102 +35,81 @@ export async function POST() {
global.existingFiles = new Set<string>(); global.existingFiles = new Set<string>();
} }
// Create Vercel sandbox with flexible authentication // Create base sandbox - we'll set up Vite ourselves for full control
console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`); console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
sandbox = await Sandbox.create({
apiKey: process.env.E2B_API_KEY,
timeoutMs: appConfig.e2b.timeoutMs
});
// Prepare sandbox configuration const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
const sandboxConfig: any = { const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
timeout: appConfig.vercelSandbox.timeoutMs,
runtime: appConfig.vercelSandbox.runtime,
ports: [appConfig.vercelSandbox.devPort]
};
// Add authentication parameters if using personal access token
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
console.log('[create-ai-sandbox] 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('[create-ai-sandbox] Using OIDC token authentication');
} else {
console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication');
}
sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = sandbox.sandboxId;
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`); console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
console.log(`[create-ai-sandbox] Sandbox host: ${host}`);
// Set up a basic Vite React app // Set up a basic Vite React app using Python to write files
console.log('[create-ai-sandbox] Setting up Vite React app...'); console.log('[create-ai-sandbox] Setting up Vite React app...');
// First, change to the working directory // Write all files in a single Python script to avoid multiple executions
await sandbox.runCommand('pwd'); const setupScript = `
const workDir = appConfig.vercelSandbox.workingDirectory; import os
import json
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
// Extract the hostname from the sandbox URL for Vite config
const sandboxHostname = new URL(sandboxUrl).hostname;
console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
// Create the Vite config content with the proper hostname (using string concatenation) print('Setting up React app with Vite and Tailwind...')
const viteConfigContent = `import { defineConfig } from 'vite'
# 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 for E2B - with allowedHosts
vite_config = """import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// Vercel Sandbox compatible Vite configuration // E2B-compatible Vite configuration
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: ${appConfig.vercelSandbox.devPort}, port: 5173,
strictPort: true, strictPort: true,
hmr: true, hmr: false,
allowedHosts: [ allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
'localhost',
'127.0.0.1',
'` + sandboxHostname + `', // Allow the Vercel Sandbox domain
'.vercel.run', // Allow all Vercel sandbox domains
'.vercel-sandbox.dev' // Fallback pattern
]
} }
})`; })"""
// Create the project files (now we have the sandbox hostname) with open('/home/user/app/vite.config.js', 'w') as f:
const projectFiles = [ f.write(vite_config)
{ print('✓ vite.config.js')
path: 'package.json',
content: Buffer.from(JSON.stringify({ # Tailwind config - standard without custom design tokens
"name": "sandbox-app", tailwind_config = """/** @type {import('tailwindcss').Config} */
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host --port 3000",
"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"
}
}, null, 2))
},
{
path: 'vite.config.js',
content: Buffer.from(viteConfigContent)
},
{
path: 'tailwind.config.js',
content: Buffer.from(`/** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: [
"./index.html", "./index.html",
@@ -140,20 +119,26 @@ export default {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
}`) }"""
},
{ with open('/home/user/app/tailwind.config.js', 'w') as f:
path: 'postcss.config.js', f.write(tailwind_config)
content: Buffer.from(`export default { print('✓ tailwind.config.js')
# PostCSS config
postcss_config = """export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}`) }"""
},
{ with open('/home/user/app/postcss.config.js', 'w') as f:
path: 'index.html', f.write(postcss_config)
content: Buffer.from(`<!DOCTYPE html> print('✓ postcss.config.js')
# Index.html
index_html = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -164,11 +149,14 @@ export default {
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html>`) </html>"""
},
{ with open('/home/user/app/index.html', 'w') as f:
path: 'src/main.jsx', f.write(index_html)
content: Buffer.from(`import React from 'react' print('✓ index.html')
# Main.jsx
main_jsx = """import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'
import './index.css' import './index.css'
@@ -177,18 +165,19 @@ ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
)`) )"""
},
{ with open('/home/user/app/src/main.jsx', 'w') as f:
path: 'src/App.jsx', f.write(main_jsx)
content: Buffer.from(`function App() { print('✓ src/main.jsx')
# App.jsx with explicit Tailwind test
app_jsx = """function App() {
return ( return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl"> <div className="text-center max-w-2xl">
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
Sandbox Ready
</h1>
<p className="text-lg text-gray-400"> <p className="text-lg text-gray-400">
Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS! Start building your React app with Vite and Tailwind CSS!
</p> </p>
</div> </div>
@@ -196,11 +185,14 @@ ReactDOM.createRoot(document.getElementById('root')).render(
) )
} }
export default App`) export default App"""
},
{ with open('/home/user/app/src/App.jsx', 'w') as f:
path: 'src/index.css', f.write(app_jsx)
content: Buffer.from(`@tailwind base; print('✓ src/App.jsx')
# Index.css with explicit Tailwind directives
index_css = """@tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@@ -224,53 +216,99 @@ export default App`)
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39); background-color: rgb(17 24 39);
}`) }"""
}
];
// Create directory structure first with open('/home/user/app/src/index.css', 'w') as f:
await sandbox.runCommand({ f.write(index_css)
cmd: 'mkdir', print('✓ src/index.css')
args: ['-p', 'src']
}); print('\\nAll files created successfully!')
`;
// Write all files
await sandbox.writeFiles(projectFiles); // Execute the setup script
console.log('[create-ai-sandbox] ✓ Project files created'); await sandbox.runCode(setupScript);
// Install dependencies // Install dependencies
console.log('[create-ai-sandbox] Installing dependencies...'); console.log('[create-ai-sandbox] Installing dependencies...');
const installResult = await sandbox.runCommand({ await sandbox.runCode(`
cmd: 'npm', import subprocess
args: ['install', '--loglevel', 'info'] import sys
});
if (installResult.exitCode === 0) { print('Installing npm packages...')
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully'); result = subprocess.run(
} else { ['npm', 'install'],
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...'); 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}')
# Continue anyway as it might still work
`);
// Start Vite dev server in detached mode // Start Vite dev server
console.log('[create-ai-sandbox] Starting Vite dev server...'); console.log('[create-ai-sandbox] Starting Vite dev server...');
const viteProcess = await sandbox.runCommand({ await sandbox.runCode(`
cmd: 'npm', import subprocess
args: ['run', 'dev'], import os
detached: true import time
});
os.chdir('/home/user/app')
console.log('[create-ai-sandbox] ✓ Vite dev server started');
# 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 fully ready // Wait for Vite to be fully ready
await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay)); await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
// Force Tailwind CSS to rebuild by touching the CSS file
await sandbox.runCode(`
import os
import time
# Touch the CSS file to trigger rebuild
css_file = '/home/user/app/src/index.css'
if os.path.exists(css_file):
os.utime(css_file, None)
print('✓ Triggered CSS rebuild')
# Also ensure PostCSS processes it
time.sleep(2)
print('✓ Tailwind CSS should be loaded')
`);
// Store sandbox globally // Store sandbox globally
global.activeSandbox = sandbox; global.activeSandbox = sandbox;
global.sandboxData = { global.sandboxData = {
sandboxId, sandboxId,
url: sandboxUrl, url: `https://${host}`
viteProcess
}; };
// Set extended timeout on the sandbox instance if method available
if (typeof sandbox.setTimeout === 'function') {
sandbox.setTimeout(appConfig.e2b.timeoutMs);
console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`);
}
// Initialize sandbox state // Initialize sandbox state
global.sandboxState = { global.sandboxState = {
fileCache: { fileCache: {
@@ -281,7 +319,7 @@ body {
sandbox, sandbox,
sandboxData: { sandboxData: {
sandboxId, sandboxId,
url: sandboxUrl url: `https://${host}`
} }
}; };
@@ -295,13 +333,13 @@ body {
global.existingFiles.add('tailwind.config.js'); global.existingFiles.add('tailwind.config.js');
global.existingFiles.add('postcss.config.js'); global.existingFiles.add('postcss.config.js');
console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl); console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
sandboxId, sandboxId,
url: sandboxUrl, url: `https://${host}`,
message: 'Vercel sandbox created and Vite React app initialized' message: 'Sandbox created and Vite React app initialized'
}); });
} catch (error) { } catch (error) {
@@ -310,9 +348,9 @@ body {
// Clean up on error // Clean up on error
if (sandbox) { if (sandbox) {
try { try {
await sandbox.stop(); await sandbox.kill();
} catch (e) { } catch (e) {
console.error('Failed to stop sandbox on error:', e); console.error('Failed to close sandbox on error:', e);
} }
} }
+37 -36
View File
@@ -15,37 +15,41 @@ export async function POST(request: NextRequest) {
console.log('[create-zip] Creating project zip...'); console.log('[create-zip] Creating project zip...');
// Create zip file in sandbox using standard commands // Create zip file in sandbox
const zipResult = await global.activeSandbox.runCommand({ const result = await global.activeSandbox.runCode(`
cmd: 'bash', import zipfile
args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`] import os
}); import json
if (zipResult.exitCode !== 0) { os.chdir('/home/user/app')
const error = await zipResult.stderr();
throw new Error(`Failed to create zip: ${error}`); # Create zip file
} with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk('.'):
const sizeResult = await global.activeSandbox.runCommand({ # Skip node_modules and .git
cmd: 'bash', dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
}); for file in files:
file_path = os.path.join(root, file)
const fileSize = await sizeResult.stdout(); arcname = os.path.relpath(file_path, '.')
console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`); zipf.write(file_path, arcname)
# Get file size
file_size = os.path.getsize('/tmp/project.zip')
print(f" Created project.zip ({file_size} bytes)")
`);
// Read the zip file and convert to base64 // Read the zip file and convert to base64
const readResult = await global.activeSandbox.runCommand({ const readResult = await global.activeSandbox.runCode(`
cmd: 'base64', import base64
args: ['/tmp/project.zip']
}); with open('/tmp/project.zip', 'rb') as f:
content = f.read()
encoded = base64.b64encode(content).decode('utf-8')
print(encoded)
`);
if (readResult.exitCode !== 0) { const base64Content = readResult.logs.stdout.join('').trim();
const error = await readResult.stderr();
throw new Error(`Failed to read zip file: ${error}`);
}
const base64Content = (await readResult.stdout()).trim();
// Create a data URL for download // Create a data URL for download
const dataUrl = `data:application/zip;base64,${base64Content}`; const dataUrl = `data:application/zip;base64,${base64Content}`;
@@ -53,18 +57,15 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
dataUrl, dataUrl,
fileName: 'vercel-sandbox-project.zip', fileName: 'e2b-project.zip',
message: 'Zip file created successfully' message: 'Zip file created successfully'
}); });
} catch (error) { } catch (error) {
console.error('[create-zip] Error:', error); console.error('[create-zip] Error:', error);
return NextResponse.json( return NextResponse.json({
{ success: false,
success: false, error: (error as Error).message
error: (error as Error).message }, { status: 500 });
},
{ status: 500 }
);
} }
} }
+132 -60
View File
@@ -64,7 +64,15 @@ export async function POST(request: NextRequest) {
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process']; const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
if (builtins.includes(imp)) return false; if (builtins.includes(imp)) return false;
return true; // Extract package name (handle scoped packages and subpaths)
const parts = imp.split('/');
if (imp.startsWith('@')) {
// Scoped package like @vitejs/plugin-react
return true;
} else {
// Regular package, return just the first part
return true;
}
}); });
// Extract just the package names (without subpaths) // Extract just the package names (without subpaths)
@@ -93,89 +101,153 @@ export async function POST(request: NextRequest) {
} }
// Check which packages are already installed // Check which packages are already installed
const installed: string[] = []; const checkResult = await global.activeSandbox.runCode(`
const missing: string[] = []; import os
import json
installed = []
missing = []
packages = ${JSON.stringify(uniquePackages)}
for package in packages:
# Handle scoped packages
if package.startswith('@'):
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
for (const packageName of uniquePackages) { if os.path.exists(package_path):
try { installed.append(package)
const checkResult = await global.activeSandbox.runCommand({ else:
cmd: 'test', missing.append(package)
args: ['-d', `node_modules/${packageName}`]
});
if (checkResult.exitCode === 0) {
installed.push(packageName);
} else {
missing.push(packageName);
}
} catch (error) {
// If test command fails, assume package is missing
missing.push(packageName);
}
}
console.log('[detect-and-install-packages] Package status:', { installed, missing }); result = {
'installed': installed,
'missing': missing
}
if (missing.length === 0) { print(json.dumps(result))
`);
const status = JSON.parse(checkResult.logs.stdout.join(''));
console.log('[detect-and-install-packages] Package status:', status);
if (status.missing.length === 0) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
packagesInstalled: [], packagesInstalled: [],
packagesAlreadyInstalled: installed, packagesAlreadyInstalled: status.installed,
message: 'All packages already installed' message: 'All packages already installed'
}); });
} }
// Install missing packages // Install missing packages
console.log('[detect-and-install-packages] Installing packages:', missing); console.log('[detect-and-install-packages] Installing packages:', status.missing);
const installResult = await global.activeSandbox.runCommand({ const installResult = await global.activeSandbox.runCode(`
cmd: 'npm', import subprocess
args: ['install', '--save', ...missing] import os
}); import json
const stdout = await installResult.stdout(); os.chdir('/home/user/app')
const stderr = await installResult.stderr(); packages_to_install = ${JSON.stringify(status.missing)}
# Join packages into a single install command
packages_str = ' '.join(packages_to_install)
cmd = f'npm install {packages_str} --save'
print(f"Running: {cmd}")
# Run npm install with explicit save flag
result = subprocess.run(['npm', 'install', '--save'] + packages_to_install,
capture_output=True,
text=True,
cwd='/home/user/app',
timeout=60)
print("stdout:", result.stdout)
if result.stderr:
print("stderr:", result.stderr)
# Verify installation
installed = []
failed = []
for package in packages_to_install:
# Handle scoped packages correctly
if package.startswith('@'):
# For scoped packages like @heroicons/react
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
console.log('[detect-and-install-packages] Install stdout:', stdout); if os.path.exists(package_path):
if (stderr) { installed.append(package)
console.log('[detect-and-install-packages] Install stderr:', stderr); print(f"✓ Verified installation of {package}")
} else:
# Check if it's a submodule of an installed package
// Verify installation base_package = package.split('/')[0]
const finalInstalled: string[] = []; if package.startswith('@'):
const failed: string[] = []; # For @scope/package, the base is @scope/package
base_package = '/'.join(package.split('/')[:2])
for (const packageName of missing) {
try {
const verifyResult = await global.activeSandbox.runCommand({
cmd: 'test',
args: ['-d', `node_modules/${packageName}`]
});
if (verifyResult.exitCode === 0) { base_path = f"/home/user/app/node_modules/{base_package}"
finalInstalled.push(packageName); if os.path.exists(base_path):
console.log(`✓ Verified installation of ${packageName}`); installed.append(package)
print(f"✓ Verified installation of {package} (via {base_package})")
else:
failed.append(package)
print(f"✗ Failed to verify installation of {package}")
result_data = {
'installed': installed,
'failed': failed,
'returncode': result.returncode
}
print("\\nResult:", json.dumps(result_data))
`, { timeout: 60000 });
// Parse the result more safely
let installStatus;
try {
const stdout = installResult.logs.stdout.join('');
const resultMatch = stdout.match(/Result:\s*({.*})/);
if (resultMatch) {
installStatus = JSON.parse(resultMatch[1]);
} else {
// Fallback parsing
const lines = stdout.split('\n');
const resultLine = lines.find((line: string) => line.includes('Result:'));
if (resultLine) {
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
} else { } else {
failed.push(packageName); throw new Error('Could not find Result in output');
console.log(`✗ Failed to verify installation of ${packageName}`);
} }
} catch (error) {
failed.push(packageName);
console.log(`✗ Error verifying ${packageName}:`, error);
} }
} catch (parseError) {
console.error('[detect-and-install-packages] Failed to parse install result:', parseError);
console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join(''));
// Fallback to assuming all packages were installed
installStatus = {
installed: status.missing,
failed: [],
returncode: 0
};
} }
if (failed.length > 0) { if (installStatus.failed.length > 0) {
console.error('[detect-and-install-packages] Failed to install:', failed); console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
packagesInstalled: finalInstalled, packagesInstalled: installStatus.installed,
packagesFailed: failed, packagesFailed: installStatus.failed,
packagesAlreadyInstalled: installed, packagesAlreadyInstalled: status.installed,
message: `Installed ${finalInstalled.length} packages`, message: `Installed ${installStatus.installed.length} packages`,
logs: stdout logs: installResult.logs.stdout.join('\n')
}); });
} catch (error) { } catch (error) {
+5 -47
View File
@@ -11,9 +11,6 @@ import { FileManifest } from '@/types/file-manifest';
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation'; import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
import { appConfig } from '@/config/app.config'; import { appConfig } from '@/config/app.config';
// Force dynamic route to enable streaming
export const dynamic = 'force-dynamic';
const groq = createGroq({ const groq = createGroq({
apiKey: process.env.GROQ_API_KEY, apiKey: process.env.GROQ_API_KEY,
}); });
@@ -1159,21 +1156,9 @@ CRITICAL: When files are provided in the context:
const isGoogle = model.startsWith('google/'); const isGoogle = model.startsWith('google/');
const isOpenAI = model.startsWith('openai/gpt-5'); const isOpenAI = model.startsWith('openai/gpt-5');
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq)); const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
const actualModel = isAnthropic ? model.replace('anthropic/', '') :
// Fix model name transformation for different providers (model === 'openai/gpt-5') ? 'gpt-5' :
let actualModel: string; (isGoogle ? model.replace('google/', '') : model);
if (isAnthropic) {
actualModel = model.replace('anthropic/', '');
} else if (model === 'openai/gpt-5') {
actualModel = 'gpt-5';
} else if (isGoogle) {
// Google uses specific model names - convert our naming to theirs
actualModel = model.replace('google/', '');
} else {
actualModel = model;
}
console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`);
// Make streaming API call with appropriate provider // Make streaming API call with appropriate provider
const streamOptions: any = { const streamOptions: any = {
@@ -1258,28 +1243,7 @@ It's better to have 3 complete files than 10 incomplete files.`
}; };
} }
let result; const result = await streamText(streamOptions);
try {
result = await streamText(streamOptions);
} catch (streamError) {
console.error('[generate-ai-code-stream] Error calling streamText:', streamError);
// Send specific error for debugging
await sendProgress({
type: 'error',
message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'Groq'} streaming: ${(streamError as Error).message}`
});
// If this is a Google model error, provide helpful info
if (isGoogle) {
await sendProgress({
type: 'info',
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
});
}
throw streamError;
}
// Stream the response and parse in real-time // Stream the response and parse in real-time
let generatedCode = ''; let generatedCode = '';
@@ -1751,18 +1715,12 @@ Provide the complete file content without any truncation. Include all necessary
} }
})(); })();
// Return the stream with proper headers for streaming support // Return the stream
return new Response(stream.readable, { return new Response(stream.readable, {
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked',
'Content-Encoding': 'none', // Prevent compression that can break streaming
'X-Accel-Buffering': 'no', // Disable nginx buffering
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}, },
}); });
+56 -79
View File
@@ -18,81 +18,58 @@ export async function GET() {
console.log('[get-sandbox-files] Fetching and analyzing file structure...'); console.log('[get-sandbox-files] Fetching and analyzing file structure...');
// Get list of all relevant files // Get all React/JS/CSS files
const findResult = await global.activeSandbox.runCommand({ const result = await global.activeSandbox.runCode(`
cmd: 'find', import os
args: [ import json
'.',
'-name', 'node_modules', '-prune', '-o', def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']):
'-name', '.git', '-prune', '-o', files_content = {}
'-name', 'dist', '-prune', '-o',
'-name', 'build', '-prune', '-o',
'-type', 'f',
'(',
'-name', '*.jsx',
'-o', '-name', '*.js',
'-o', '-name', '*.tsx',
'-o', '-name', '*.ts',
'-o', '-name', '*.css',
'-o', '-name', '*.json',
')',
'-print'
]
});
if (findResult.exitCode !== 0) { for root, dirs, files in os.walk(directory):
throw new Error('Failed to list files'); # Skip node_modules and other unwanted directories
} dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim());
console.log('[get-sandbox-files] Found', fileList.length, 'files');
// Read content of each file (limit to reasonable sizes)
const filesContent: Record<string, string> = {};
for (const filePath of fileList) {
try {
// Check file size first
const statResult = await global.activeSandbox.runCommand({
cmd: 'stat',
args: ['-f', '%z', filePath]
});
if (statResult.exitCode === 0) { for file in files:
const fileSize = parseInt(await statResult.stdout()); if any(file.endswith(ext) for ext in extensions):
file_path = os.path.join(root, file)
// Only read files smaller than 10KB relative_path = os.path.relpath(file_path, '/home/user/app')
if (fileSize < 10000) {
const catResult = await global.activeSandbox.runCommand({ try:
cmd: 'cat', with open(file_path, 'r') as f:
args: [filePath] content = f.read()
}); # Only include files under 10KB to avoid huge responses
if len(content) < 10000:
if (catResult.exitCode === 0) { files_content[relative_path] = content
const content = await catResult.stdout(); except:
// Remove leading './' from path pass
const relativePath = filePath.replace(/^\.\//, '');
filesContent[relativePath] = content;
}
}
}
} catch (error) {
// Skip files that can't be read
continue;
}
}
// Get directory structure return files_content
const treeResult = await global.activeSandbox.runCommand({
cmd: 'find', # Get the files
args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*'] files = get_files_content()
});
# Also get the directory structure
let structure = ''; structure = []
if (treeResult.exitCode === 0) { for root, dirs, files in os.walk('/home/user/app'):
const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim()); level = root.replace('/home/user/app', '').count(os.sep)
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines indent = ' ' * 2 * level
} structure.append(f"{indent}{os.path.basename(root)}/")
sub_indent = ' ' * 2 * (level + 1)
for file in files:
if not any(skip in root for skip in ['node_modules', '.git', 'dist', 'build']):
structure.append(f"{sub_indent}{file}")
result = {
'files': files,
'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines
}
print(json.dumps(result))
`);
const output = result.logs.stdout.join('');
const parsedResult = JSON.parse(output);
// Build enhanced file manifest // Build enhanced file manifest
const fileManifest: FileManifest = { const fileManifest: FileManifest = {
@@ -105,12 +82,12 @@ export async function GET() {
}; };
// Process each file // Process each file
for (const [relativePath, content] of Object.entries(filesContent)) { for (const [relativePath, content] of Object.entries(parsedResult.files)) {
const fullPath = `/${relativePath}`; const fullPath = `/home/user/app/${relativePath}`;
// Create base file info // Create base file info
const fileInfo: FileInfo = { const fileInfo: FileInfo = {
content: content, content: content as string,
type: 'utility', type: 'utility',
path: fullPath, path: fullPath,
relativePath, relativePath,
@@ -119,7 +96,7 @@ export async function GET() {
// Parse JavaScript/JSX files // Parse JavaScript/JSX files
if (relativePath.match(/\.(jsx?|tsx?)$/)) { if (relativePath.match(/\.(jsx?|tsx?)$/)) {
const parseResult = parseJavaScriptFile(content, fullPath); const parseResult = parseJavaScriptFile(content as string, fullPath);
Object.assign(fileInfo, parseResult); Object.assign(fileInfo, parseResult);
// Identify entry point // Identify entry point
@@ -155,9 +132,9 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
files: filesContent, files: parsedResult.files,
structure, structure: parsedResult.structure,
fileCount: Object.keys(filesContent).length, fileCount: Object.keys(parsedResult.files).length,
manifest: fileManifest, manifest: fileManifest,
}); });
+219 -121
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
declare global { declare global {
var activeSandbox: any; var activeSandbox: any;
@@ -35,8 +36,23 @@ export async function POST(request: NextRequest) {
console.log(`[install-packages] Cleaned:`, validPackages); console.log(`[install-packages] Cleaned:`, validPackages);
} }
// Get active sandbox // Try to get sandbox - either from global or reconnect
const sandbox = global.activeSandbox; let sandbox = global.activeSandbox;
if (!sandbox && sandboxId) {
console.log(`[install-packages] Reconnecting to sandbox ${sandboxId}...`);
try {
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
global.activeSandbox = sandbox;
console.log(`[install-packages] Successfully reconnected to sandbox ${sandboxId}`);
} catch (error) {
console.error(`[install-packages] Failed to reconnect to sandbox:`, error);
return NextResponse.json({
success: false,
error: `Failed to reconnect to sandbox: ${(error as Error).message}`
}, { status: 500 });
}
}
if (!sandbox) { if (!sandbox) {
return NextResponse.json({ return NextResponse.json({
@@ -45,7 +61,7 @@ export async function POST(request: NextRequest) {
}, { status: 400 }); }, { status: 400 });
} }
console.log('[install-packages] Installing packages:', validPackages); console.log('[install-packages] Installing packages:', packages);
// Create a response stream for real-time updates // Create a response stream for real-time updates
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@@ -67,20 +83,23 @@ export async function POST(request: NextRequest) {
packages: validPackages packages: validPackages
}); });
// Stop any existing development server first // Kill any existing Vite process first
await sendProgress({ type: 'status', message: 'Stopping development server...' }); await sendProgress({ type: 'status', message: 'Stopping development server...' });
try { await sandboxInstance.runCode(`
// Try to kill any running dev server processes import subprocess
await sandboxInstance.runCommand({ import os
cmd: 'pkill', import signal
args: ['-f', 'vite']
}); # Try to kill any existing Vite process
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit try:
} catch (error) { with open('/tmp/vite-process.pid', 'r') as f:
// It's OK if no process is found pid = int(f.read().strip())
console.log('[install-packages] No existing dev server found'); os.kill(pid, signal.SIGTERM)
} print("Stopped existing Vite process")
except:
print("No existing Vite process found")
`);
// Check which packages are already installed // Check which packages are already installed
await sendProgress({ await sendProgress({
@@ -88,51 +107,70 @@ export async function POST(request: NextRequest) {
message: 'Checking installed packages...' message: 'Checking installed packages...'
}); });
const checkResult = await sandboxInstance.runCode(`
import os
import json
os.chdir('/home/user/app')
# Read package.json to check installed packages
try:
with open('package.json', 'r') as f:
package_json = json.load(f)
dependencies = package_json.get('dependencies', {})
dev_dependencies = package_json.get('devDependencies', {})
all_deps = {**dependencies, **dev_dependencies}
# Check which packages need to be installed
packages_to_check = ${JSON.stringify(validPackages)}
already_installed = []
need_install = []
for pkg in packages_to_check:
# Handle scoped packages
if pkg.startswith('@'):
pkg_name = pkg
else:
# Extract package name without version
pkg_name = pkg.split('@')[0]
if pkg_name in all_deps:
already_installed.append(pkg_name)
else:
need_install.append(pkg)
print(f"Already installed: {already_installed}")
print(f"Need to install: {need_install}")
print(f"NEED_INSTALL:{json.dumps(need_install)}")
except Exception as e:
print(f"Error checking packages: {e}")
print(f"NEED_INSTALL:{json.dumps(packages_to_check)}")
`);
// Parse packages that need installation
let packagesToInstall = validPackages; let packagesToInstall = validPackages;
try { // Check if checkResult has the expected structure
// Read package.json to check existing dependencies if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
const catResult = await sandboxInstance.runCommand({ const outputLines = checkResult.results[0].text.split('\n');
cmd: 'cat', for (const line of outputLines) {
args: ['package.json'] if (line.startsWith('NEED_INSTALL:')) {
}); try {
if (catResult.exitCode === 0) { packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
const packageJsonContent = await catResult.stdout(); } catch (e) {
const packageJson = JSON.parse(packageJsonContent); console.error('Failed to parse packages to install:', e);
const dependencies = packageJson.dependencies || {};
const devDependencies = packageJson.devDependencies || {};
const allDeps = { ...dependencies, ...devDependencies };
const alreadyInstalled = [];
const needInstall = [];
for (const pkg of validPackages) {
// Handle scoped packages
const pkgName = pkg.startsWith('@') ? pkg : pkg.split('@')[0];
if (allDeps[pkgName]) {
alreadyInstalled.push(pkgName);
} else {
needInstall.push(pkg);
} }
} }
packagesToInstall = needInstall;
if (alreadyInstalled.length > 0) {
await sendProgress({
type: 'info',
message: `Already installed: ${alreadyInstalled.join(', ')}`
});
}
} }
} catch (error) { } else {
console.error('[install-packages] Error checking existing packages:', error); console.error('[install-packages] Invalid checkResult structure:', checkResult);
// If we can't check, just try to install all packages // If we can't check, just try to install all packages
packagesToInstall = validPackages; packagesToInstall = validPackages;
} }
if (packagesToInstall.length === 0) { if (packagesToInstall.length === 0) {
await sendProgress({ await sendProgress({
type: 'success', type: 'success',
@@ -140,104 +178,164 @@ export async function POST(request: NextRequest) {
installedPackages: [], installedPackages: [],
alreadyInstalled: validPackages alreadyInstalled: validPackages
}); });
// Restart dev server
await sendProgress({ type: 'status', message: 'Restarting development server...' });
const devServerProcess = await sandboxInstance.runCommand({
cmd: 'npm',
args: ['run', 'dev'],
detached: true
});
await sendProgress({
type: 'complete',
message: 'Dev server restarted!',
installedPackages: []
});
return; return;
} }
// Install only packages that aren't already installed // Install only packages that aren't already installed
const packageList = packagesToInstall.join(' ');
// Only send the npm install command message if we're actually installing new packages
await sendProgress({ await sendProgress({
type: 'info', type: 'info',
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}` message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
}); });
// Run npm install const installResult = await sandboxInstance.runCode(`
const installArgs = ['install', '--legacy-peer-deps', ...packagesToInstall]; import subprocess
const installResult = await sandboxInstance.runCommand({ import os
cmd: 'npm',
args: installArgs os.chdir('/home/user/app')
});
# Run npm install with output capture
packages_to_install = ${JSON.stringify(packagesToInstall)}
cmd_args = ['npm', 'install', '--legacy-peer-deps'] + packages_to_install
print(f"Running command: {' '.join(cmd_args)}")
process = subprocess.Popen(
cmd_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Stream output
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
# Get the return code
rc = process.poll()
# Capture any stderr
stderr = process.stderr.read()
if stderr:
print("STDERR:", stderr)
if 'ERESOLVE' in stderr:
print("ERESOLVE_ERROR: Dependency conflict detected - using --legacy-peer-deps flag")
print(f"\\nInstallation completed with code: {rc}")
# Verify packages were installed
import json
with open('/home/user/app/package.json', 'r') as f:
package_json = json.load(f)
installed = []
for pkg in ${JSON.stringify(packagesToInstall)}:
if pkg in package_json.get('dependencies', {}):
installed.append(pkg)
print(f"✓ Verified {pkg}")
else:
print(f"✗ Package {pkg} not found in dependencies")
// Get install output print(f"\\nVerified installed packages: {installed}")
const stdout = await installResult.stdout(); `, { timeout: 60000 }); // 60 second timeout for npm install
const stderr = await installResult.stderr();
if (stdout) { // Send npm output
const lines = stdout.split('\n').filter(line => line.trim()); const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
for (const line of lines) { const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
if (line.includes('npm WARN')) { for (const line of npmOutputLines) {
await sendProgress({ type: 'warning', message: line }); if (line.includes('STDERR:')) {
} else if (line.trim()) { const errorMsg = line.replace('STDERR:', '').trim();
await sendProgress({ type: 'output', message: line }); if (errorMsg && errorMsg !== 'undefined') {
await sendProgress({ type: 'error', message: errorMsg });
} }
} else if (line.includes('ERESOLVE_ERROR:')) {
const msg = line.replace('ERESOLVE_ERROR:', '').trim();
await sendProgress({
type: 'warning',
message: `Dependency conflict resolved with --legacy-peer-deps: ${msg}`
});
} else if (line.includes('npm WARN')) {
await sendProgress({ type: 'warning', message: line });
} else if (line.trim() && !line.includes('undefined')) {
await sendProgress({ type: 'output', message: line });
} }
} }
if (stderr) { // Check if installation was successful
const errorLines = stderr.split('\n').filter(line => line.trim()); const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
for (const line of errorLines) { let installedPackages: string[] = [];
if (line.includes('ERESOLVE')) {
await sendProgress({ if (installedMatch && installedMatch[1]) {
type: 'warning', installedPackages = installedMatch[1]
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}` .split(',')
}); .map((p: string) => p.trim().replace(/'/g, ''))
} else if (line.trim()) { .filter((p: string) => p.length > 0);
await sendProgress({ type: 'error', message: line });
}
}
} }
if (installResult.exitCode === 0) { if (installedPackages.length > 0) {
await sendProgress({ await sendProgress({
type: 'success', type: 'success',
message: `Successfully installed: ${packagesToInstall.join(', ')}`, message: `Successfully installed: ${installedPackages.join(', ')}`,
installedPackages: packagesToInstall installedPackages
}); });
} else { } else {
await sendProgress({ await sendProgress({
type: 'error', type: 'error',
message: 'Package installation failed' message: 'Failed to verify package installation'
}); });
} }
// Restart development server // Restart Vite dev server
await sendProgress({ type: 'status', message: 'Restarting development server...' }); await sendProgress({ type: 'status', message: 'Restarting development server...' });
try { await sandboxInstance.runCode(`
const devServerProcess = await sandboxInstance.runCommand({ import subprocess
cmd: 'npm', import os
args: ['run', 'dev'], import time
detached: true
}); os.chdir('/home/user/app')
// Wait a bit for the server to start # Kill any existing Vite processes
await new Promise(resolve => setTimeout(resolve, 3000)); subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(1)
await sendProgress({
type: 'complete', # Start Vite dev server
message: 'Package installation complete and dev server restarted!', env = os.environ.copy()
installedPackages: packagesToInstall env['FORCE_COLOR'] = '0'
});
} catch (error) { process = subprocess.Popen(
await sendProgress({ ['npm', 'run', 'dev'],
type: 'error', stdout=subprocess.PIPE,
message: `Failed to restart dev server: ${(error as Error).message}` stderr=subprocess.PIPE,
}); env=env
} )
print(f'✓ Vite dev server restarted with PID: {process.pid}')
# Store process info for later
with open('/tmp/vite-process.pid', 'w') as f:
f.write(str(process.pid))
# Wait a bit for Vite to start up
time.sleep(3)
# Touch files to trigger Vite reload
subprocess.run(['touch', '/home/user/app/package.json'])
subprocess.run(['touch', '/home/user/app/vite.config.js'])
print("Vite restarted and should now recognize all packages")
`);
await sendProgress({
type: 'complete',
message: 'Package installation complete and dev server restarted!',
installedPackages
});
} catch (error) { } catch (error) {
const errorMessage = (error as Error).message; const errorMessage = (error as Error).message;
+5 -5
View File
@@ -8,18 +8,18 @@ declare global {
export async function POST() { export async function POST() {
try { try {
console.log('[kill-sandbox] Stopping active sandbox...'); console.log('[kill-sandbox] Killing active sandbox...');
let sandboxKilled = false; let sandboxKilled = false;
// Stop existing sandbox if any // Kill existing sandbox if any
if (global.activeSandbox) { if (global.activeSandbox) {
try { try {
await global.activeSandbox.stop(); await global.activeSandbox.close();
sandboxKilled = true; sandboxKilled = true;
console.log('[kill-sandbox] Sandbox stopped successfully'); console.log('[kill-sandbox] Sandbox closed successfully');
} catch (e) { } catch (e) {
console.error('[kill-sandbox] Failed to stop sandbox:', e); console.error('[kill-sandbox] Failed to close sandbox:', e);
} }
global.activeSandbox = null; global.activeSandbox = null;
global.sandboxData = null; global.sandboxData = null;
+85 -88
View File
@@ -15,100 +15,97 @@ export async function GET() {
console.log('[monitor-vite-logs] Checking Vite process logs...'); console.log('[monitor-vite-logs] Checking Vite process logs...');
const errors: any[] = []; // Check both the error file and recent logs
const result = await global.activeSandbox.runCode(`
import json
import subprocess
import re
errors = []
# First check the error file
try:
with open('/tmp/vite-errors.json', 'r') as f:
data = json.load(f)
errors.extend(data.get('errors', []))
except:
pass
# Also check if we can get recent Vite logs
try:
# Try to get the Vite process PID
with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
// Check if there's an error file from previous runs # Check if process is still running and get its logs
try { # This is a bit hacky but works for our use case
const catResult = await global.activeSandbox.runCommand({ result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
cmd: 'cat', if result.returncode == 0:
args: ['/tmp/vite-errors.json'] # Process is running, try to check for errors in output
}); # Note: We can't easily get stdout/stderr from a running process
# but we can check if there are new errors
if (catResult.exitCode === 0) { pass
const errorFileContent = await catResult.stdout(); except:
const data = JSON.parse(errorFileContent); pass
errors.push(...(data.errors || []));
} # Also scan the current console output for any HMR errors
} catch (error) { # This won't catch everything but helps with recent errors
// No error file exists, that's OK try:
} # Check if there's a log file we can read
import os
log_files = []
for root, dirs, files in os.walk('/tmp'):
for file in files:
if 'vite' in file.lower() and file.endswith('.log'):
log_files.append(os.path.join(root, file))
// Look for any Vite-related log files that might contain errors for log_file in log_files[:5]: # Check up to 5 log files
try { try:
const findResult = await global.activeSandbox.runCommand({ with open(log_file, 'r') as f:
cmd: 'find', content = f.read()
args: ['/tmp', '-name', '*vite*', '-type', 'f'] # Look for import errors
}); import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
for pkg in import_errors:
if (findResult.exitCode === 0) { if not pkg.startswith('.'):
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim()); # Extract base package name
if pkg.startswith('@'):
for (const logFile of logFiles.slice(0, 3)) { parts = pkg.split('/')
try { final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
const grepResult = await global.activeSandbox.runCommand({ else:
cmd: 'grep', final_pkg = pkg.split('/')[0]
args: ['-i', 'failed to resolve import', logFile]
}); error_obj = {
"type": "npm-missing",
if (grepResult.exitCode === 0) { "package": final_pkg,
const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim()); "message": f"Failed to resolve import \\"{pkg}\\"",
"file": "Unknown"
for (const line of errorLines) { }
// Extract package name from error line
const importMatch = line.match(/"([^"]+)"/); # Avoid duplicates
if (importMatch) { if not any(e['package'] == error_obj['package'] for e in errors):
const importPath = importMatch[1]; errors.append(error_obj)
except:
// Skip relative imports pass
if (!importPath.startsWith('.')) { except Exception as e:
// Extract base package name print(f"Error scanning logs: {e}")
let packageName;
if (importPath.startsWith('@')) { # Deduplicate errors
const parts = importPath.split('/'); unique_errors = []
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath; seen_packages = set()
} else { for error in errors:
packageName = importPath.split('/')[0]; if error.get('package') and error['package'] not in seen_packages:
} seen_packages.add(error['package'])
unique_errors.append(error)
const errorObj = {
type: "npm-missing", print(json.dumps({"errors": unique_errors}))
package: packageName, `, { timeout: 5000 });
message: `Failed to resolve import "${importPath}"`,
file: "Unknown"
};
// Avoid duplicates
if (!errors.some(e => e.package === errorObj.package)) {
errors.push(errorObj);
}
}
}
}
}
} catch (error) {
// Skip if grep fails
}
}
}
} catch (error) {
// No log files found, that's OK
}
// Deduplicate errors by package name const data = JSON.parse(result.output || '{"errors": []}');
const uniqueErrors: any[] = [];
const seenPackages = new Set<string>();
for (const error of errors) {
if (error.package && !seenPackages.has(error.package)) {
seenPackages.add(error.package);
uniqueErrors.push(error);
}
}
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
hasErrors: uniqueErrors.length > 0, hasErrors: data.errors.length > 0,
errors: uniqueErrors errors: data.errors
}); });
} catch (error) { } catch (error) {
+106 -36
View File
@@ -15,45 +15,115 @@ export async function POST() {
console.log('[restart-vite] Forcing Vite restart...'); console.log('[restart-vite] Forcing Vite restart...');
// Kill existing Vite processes // Kill existing Vite process and restart
try { const result = await global.activeSandbox.runCode(`
await global.activeSandbox.runCommand({ import subprocess
cmd: 'pkill', import os
args: ['-f', 'vite'] import signal
}); import time
console.log('[restart-vite] Killed existing Vite processes'); import threading
import json
// Wait a moment for processes to terminate import sys
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) { # Kill existing Vite process
console.log('[restart-vite] No existing Vite processes found'); try:
} with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
// Clear any error tracking files os.kill(pid, signal.SIGTERM)
try { print("Killed existing Vite process")
await global.activeSandbox.runCommand({ time.sleep(1)
cmd: 'bash', except:
args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json'] print("No existing Vite process found")
});
} catch (error) { os.chdir('/home/user/app')
// Ignore if this fails
} # Clear error file
error_file = '/tmp/vite-errors.json'
// Start Vite dev server in detached mode with open(error_file, 'w') as f:
const viteProcess = await global.activeSandbox.runCommand({ json.dump({"errors": [], "lastChecked": time.time()}, f)
cmd: 'npm',
args: ['run', 'dev'], # Function to monitor Vite output for errors
detached: true def monitor_output(proc, error_file):
}); while True:
line = proc.stderr.readline()
console.log('[restart-vite] Vite dev server restarted'); if not line:
break
// Wait for Vite to start up
await new Promise(resolve => setTimeout(resolve, 3000)); sys.stdout.write(line) # Also print to console
# Check for import resolution errors
if "Failed to resolve import" in line:
try:
# Extract package name from error
import_match = line.find('"')
if import_match != -1:
end_match = line.find('"', import_match + 1)
if end_match != -1:
package_name = line[import_match + 1:end_match]
# Skip relative imports
if not package_name.startswith('.'):
with open(error_file, 'r') as f:
data = json.load(f)
# Handle scoped packages correctly
if package_name.startswith('@'):
# For @scope/package, keep the scope
pkg_parts = package_name.split('/')
if len(pkg_parts) >= 2:
final_package = '/'.join(pkg_parts[:2])
else:
final_package = package_name
else:
# For regular packages, just take the first part
final_package = package_name.split('/')[0]
error_obj = {
"type": "npm-missing",
"package": final_package,
"message": line.strip(),
"timestamp": time.time()
}
# Avoid duplicates
if not any(e['package'] == error_obj['package'] for e in data['errors']):
data['errors'].append(error_obj)
with open(error_file, 'w') as f:
json.dump(data, f)
print(f"WARNING: Detected missing package: {error_obj['package']}")
except Exception as e:
print(f"Error parsing Vite error: {e}")
# Start Vite with error monitoring
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
# Start monitoring thread
monitor_thread = threading.Thread(target=monitor_output, args=(process, error_file))
monitor_thread.daemon = True
monitor_thread.start()
print("Vite restarted successfully!")
# Store process info for later
with open('/tmp/vite-process.pid', 'w') as f:
f.write(str(process.pid))
# Wait for Vite to fully start
time.sleep(5)
print("Vite is ready")
`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Vite restarted successfully' message: 'Vite restarted successfully',
output: result.output
}); });
} catch (error) { } catch (error) {
+20 -21
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
// Get active sandbox from global state (in production, use a proper state management solution) // Get active sandbox from global state (in production, use a proper state management solution)
declare global { declare global {
@@ -25,32 +26,30 @@ export async function POST(request: NextRequest) {
console.log(`[run-command] Executing: ${command}`); console.log(`[run-command] Executing: ${command}`);
// Parse command and arguments const result = await global.activeSandbox.runCode(`
const commandParts = command.trim().split(/\s+/); import subprocess
const cmd = commandParts[0]; import os
const args = commandParts.slice(1);
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}")
`);
// Execute command using Vercel Sandbox const output = result.logs.stdout.join('\n');
const result = await global.activeSandbox.runCommand({
cmd,
args
});
// Get output streams
const stdout = await result.stdout();
const stderr = await result.stderr();
const output = [
stdout ? `STDOUT:\n${stdout}` : '',
stderr ? `\nSTDERR:\n${stderr}` : '',
`\nExit code: ${result.exitCode}`
].filter(Boolean).join('');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
output, output,
exitCode: result.exitCode, message: 'Command executed successfully'
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
}); });
} catch (error) { } catch (error) {
+42 -57
View File
@@ -15,70 +15,55 @@ export async function GET(request: NextRequest) {
console.log('[sandbox-logs] Fetching Vite dev server logs...'); console.log('[sandbox-logs] Fetching Vite dev server logs...');
// Check if Vite processes are running // Get the last N lines of the Vite dev server output
const psResult = await global.activeSandbox.runCommand({ const result = await global.activeSandbox.runCode(`
cmd: 'ps', import subprocess
args: ['aux'] import os
});
# Try to get the Vite process output
try:
# Read the last 100 lines of any log files
log_content = []
let viteRunning = false; # Check if there are any node processes running
let logContent: string[] = []; ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()]
if (psResult.exitCode === 0) { if vite_processes:
const psOutput = await psResult.stdout(); log_content.append("Vite is running")
const viteProcesses = psOutput.split('\n').filter(line => else:
line.toLowerCase().includes('vite') || log_content.append("Vite process not found")
line.toLowerCase().includes('npm run dev')
); # Try to capture recent console output (this is a simplified approach)
# In a real implementation, you'd want to capture the Vite process output directly
viteRunning = viteProcesses.length > 0; print(json.dumps({
"hasErrors": False,
if (viteRunning) { "logs": log_content,
logContent.push("Vite is running"); "status": "running" if vite_processes else "stopped"
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes }))
} else { except Exception as e:
logContent.push("Vite process not found"); print(json.dumps({
} "hasErrors": True,
} "logs": [str(e)],
"status": "error"
}))
`);
// Try to read any recent log files
try { try {
const findResult = await global.activeSandbox.runCommand({ const logData = JSON.parse(result.output || '{}');
cmd: 'find', return NextResponse.json({
args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f'] success: true,
...logData
});
} catch {
return NextResponse.json({
success: true,
hasErrors: false,
logs: [result.output],
status: 'unknown'
}); });
if (findResult.exitCode === 0) {
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
for (const logFile of logFiles.slice(0, 2)) {
try {
const catResult = await global.activeSandbox.runCommand({
cmd: 'tail',
args: ['-n', '10', logFile]
});
if (catResult.exitCode === 0) {
const logFileContent = await catResult.stdout();
logContent.push(`--- ${logFile} ---`);
logContent.push(logFileContent);
}
} catch (error) {
// Skip if can't read log file
}
}
}
} catch (error) {
// No log files found, that's OK
} }
return NextResponse.json({
success: true,
hasErrors: false,
logs: logContent,
status: viteRunning ? 'running' : 'stopped'
});
} catch (error) { } catch (error) {
console.error('[sandbox-logs] Error:', error); console.error('[sandbox-logs] Error:', error);
return NextResponse.json({ return NextResponse.json({
+35 -40
View File
@@ -1,24 +1,32 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react'; import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
interface SandboxPreviewProps { interface SandboxPreviewProps {
sandboxId: string;
port: number;
type: 'vite' | 'nextjs' | 'console'; type: 'vite' | 'nextjs' | 'console';
output?: string; output?: string;
isLoading?: boolean; isLoading?: boolean;
sandboxUrl?: string; // Real URL from Vercel Sandbox API
} }
export default function SandboxPreview({ export default function SandboxPreview({
sandboxId,
port,
type, type,
output, output,
isLoading = false, isLoading = false
sandboxUrl
}: SandboxPreviewProps) { }: SandboxPreviewProps) {
const [previewUrl, setPreviewUrl] = useState<string>('');
const [showConsole, setShowConsole] = useState(false); const [showConsole, setShowConsole] = useState(false);
const [iframeKey, setIframeKey] = useState(0); const [iframeKey, setIframeKey] = useState(0);
// Use the real sandbox URL passed from the API useEffect(() => {
const previewUrl = sandboxUrl || ''; if (sandboxId && type !== 'console') {
// In production, this would be the actual E2B sandbox URL
// Format: https://{sandboxId}-{port}.e2b.dev
setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`);
}
}, [sandboxId, port, type]);
const handleRefresh = () => { const handleRefresh = () => {
setIframeKey(prev => prev + 1); setIframeKey(prev => prev + 1);
@@ -42,13 +50,9 @@ export default function SandboxPreview({
<span className="text-sm text-gray-400"> <span className="text-sm text-gray-400">
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview {type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
</span> </span>
{previewUrl ? ( <code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400"> {previewUrl}
{previewUrl} </code>
</code>
) : (
<span className="text-xs text-gray-500">Waiting for sandbox URL...</span>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
@@ -65,47 +69,38 @@ export default function SandboxPreview({
> >
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
</button> </button>
{previewUrl && ( <a
<a href={previewUrl}
href={previewUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="p-2 hover:bg-gray-700 rounded transition-colors"
className="p-2 hover:bg-gray-700 rounded transition-colors" title="Open in new tab"
title="Open in new tab" >
> <ExternalLink className="w-4 h-4" />
<ExternalLink className="w-4 h-4" /> </a>
</a>
)}
</div> </div>
</div> </div>
{/* Main Preview */} {/* Main Preview */}
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700"> <div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
{(isLoading || !previewUrl) && ( {isLoading && (
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10"> <div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
<div className="text-center"> <div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" /> <Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
{!previewUrl {type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'}
? 'Setting up sandbox environment...'
: type === 'vite'
? 'Starting Vite dev server...'
: 'Starting Next.js dev server...'
}
</p> </p>
</div> </div>
</div> </div>
)} )}
{previewUrl && ( <iframe
<iframe key={iframeKey}
key={iframeKey} src={previewUrl}
src={previewUrl} className="w-full h-[600px] bg-white"
className="w-full h-[600px] bg-white" title={`${type} preview`}
title={`${type} preview`} sandbox="allow-scripts allow-same-origin allow-forms"
sandbox="allow-scripts allow-same-origin allow-forms" />
/>
)}
</div> </div>
{/* Console Output (Toggle) */} {/* Console Output (Toggle) */}
+11 -14
View File
@@ -2,30 +2,27 @@
// This file contains all configurable settings for the application // This file contains all configurable settings for the application
export const appConfig = { export const appConfig = {
// Vercel Sandbox Configuration // E2B Sandbox Configuration
vercelSandbox: { e2b: {
// Sandbox timeout in minutes // Sandbox timeout in minutes
timeoutMinutes: 15, timeoutMinutes: 15,
// Convert to milliseconds for Vercel Sandbox API // Convert to milliseconds for E2B API
get timeoutMs() { get timeoutMs() {
return this.timeoutMinutes * 60 * 1000; return this.timeoutMinutes * 60 * 1000;
}, },
// Development server port (Vercel Sandbox typically uses 3000 for Next.js/React) // Vite development server port
devPort: 3000, vitePort: 5173,
// Time to wait for dev server to be ready (in milliseconds) // Time to wait for Vite to be ready (in milliseconds)
devServerStartupDelay: 7000, viteStartupDelay: 7000,
// Time to wait for CSS rebuild (in milliseconds) // Time to wait for CSS rebuild (in milliseconds)
cssRebuildDelay: 2000, cssRebuildDelay: 2000,
// Working directory in sandbox // Default sandbox template (if using templates)
workingDirectory: '/app', defaultTemplate: undefined, // or specify a template ID
// Default runtime for sandbox
runtime: 'node22' // Available: node22, python3.13, v0-next-shadcn, cua-ubuntu-xfce
}, },
// AI Model Configuration // AI Model Configuration
@@ -38,7 +35,7 @@ export const appConfig = {
'openai/gpt-5', 'openai/gpt-5',
'moonshotai/kimi-k2-instruct', 'moonshotai/kimi-k2-instruct',
'anthropic/claude-sonnet-4-20250514', 'anthropic/claude-sonnet-4-20250514',
'google/gemini-2.0-flash-exp' 'google/gemini-2.5-pro'
], ],
// Model display names // Model display names
@@ -46,7 +43,7 @@ export const appConfig = {
'openai/gpt-5': 'GPT-5', 'openai/gpt-5': 'GPT-5',
'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct', 'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct',
'anthropic/claude-sonnet-4-20250514': 'Sonnet 4', 'anthropic/claude-sonnet-4-20250514': 'Sonnet 4',
'google/gemini-2.0-flash-exp': 'Gemini 2.0 Flash (Experimental)' 'google/gemini-2.5-pro': 'Gemini 2.5 Pro'
}, },
// Temperature settings for non-reasoning models // Temperature settings for non-reasoning models
+27 -21
View File
@@ -1,10 +1,10 @@
# Package Detection and Installation Guide # Package Detection and Installation Guide
This document explains how to use the XML-based package detection and installation mechanism in the Vercel Sandbox environment. This document explains how to use the XML-based package detection and installation mechanism in the E2B sandbox environment.
## Overview ## Overview
The Vercel Sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system. The E2B sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system.
## XML Tag Formats ## XML Tag Formats
@@ -196,37 +196,43 @@ Directly installs packages in the sandbox.
3. **Order matters**: Packages are installed before files are created 3. **Order matters**: Packages are installed before files are created
4. **Use commands** for post-installation tasks like building or testing 4. **Use commands** for post-installation tasks like building or testing
## Integration with Vercel Sandbox ## Integration with E2B Sandbox
The package detection mechanism integrates seamlessly with the Vercel Sandbox: The package detection mechanism integrates seamlessly with the E2B sandbox:
1. Packages are installed in the sandbox's working directory 1. Packages are installed in `/home/user/app/node_modules`
2. The development server is automatically restarted after package installation 2. The Vite dev server is automatically restarted after package installation
3. All npm operations run within the sandbox environment 3. All npm operations run within the sandbox environment
4. Package.json is automatically updated with new dependencies 4. Package.json is automatically updated with new dependencies
## Vercel Sandbox Command Execution Methods ## E2B Command Execution Methods
### Using runCommand() (Recommended) ### Method 1: Using runCode() with Python subprocess
```javascript ```javascript
// Direct command execution using Vercel Sandbox API // Current implementation pattern
const result = await global.activeSandbox.runCommand({ await global.activeSandbox.runCode(`
cmd: 'npm', import subprocess
args: ['install', 'axios'] import os
os.chdir('/home/user/app')
result = subprocess.run(['npm', 'install', 'axios'], capture_output=True, text=True)
print(result.stdout)
`);
```
### Method 2: Using commands.run() directly (Recommended)
```javascript
// Direct command execution - cleaner approach
const result = await global.activeSandbox.commands.run('npm install axios', {
cwd: '/home/user/app',
timeout: 60000
}); });
const stdout = await result.stdout(); console.log(result.stdout);
const stderr = await result.stderr();
console.log(stdout);
``` ```
### Command Execution Options ### Command Execution Options
When using `sandbox.runCommand()`, you can specify: When using `sandbox.commands.run()`, you can specify:
- `cmd`: The command to execute
- `args`: Array of arguments
- `detached`: Run in background (for long-running processes)
- `stdout`: Stream for capturing stdout
- `stderr`: Stream for capturing stderr
- `cmd`: Command string to execute - `cmd`: Command string to execute
- `background`: Run in background (true) or wait for completion (false) - `background`: Run in background (true) or wait for completion (false)
- `envs`: Environment variables as key-value pairs - `envs`: Environment variables as key-value pairs
-21
View File
@@ -1,21 +0,0 @@
# Required
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
# Vercel Sandbox Authentication (choose one method)
# See: https://vercel.com/docs/vercel-sandbox#authentication
# Method 1: OIDC Token (recommended for development)
# Run `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
# Method 2: Personal Access Token (for production or when OIDC unavailable)
# Get these values from your Vercel dashboard
# VERCEL_TEAM_ID=team_xxxxxxxxx
# VERCEL_PROJECT_ID=prj_xxxxxxxxx
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx
# Optional AI providers (need at least one)
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference)
+612 -159
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -8,6 +8,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test:integration": "node tests/e2b-integration.test.js",
"test:api": "node tests/api-endpoints.test.js", "test:api": "node tests/api-endpoints.test.js",
"test:code": "node tests/code-execution.test.js", "test:code": "node tests/code-execution.test.js",
"test:all": "npm run test:integration && npm run test:api && npm run test:code" "test:all": "npm run test:integration && npm run test:api && npm run test:code"
@@ -18,7 +19,7 @@
"@ai-sdk/groq": "^2.0.0", "@ai-sdk/groq": "^2.0.0",
"@ai-sdk/openai": "^2.0.4", "@ai-sdk/openai": "^2.0.4",
"@anthropic-ai/sdk": "^0.57.0", "@anthropic-ai/sdk": "^0.57.0",
"@vercel/sandbox": "^0.0.17", "@e2b/code-interpreter": "^1.5.1",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
@@ -28,7 +29,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"e2b": "^1.13.2",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"groq-sdk": "^0.29.0", "groq-sdk": "^0.29.0",
"lucide-react": "^0.532.0", "lucide-react": "^0.532.0",