Merge pull request #109 from MFCo/vercel-sandbox

Vercel sandbox support
This commit is contained in:
Developers Digest
2025-09-02 18:23:23 -04:00
committed by GitHub
22 changed files with 1072 additions and 1669 deletions
+16 -16
View File
@@ -1,20 +1,20 @@
# REQUIRED - Sandboxes for code execution # Required
# Get yours at https://e2b.dev FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
E2B_API_KEY=your_e2b_api_key_here
# REQUIRED - Web scraping for cloning websites # Vercel Sandbox Authentication (choose one method)
# Get yours at https://firecrawl.dev # See: https://vercel.com/docs/vercel-sandbox#authentication
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
# OPTIONAL - AI Providers (need at least one) # Method 1: OIDC Token (recommended for development)
# Get yours at https://console.anthropic.com # Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
ANTHROPIC_API_KEY=your_anthropic_api_key_here # VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
# Get yours at https://platform.openai.com # Method 2: Personal Access Token (for production or when OIDC unavailable)
OPENAI_API_KEY=your_openai_api_key_here # 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
# Get yours at https://aistudio.google.com/app/apikey # Optional (need at least one AI provider)
GEMINI_API_KEY=your_gemini_api_key_here 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)
# Get yours at https://console.groq.com GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
GROQ_API_KEY=your_groq_api_key_here GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended)
+1
View File
@@ -56,3 +56,4 @@ e2b-template-*
*.temp *.temp
repomix-output.txt repomix-output.txt
bun.lockb bun.lockb
.env*.local
+13 -1
View File
@@ -16,11 +16,23 @@ 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)
+35 -24
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter'; import { Sandbox } from '@vercel/sandbox';
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,7 +525,6 @@ 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)
@@ -534,19 +533,20 @@ export async function POST(request: NextRequest) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
} }
// Write the file using Python (code-interpreter SDK) // Create directory if needed
const escapedContent = fileContent const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
.replace(/\\/g, '\\\\') if (dirPath) {
.replace(/"""/g, '\\"\\"\\"') await sandboxInstance.runCommand({
.replace(/\$/g, '\\$'); cmd: 'mkdir',
args: ['-p', dirPath]
});
}
await sandboxInstance.runCode(` // Write the file using Vercel Sandbox writeFiles
import os await sandboxInstance.writeFiles([{
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True) path: normalizedPath,
with open("${fullPath}", 'w') as f: content: Buffer.from(fileContent)
f.write("""${escapedContent}""") }]);
print(f"File written: ${fullPath}")
`);
// Update file cache // Update file cache
if (global.sandboxState?.fileCache) { if (global.sandboxState?.fileCache) {
@@ -599,27 +599,38 @@ print(f"File written: ${fullPath}")
action: 'executing' action: 'executing'
}); });
// Use E2B commands.run() for cleaner execution // Parse command and arguments for Vercel Sandbox
const result = await sandboxInstance.commands.run(cmd, { const commandParts = cmd.trim().split(/\s+/);
cwd: '/home/user/app', const cmdName = commandParts[0];
timeout: 60, const args = commandParts.slice(1);
on_stdout: async (data: string) => {
// Use Vercel Sandbox runCommand
const result = await sandboxInstance.runCommand({
cmd: cmdName,
args
});
// Get command output
const stdout = await result.stdout();
const stderr = await result.stderr();
if (stdout) {
await sendProgress({ await sendProgress({
type: 'command-output', type: 'command-output',
command: cmd, command: cmd,
output: data, output: stdout,
stream: 'stdout' stream: 'stdout'
}); });
}, }
on_stderr: async (data: string) => {
if (stderr) {
await sendProgress({ await sendProgress({
type: 'command-output', type: 'command-output',
command: cmd, command: cmd,
output: data, output: stderr,
stream: 'stderr' stream: 'stderr'
}); });
} }
});
if (results.commandsExecuted) { if (results.commandsExecuted) {
results.commandsExecuted.push(cmd); results.commandsExecuted.push(cmd);
+30 -25
View File
@@ -432,15 +432,12 @@ function App() {
export default App;`; export default App;`;
try { try {
await global.activeSandbox.runCode(` await global.activeSandbox.writeFiles([{
file_path = "/home/user/app/src/App.jsx" path: 'src/App.jsx',
file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}""" content: Buffer.from(appContent)
}]);
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}`);
@@ -459,9 +456,7 @@ print(f"Auto-generated: {file_path}")
if (!isEdit && !indexCssInParsed && !indexCssExists) { if (!isEdit && !indexCssInParsed && !indexCssExists) {
try { try {
await global.activeSandbox.runCode(` const indexCssContent = `@tailwind base;
file_path = "/home/user/app/src/index.css"
file_content = """@tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@@ -483,13 +478,14 @@ body {
margin: 0; margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
}""" }`;
with open(file_path, 'w') as f: await global.activeSandbox.writeFiles([{
f.write(file_content) path: 'src/index.css',
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');
@@ -500,15 +496,24 @@ print(f"Auto-generated: {file_path}")
// Execute commands // Execute commands
for (const cmd of parsed.commands) { for (const cmd of parsed.commands) {
try { try {
await global.activeSandbox.runCode(` // Parse command and arguments
import subprocess const commandParts = cmd.trim().split(/\s+/);
os.chdir('/home/user/app') const cmdName = commandParts[0];
result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True) const args = commandParts.slice(1);
print(f"Executed: ${cmd}")
print(result.stdout) // Execute command using Vercel Sandbox
if result.stderr: const result = await global.activeSandbox.runCommand({
print(f"Errors: {result.stderr}") cmd: cmdName,
`); 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}`);
+138 -176
View File
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter'; import { Sandbox } from '@vercel/sandbox';
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 base sandbox...'); console.log('[create-ai-sandbox] Creating Vercel sandbox...');
// Kill existing sandbox if any // Kill existing sandbox if any
if (global.activeSandbox) { if (global.activeSandbox) {
console.log('[create-ai-sandbox] Killing existing sandbox...'); console.log('[create-ai-sandbox] Stopping existing sandbox...');
try { try {
await global.activeSandbox.kill(); await global.activeSandbox.stop();
} catch (e) { } catch (e) {
console.error('Failed to close existing sandbox:', e); console.error('Failed to stop existing sandbox:', e);
} }
global.activeSandbox = null; global.activeSandbox = null;
} }
@@ -35,39 +35,79 @@ export async function POST() {
global.existingFiles = new Set<string>(); global.existingFiles = new Set<string>();
} }
// Create base sandbox - we'll set up Vite ourselves for full control // Create Vercel sandbox with flexible authentication
console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`); console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
sandbox = await Sandbox.create({
apiKey: process.env.E2B_API_KEY,
timeoutMs: appConfig.e2b.timeoutMs
});
const sandboxId = (sandbox as any).sandboxId || Date.now().toString(); // Prepare sandbox configuration
const host = (sandbox as any).getHost(appConfig.e2b.vitePort); const sandboxConfig: any = {
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 using Python to write files // Set up a basic Vite React app
console.log('[create-ai-sandbox] Setting up Vite React app...'); console.log('[create-ai-sandbox] Setting up Vite React app...');
// Write all files in a single Python script to avoid multiple executions // First, change to the working directory
const setupScript = ` await sandbox.runCommand('pwd');
import os const workDir = appConfig.vercelSandbox.workingDirectory;
import json
print('Setting up React app with Vite and Tailwind...') // Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
# Create directory structure // Extract the hostname from the sandbox URL for Vite config
os.makedirs('/home/user/app/src', exist_ok=True) const sandboxHostname = new URL(sandboxUrl).hostname;
console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
# Package.json // Create the Vite config content with the proper hostname (using string concatenation)
package_json = { const viteConfigContent = `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Vercel Sandbox compatible Vite configuration
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: ${appConfig.vercelSandbox.devPort},
strictPort: true,
hmr: true,
allowedHosts: [
'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)
const projectFiles = [
{
path: 'package.json',
content: Buffer.from(JSON.stringify({
"name": "sandbox-app", "name": "sandbox-app",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host --port 3000",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
@@ -82,34 +122,15 @@ package_json = {
"postcss": "^8.4.31", "postcss": "^8.4.31",
"autoprefixer": "^10.4.16" "autoprefixer": "^10.4.16"
} }
} }, null, 2))
},
with open('/home/user/app/package.json', 'w') as f: {
json.dump(package_json, f, indent=2) path: 'vite.config.js',
print('✓ package.json') content: Buffer.from(viteConfigContent)
},
# Vite config for E2B - with allowedHosts {
vite_config = """import { defineConfig } from 'vite' path: 'tailwind.config.js',
import react from '@vitejs/plugin-react' content: Buffer.from(`/** @type {import('tailwindcss').Config} */
// E2B-compatible Vite configuration
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 - standard without custom design tokens
tailwind_config = """/** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: [
"./index.html", "./index.html",
@@ -119,26 +140,20 @@ export default {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
}""" }`)
},
with open('/home/user/app/tailwind.config.js', 'w') as f: {
f.write(tailwind_config) path: 'postcss.config.js',
print('✓ tailwind.config.js') content: Buffer.from(`export default {
# PostCSS config
postcss_config = """export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}""" }`)
},
with open('/home/user/app/postcss.config.js', 'w') as f: {
f.write(postcss_config) path: 'index.html',
print('✓ postcss.config.js') content: Buffer.from(`<!DOCTYPE html>
# Index.html
index_html = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -149,14 +164,11 @@ index_html = """<!DOCTYPE html>
<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: {
f.write(index_html) path: 'src/main.jsx',
print('✓ index.html') content: Buffer.from(`import React from 'react'
# 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'
@@ -165,19 +177,18 @@ 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: {
f.write(main_jsx) path: 'src/App.jsx',
print('✓ src/main.jsx') content: Buffer.from(`function App() {
# 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>
@@ -185,14 +196,11 @@ app_jsx = """function App() {
) )
} }
export default App""" export default App`)
},
with open('/home/user/app/src/App.jsx', 'w') as f: {
f.write(app_jsx) path: 'src/index.css',
print('✓ src/App.jsx') content: Buffer.from(`@tailwind base;
# Index.css with explicit Tailwind directives
index_css = """@tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@@ -216,99 +224,53 @@ index_css = """@tailwind base;
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);
}""" }`)
}
];
with open('/home/user/app/src/index.css', 'w') as f: // Create directory structure first
f.write(index_css) await sandbox.runCommand({
print('✓ src/index.css') cmd: 'mkdir',
args: ['-p', 'src']
});
print('\\nAll files created successfully!') // Write all files
`; await sandbox.writeFiles(projectFiles);
console.log('[create-ai-sandbox] ✓ Project files created');
// Execute the setup script
await sandbox.runCode(setupScript);
// Install dependencies // Install dependencies
console.log('[create-ai-sandbox] Installing dependencies...'); console.log('[create-ai-sandbox] Installing dependencies...');
await sandbox.runCode(` const installResult = await sandbox.runCommand({
import subprocess cmd: 'npm',
import sys args: ['install', '--loglevel', 'info']
});
if (installResult.exitCode === 0) {
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully');
} else {
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...');
}
print('Installing npm packages...') // Start Vite dev server in detached mode
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}')
# Continue anyway as it might still work
`);
// Start Vite dev server
console.log('[create-ai-sandbox] Starting Vite dev server...'); console.log('[create-ai-sandbox] Starting Vite dev server...');
await sandbox.runCode(` const viteProcess = await sandbox.runCommand({
import subprocess cmd: 'npm',
import os args: ['run', 'dev'],
import time detached: true
});
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.e2b.viteStartupDelay)); await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay));
// 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: `https://${host}` url: sandboxUrl,
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: {
@@ -319,7 +281,7 @@ print('✓ Tailwind CSS should be loaded')
sandbox, sandbox,
sandboxData: { sandboxData: {
sandboxId, sandboxId,
url: `https://${host}` url: sandboxUrl
} }
}; };
@@ -333,13 +295,13 @@ print('✓ Tailwind CSS should be loaded')
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:', `https://${host}`); console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
sandboxId, sandboxId,
url: `https://${host}`, url: sandboxUrl,
message: 'Sandbox created and Vite React app initialized' message: 'Vercel sandbox created and Vite React app initialized'
}); });
} catch (error) { } catch (error) {
@@ -348,9 +310,9 @@ print('✓ Tailwind CSS should be loaded')
// Clean up on error // Clean up on error
if (sandbox) { if (sandbox) {
try { try {
await sandbox.kill(); await sandbox.stop();
} catch (e) { } catch (e) {
console.error('Failed to close sandbox on error:', e); console.error('Failed to stop sandbox on error:', e);
} }
} }
+30 -31
View File
@@ -15,41 +15,37 @@ 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 // Create zip file in sandbox using standard commands
const result = await global.activeSandbox.runCode(` const zipResult = await global.activeSandbox.runCommand({
import zipfile cmd: 'bash',
import os args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`]
import json });
os.chdir('/home/user/app') if (zipResult.exitCode !== 0) {
const error = await zipResult.stderr();
throw new Error(`Failed to create zip: ${error}`);
}
# Create zip file const sizeResult = await global.activeSandbox.runCommand({
with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf: cmd: 'bash',
for root, dirs, files in os.walk('.'): args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
# Skip node_modules and .git });
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
for file in files: const fileSize = await sizeResult.stdout();
file_path = os.path.join(root, file) console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`);
arcname = os.path.relpath(file_path, '.')
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.runCode(` const readResult = await global.activeSandbox.runCommand({
import base64 cmd: 'base64',
args: ['/tmp/project.zip']
});
with open('/tmp/project.zip', 'rb') as f: if (readResult.exitCode !== 0) {
content = f.read() const error = await readResult.stderr();
encoded = base64.b64encode(content).decode('utf-8') throw new Error(`Failed to read zip file: ${error}`);
print(encoded) }
`);
const base64Content = readResult.logs.stdout.join('').trim(); 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}`;
@@ -57,15 +53,18 @@ with open('/tmp/project.zip', 'rb') as f:
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
dataUrl, dataUrl,
fileName: 'e2b-project.zip', fileName: 'vercel-sandbox-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 }
);
} }
} }
+56 -128
View File
@@ -64,15 +64,7 @@ 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;
// Extract package name (handle scoped packages and subpaths)
const parts = imp.split('/');
if (imp.startsWith('@')) {
// Scoped package like @vitejs/plugin-react
return true; 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)
@@ -101,153 +93,89 @@ export async function POST(request: NextRequest) {
} }
// Check which packages are already installed // Check which packages are already installed
const checkResult = await global.activeSandbox.runCode(` const installed: string[] = [];
import os const missing: string[] = [];
import json
installed = [] for (const packageName of uniquePackages) {
missing = [] try {
const checkResult = await global.activeSandbox.runCommand({
cmd: 'test',
args: ['-d', `node_modules/${packageName}`]
});
packages = ${JSON.stringify(uniquePackages)} if (checkResult.exitCode === 0) {
installed.push(packageName);
} else {
missing.push(packageName);
}
} catch (error) {
// If test command fails, assume package is missing
missing.push(packageName);
}
}
for package in packages: console.log('[detect-and-install-packages] Package status:', { installed, missing });
# 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}"
if os.path.exists(package_path): if (missing.length === 0) {
installed.append(package)
else:
missing.append(package)
result = {
'installed': installed,
'missing': missing
}
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: status.installed, packagesAlreadyInstalled: 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:', status.missing); console.log('[detect-and-install-packages] Installing packages:', missing);
const installResult = await global.activeSandbox.runCode(` const installResult = await global.activeSandbox.runCommand({
import subprocess cmd: 'npm',
import os args: ['install', '--save', ...missing]
import json });
os.chdir('/home/user/app') const stdout = await installResult.stdout();
packages_to_install = ${JSON.stringify(status.missing)} const stderr = await installResult.stderr();
# Join packages into a single install command console.log('[detect-and-install-packages] Install stdout:', stdout);
packages_str = ' '.join(packages_to_install) if (stderr) {
cmd = f'npm install {packages_str} --save' console.log('[detect-and-install-packages] Install stderr:', stderr);
}
print(f"Running: {cmd}") // Verify installation
const finalInstalled: string[] = [];
const failed: string[] = [];
# Run npm install with explicit save flag for (const packageName of missing) {
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}"
if os.path.exists(package_path):
installed.append(package)
print(f"✓ Verified installation of {package}")
else:
# Check if it's a submodule of an installed package
base_package = package.split('/')[0]
if package.startswith('@'):
# For @scope/package, the base is @scope/package
base_package = '/'.join(package.split('/')[:2])
base_path = f"/home/user/app/node_modules/{base_package}"
if os.path.exists(base_path):
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 { try {
const stdout = installResult.logs.stdout.join(''); const verifyResult = await global.activeSandbox.runCommand({
const resultMatch = stdout.match(/Result:\s*({.*})/); cmd: 'test',
if (resultMatch) { args: ['-d', `node_modules/${packageName}`]
installStatus = JSON.parse(resultMatch[1]); });
if (verifyResult.exitCode === 0) {
finalInstalled.push(packageName);
console.log(`✓ Verified installation of ${packageName}`);
} else { } else {
// Fallback parsing failed.push(packageName);
const lines = stdout.split('\n'); console.log(`✗ Failed to verify installation of ${packageName}`);
const resultLine = lines.find((line: string) => line.includes('Result:'));
if (resultLine) {
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
} else {
throw new Error('Could not find Result in output');
} }
} 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 (installStatus.failed.length > 0) { if (failed.length > 0) {
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed); console.error('[detect-and-install-packages] Failed to install:', failed);
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
packagesInstalled: installStatus.installed, packagesInstalled: finalInstalled,
packagesFailed: installStatus.failed, packagesFailed: failed,
packagesAlreadyInstalled: status.installed, packagesAlreadyInstalled: installed,
message: `Installed ${installStatus.installed.length} packages`, message: `Installed ${finalInstalled.length} packages`,
logs: installResult.logs.stdout.join('\n') logs: stdout
}); });
} catch (error) { } catch (error) {
+47 -5
View File
@@ -11,6 +11,9 @@ 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,
}); });
@@ -1156,9 +1159,21 @@ 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/', '') :
(model === 'openai/gpt-5') ? 'gpt-5' : // Fix model name transformation for different providers
(isGoogle ? model.replace('google/', '') : model); let actualModel: string;
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 = {
@@ -1243,7 +1258,28 @@ It's better to have 3 complete files than 10 incomplete files.`
}; };
} }
const result = await streamText(streamOptions); let result;
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 = '';
@@ -1715,12 +1751,18 @@ Provide the complete file content without any truncation. Include all necessary
} }
})(); })();
// Return the stream // Return the stream with proper headers for streaming support
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',
}, },
}); });
+73 -50
View File
@@ -18,58 +18,81 @@ 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 all React/JS/CSS files // Get list of all relevant files
const result = await global.activeSandbox.runCode(` const findResult = await global.activeSandbox.runCommand({
import os cmd: 'find',
import json args: [
'.',
'-name', 'node_modules', '-prune', '-o',
'-name', '.git', '-prune', '-o',
'-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'
]
});
def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']): if (findResult.exitCode !== 0) {
files_content = {} throw new Error('Failed to list files');
}
for root, dirs, files in os.walk(directory): const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim());
# Skip node_modules and other unwanted directories console.log('[get-sandbox-files] Found', fileList.length, 'files');
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
for file in files: // Read content of each file (limit to reasonable sizes)
if any(file.endswith(ext) for ext in extensions): const filesContent: Record<string, string> = {};
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, '/home/user/app')
try: for (const filePath of fileList) {
with open(file_path, 'r') as f: try {
content = f.read() // Check file size first
# Only include files under 10KB to avoid huge responses const statResult = await global.activeSandbox.runCommand({
if len(content) < 10000: cmd: 'stat',
files_content[relative_path] = content args: ['-f', '%z', filePath]
except: });
pass
return files_content if (statResult.exitCode === 0) {
const fileSize = parseInt(await statResult.stdout());
# Get the files // Only read files smaller than 10KB
files = get_files_content() if (fileSize < 10000) {
const catResult = await global.activeSandbox.runCommand({
cmd: 'cat',
args: [filePath]
});
# Also get the directory structure if (catResult.exitCode === 0) {
structure = [] const content = await catResult.stdout();
for root, dirs, files in os.walk('/home/user/app'): // Remove leading './' from path
level = root.replace('/home/user/app', '').count(os.sep) const relativePath = filePath.replace(/^\.\//, '');
indent = ' ' * 2 * level filesContent[relativePath] = content;
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']): } catch (error) {
structure.append(f"{sub_indent}{file}") // Skip files that can't be read
continue;
}
}
result = { // Get directory structure
'files': files, const treeResult = await global.activeSandbox.runCommand({
'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines cmd: 'find',
} args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*']
});
print(json.dumps(result)) let structure = '';
`); if (treeResult.exitCode === 0) {
const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim());
const output = result.logs.stdout.join(''); structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
const parsedResult = JSON.parse(output); }
// Build enhanced file manifest // Build enhanced file manifest
const fileManifest: FileManifest = { const fileManifest: FileManifest = {
@@ -82,12 +105,12 @@ print(json.dumps(result))
}; };
// Process each file // Process each file
for (const [relativePath, content] of Object.entries(parsedResult.files)) { for (const [relativePath, content] of Object.entries(filesContent)) {
const fullPath = `/home/user/app/${relativePath}`; const fullPath = `/${relativePath}`;
// Create base file info // Create base file info
const fileInfo: FileInfo = { const fileInfo: FileInfo = {
content: content as string, content: content,
type: 'utility', type: 'utility',
path: fullPath, path: fullPath,
relativePath, relativePath,
@@ -96,7 +119,7 @@ print(json.dumps(result))
// Parse JavaScript/JSX files // Parse JavaScript/JSX files
if (relativePath.match(/\.(jsx?|tsx?)$/)) { if (relativePath.match(/\.(jsx?|tsx?)$/)) {
const parseResult = parseJavaScriptFile(content as string, fullPath); const parseResult = parseJavaScriptFile(content, fullPath);
Object.assign(fileInfo, parseResult); Object.assign(fileInfo, parseResult);
// Identify entry point // Identify entry point
@@ -132,9 +155,9 @@ print(json.dumps(result))
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
files: parsedResult.files, files: filesContent,
structure: parsedResult.structure, structure,
fileCount: Object.keys(parsedResult.files).length, fileCount: Object.keys(filesContent).length,
manifest: fileManifest, manifest: fileManifest,
}); });
+117 -215
View File
@@ -1,5 +1,4 @@
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;
@@ -36,23 +35,8 @@ export async function POST(request: NextRequest) {
console.log(`[install-packages] Cleaned:`, validPackages); console.log(`[install-packages] Cleaned:`, validPackages);
} }
// Try to get sandbox - either from global or reconnect // Get active sandbox
let sandbox = global.activeSandbox; const 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({
@@ -61,7 +45,7 @@ export async function POST(request: NextRequest) {
}, { status: 400 }); }, { status: 400 });
} }
console.log('[install-packages] Installing packages:', packages); console.log('[install-packages] Installing packages:', validPackages);
// Create a response stream for real-time updates // Create a response stream for real-time updates
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@@ -83,23 +67,20 @@ export async function POST(request: NextRequest) {
packages: validPackages packages: validPackages
}); });
// Kill any existing Vite process first // Stop any existing development server first
await sendProgress({ type: 'status', message: 'Stopping development server...' }); await sendProgress({ type: 'status', message: 'Stopping development server...' });
await sandboxInstance.runCode(` try {
import subprocess // Try to kill any running dev server processes
import os await sandboxInstance.runCommand({
import signal cmd: 'pkill',
args: ['-f', 'vite']
# Try to kill any existing Vite process });
try: await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
with open('/tmp/vite-process.pid', 'r') as f: } catch (error) {
pid = int(f.read().strip()) // It's OK if no process is found
os.kill(pid, signal.SIGTERM) console.log('[install-packages] No existing dev server found');
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({
@@ -107,70 +88,51 @@ except:
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;
// Check if checkResult has the expected structure
if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
const outputLines = checkResult.results[0].text.split('\n');
for (const line of outputLines) {
if (line.startsWith('NEED_INSTALL:')) {
try { try {
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length)); // Read package.json to check existing dependencies
} catch (e) { const catResult = await sandboxInstance.runCommand({
console.error('Failed to parse packages to install:', e); cmd: 'cat',
} args: ['package.json']
} });
} if (catResult.exitCode === 0) {
const packageJsonContent = await catResult.stdout();
const packageJson = JSON.parse(packageJsonContent);
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 { } else {
console.error('[install-packages] Invalid checkResult structure:', checkResult); needInstall.push(pkg);
}
}
packagesToInstall = needInstall;
if (alreadyInstalled.length > 0) {
await sendProgress({
type: 'info',
message: `Already installed: ${alreadyInstalled.join(', ')}`
});
}
}
} catch (error) {
console.error('[install-packages] Error checking existing packages:', error);
// 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',
@@ -178,164 +140,104 @@ except Exception as e:
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(', ')}`
}); });
const installResult = await sandboxInstance.runCode(` // Run npm install
import subprocess const installArgs = ['install', '--legacy-peer-deps', ...packagesToInstall];
import os const installResult = await sandboxInstance.runCommand({
cmd: 'npm',
os.chdir('/home/user/app') args: installArgs
# 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")
print(f"\\nVerified installed packages: {installed}")
`, { timeout: 60000 }); // 60 second timeout for npm install
// Send npm output
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
for (const line of npmOutputLines) {
if (line.includes('STDERR:')) {
const errorMsg = line.replace('STDERR:', '').trim();
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')) {
// Get install output
const stdout = await installResult.stdout();
const stderr = await installResult.stderr();
if (stdout) {
const lines = stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.includes('npm WARN')) {
await sendProgress({ type: 'warning', message: line }); await sendProgress({ type: 'warning', message: line });
} else if (line.trim() && !line.includes('undefined')) { } else if (line.trim()) {
await sendProgress({ type: 'output', message: line }); await sendProgress({ type: 'output', message: line });
} }
} }
// Check if installation was successful
const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
let installedPackages: string[] = [];
if (installedMatch && installedMatch[1]) {
installedPackages = installedMatch[1]
.split(',')
.map((p: string) => p.trim().replace(/'/g, ''))
.filter((p: string) => p.length > 0);
} }
if (installedPackages.length > 0) { if (stderr) {
const errorLines = stderr.split('\n').filter(line => line.trim());
for (const line of errorLines) {
if (line.includes('ERESOLVE')) {
await sendProgress({
type: 'warning',
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}`
});
} else if (line.trim()) {
await sendProgress({ type: 'error', message: line });
}
}
}
if (installResult.exitCode === 0) {
await sendProgress({ await sendProgress({
type: 'success', type: 'success',
message: `Successfully installed: ${installedPackages.join(', ')}`, message: `Successfully installed: ${packagesToInstall.join(', ')}`,
installedPackages installedPackages: packagesToInstall
}); });
} else { } else {
await sendProgress({ await sendProgress({
type: 'error', type: 'error',
message: 'Failed to verify package installation' message: 'Package installation failed'
}); });
} }
// Restart Vite dev server // Restart development server
await sendProgress({ type: 'status', message: 'Restarting development server...' }); await sendProgress({ type: 'status', message: 'Restarting development server...' });
await sandboxInstance.runCode(` try {
import subprocess const devServerProcess = await sandboxInstance.runCommand({
import os cmd: 'npm',
import time args: ['run', 'dev'],
detached: true
});
os.chdir('/home/user/app') // Wait a bit for the server to start
await new Promise(resolve => setTimeout(resolve, 3000));
# 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 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({ await sendProgress({
type: 'complete', type: 'complete',
message: 'Package installation complete and dev server restarted!', message: 'Package installation complete and dev server restarted!',
installedPackages installedPackages: packagesToInstall
}); });
} catch (error) {
await sendProgress({
type: 'error',
message: `Failed to restart dev server: ${(error as Error).message}`
});
}
} 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] Killing active sandbox...'); console.log('[kill-sandbox] Stopping active sandbox...');
let sandboxKilled = false; let sandboxKilled = false;
// Kill existing sandbox if any // Stop existing sandbox if any
if (global.activeSandbox) { if (global.activeSandbox) {
try { try {
await global.activeSandbox.close(); await global.activeSandbox.stop();
sandboxKilled = true; sandboxKilled = true;
console.log('[kill-sandbox] Sandbox closed successfully'); console.log('[kill-sandbox] Sandbox stopped successfully');
} catch (e) { } catch (e) {
console.error('[kill-sandbox] Failed to close sandbox:', e); console.error('[kill-sandbox] Failed to stop sandbox:', e);
} }
global.activeSandbox = null; global.activeSandbox = null;
global.sandboxData = null; global.sandboxData = null;
+84 -81
View File
@@ -15,97 +15,100 @@ export async function GET() {
console.log('[monitor-vite-logs] Checking Vite process logs...'); console.log('[monitor-vite-logs] Checking Vite process logs...');
// Check both the error file and recent logs const errors: any[] = [];
const result = await global.activeSandbox.runCode(`
import json
import subprocess
import re
errors = [] // Check if there's an error file from previous runs
try {
const catResult = await global.activeSandbox.runCommand({
cmd: 'cat',
args: ['/tmp/vite-errors.json']
});
# First check the error file if (catResult.exitCode === 0) {
try: const errorFileContent = await catResult.stdout();
with open('/tmp/vite-errors.json', 'r') as f: const data = JSON.parse(errorFileContent);
data = json.load(f) errors.push(...(data.errors || []));
errors.extend(data.get('errors', [])) }
except: } catch (error) {
pass // No error file exists, that's OK
# 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 process is still running and get its logs
# This is a bit hacky but works for our use case
result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
if result.returncode == 0:
# 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
pass
except:
pass
# Also scan the current console output for any HMR errors
# This won't catch everything but helps with recent errors
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))
for log_file in log_files[:5]: # Check up to 5 log files
try:
with open(log_file, 'r') as f:
content = f.read()
# Look for import errors
import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
for pkg in import_errors:
if not pkg.startswith('.'):
# Extract base package name
if pkg.startswith('@'):
parts = pkg.split('/')
final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
else:
final_pkg = pkg.split('/')[0]
error_obj = {
"type": "npm-missing",
"package": final_pkg,
"message": f"Failed to resolve import \\"{pkg}\\"",
"file": "Unknown"
} }
# Avoid duplicates // Look for any Vite-related log files that might contain errors
if not any(e['package'] == error_obj['package'] for e in errors): try {
errors.append(error_obj) const findResult = await global.activeSandbox.runCommand({
except: cmd: 'find',
pass args: ['/tmp', '-name', '*vite*', '-type', 'f']
except Exception as e: });
print(f"Error scanning logs: {e}")
# Deduplicate errors if (findResult.exitCode === 0) {
unique_errors = [] const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
seen_packages = set()
for error in errors:
if error.get('package') and error['package'] not in seen_packages:
seen_packages.add(error['package'])
unique_errors.append(error)
print(json.dumps({"errors": unique_errors})) for (const logFile of logFiles.slice(0, 3)) {
`, { timeout: 5000 }); try {
const grepResult = await global.activeSandbox.runCommand({
cmd: 'grep',
args: ['-i', 'failed to resolve import', logFile]
});
const data = JSON.parse(result.output || '{"errors": []}'); if (grepResult.exitCode === 0) {
const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim());
for (const line of errorLines) {
// Extract package name from error line
const importMatch = line.match(/"([^"]+)"/);
if (importMatch) {
const importPath = importMatch[1];
// Skip relative imports
if (!importPath.startsWith('.')) {
// Extract base package name
let packageName;
if (importPath.startsWith('@')) {
const parts = importPath.split('/');
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath;
} else {
packageName = importPath.split('/')[0];
}
const errorObj = {
type: "npm-missing",
package: packageName,
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 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: data.errors.length > 0, hasErrors: uniqueErrors.length > 0,
errors: data.errors errors: uniqueErrors
}); });
} catch (error) { } catch (error) {
+30 -100
View File
@@ -15,115 +15,45 @@ export async function POST() {
console.log('[restart-vite] Forcing Vite restart...'); console.log('[restart-vite] Forcing Vite restart...');
// Kill existing Vite process and restart // Kill existing Vite processes
const result = await global.activeSandbox.runCode(` try {
import subprocess await global.activeSandbox.runCommand({
import os cmd: 'pkill',
import signal args: ['-f', 'vite']
import time });
import threading console.log('[restart-vite] Killed existing Vite processes');
import json
import sys
# Kill existing Vite process // Wait a moment for processes to terminate
try: await new Promise(resolve => setTimeout(resolve, 2000));
with open('/tmp/vite-process.pid', 'r') as f: } catch (error) {
pid = int(f.read().strip()) console.log('[restart-vite] No existing Vite processes found');
os.kill(pid, signal.SIGTERM)
print("Killed existing Vite process")
time.sleep(1)
except:
print("No existing Vite process found")
os.chdir('/home/user/app')
# Clear error file
error_file = '/tmp/vite-errors.json'
with open(error_file, 'w') as f:
json.dump({"errors": [], "lastChecked": time.time()}, f)
# Function to monitor Vite output for errors
def monitor_output(proc, error_file):
while True:
line = proc.stderr.readline()
if not line:
break
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 // Clear any error tracking files
if not any(e['package'] == error_obj['package'] for e in data['errors']): try {
data['errors'].append(error_obj) await global.activeSandbox.runCommand({
cmd: 'bash',
args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json']
});
} catch (error) {
// Ignore if this fails
}
with open(error_file, 'w') as f: // Start Vite dev server in detached mode
json.dump(data, f) const viteProcess = await global.activeSandbox.runCommand({
cmd: 'npm',
args: ['run', 'dev'],
detached: true
});
print(f"WARNING: Detected missing package: {error_obj['package']}") console.log('[restart-vite] Vite dev server restarted');
except Exception as e:
print(f"Error parsing Vite error: {e}")
# Start Vite with error monitoring // Wait for Vite to start up
process = subprocess.Popen( await new Promise(resolve => setTimeout(resolve, 3000));
['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) {
+19 -18
View File
@@ -1,5 +1,4 @@
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 {
@@ -26,30 +25,32 @@ export async function POST(request: NextRequest) {
console.log(`[run-command] Executing: ${command}`); console.log(`[run-command] Executing: ${command}`);
const result = await global.activeSandbox.runCode(` // Parse command and arguments
import subprocess const commandParts = command.trim().split(/\s+/);
import os const cmd = commandParts[0];
const args = commandParts.slice(1);
os.chdir('/home/user/app') // Execute command using Vercel Sandbox
result = subprocess.run(${JSON.stringify(command.split(' '))}, const result = await global.activeSandbox.runCommand({
capture_output=True, cmd,
text=True, args
shell=False) });
print("STDOUT:") // Get output streams
print(result.stdout) const stdout = await result.stdout();
if result.stderr: const stderr = await result.stderr();
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n'); 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,
message: 'Command executed successfully' exitCode: result.exitCode,
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
}); });
} catch (error) { } catch (error) {
+58 -43
View File
@@ -15,54 +15,69 @@ export async function GET(request: NextRequest) {
console.log('[sandbox-logs] Fetching Vite dev server logs...'); console.log('[sandbox-logs] Fetching Vite dev server logs...');
// Get the last N lines of the Vite dev server output // Check if Vite processes are running
const result = await global.activeSandbox.runCode(` const psResult = await global.activeSandbox.runCommand({
import subprocess cmd: 'ps',
import os args: ['aux']
# Try to get the Vite process output
try:
# Read the last 100 lines of any log files
log_content = []
# Check if there are any node processes running
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 vite_processes:
log_content.append("Vite is running")
else:
log_content.append("Vite process not found")
# 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
print(json.dumps({
"hasErrors": False,
"logs": log_content,
"status": "running" if vite_processes else "stopped"
}))
except Exception as e:
print(json.dumps({
"hasErrors": True,
"logs": [str(e)],
"status": "error"
}))
`);
try {
const logData = JSON.parse(result.output || '{}');
return NextResponse.json({
success: true,
...logData
}); });
} catch {
let viteRunning = false;
let logContent: string[] = [];
if (psResult.exitCode === 0) {
const psOutput = await psResult.stdout();
const viteProcesses = psOutput.split('\n').filter(line =>
line.toLowerCase().includes('vite') ||
line.toLowerCase().includes('npm run dev')
);
viteRunning = viteProcesses.length > 0;
if (viteRunning) {
logContent.push("Vite is running");
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes
} else {
logContent.push("Vite process not found");
}
}
// Try to read any recent log files
try {
const findResult = await global.activeSandbox.runCommand({
cmd: 'find',
args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f']
});
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({ return NextResponse.json({
success: true, success: true,
hasErrors: false, hasErrors: false,
logs: [result.output], logs: logContent,
status: 'unknown' status: viteRunning ? 'running' : 'stopped'
}); });
}
} catch (error) { } catch (error) {
console.error('[sandbox-logs] Error:', error); console.error('[sandbox-logs] Error:', error);
+21 -16
View File
@@ -1,32 +1,24 @@
import { useState, useEffect } from 'react'; import { useState } 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);
useEffect(() => { // Use the real sandbox URL passed from the API
if (sandboxId && type !== 'console') { const previewUrl = sandboxUrl || '';
// 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);
@@ -50,9 +42,13 @@ 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
@@ -69,6 +65,7 @@ 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"
@@ -78,22 +75,29 @@ export default function SandboxPreview({
> >
<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 && ( {(isLoading || !previewUrl) && (
<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">
{type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'} {!previewUrl
? '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}
@@ -101,6 +105,7 @@ export default function SandboxPreview({
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) */}
+14 -11
View File
@@ -2,27 +2,30 @@
// This file contains all configurable settings for the application // This file contains all configurable settings for the application
export const appConfig = { export const appConfig = {
// E2B Sandbox Configuration // Vercel Sandbox Configuration
e2b: { vercelSandbox: {
// Sandbox timeout in minutes // Sandbox timeout in minutes
timeoutMinutes: 15, timeoutMinutes: 15,
// Convert to milliseconds for E2B API // Convert to milliseconds for Vercel Sandbox API
get timeoutMs() { get timeoutMs() {
return this.timeoutMinutes * 60 * 1000; return this.timeoutMinutes * 60 * 1000;
}, },
// Vite development server port // Development server port (Vercel Sandbox typically uses 3000 for Next.js/React)
vitePort: 5173, devPort: 3000,
// Time to wait for Vite to be ready (in milliseconds) // Time to wait for dev server to be ready (in milliseconds)
viteStartupDelay: 7000, devServerStartupDelay: 7000,
// Time to wait for CSS rebuild (in milliseconds) // Time to wait for CSS rebuild (in milliseconds)
cssRebuildDelay: 2000, cssRebuildDelay: 2000,
// Default sandbox template (if using templates) // Working directory in sandbox
defaultTemplate: undefined, // or specify a template ID workingDirectory: '/app',
// Default runtime for sandbox
runtime: 'node22' // Available: node22, python3.13, v0-next-shadcn, cua-ubuntu-xfce
}, },
// AI Model Configuration // AI Model Configuration
@@ -35,7 +38,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.5-pro' 'google/gemini-2.0-flash-exp'
], ],
// Model display names // Model display names
@@ -43,7 +46,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.5-pro': 'Gemini 2.5 Pro' 'google/gemini-2.0-flash-exp': 'Gemini 2.0 Flash (Experimental)'
}, },
// Temperature settings for non-reasoning models // Temperature settings for non-reasoning models
+21 -27
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 E2B sandbox environment. This document explains how to use the XML-based package detection and installation mechanism in the Vercel Sandbox environment.
## Overview ## Overview
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. 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.
## XML Tag Formats ## XML Tag Formats
@@ -196,43 +196,37 @@ 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 E2B Sandbox ## Integration with Vercel Sandbox
The package detection mechanism integrates seamlessly with the E2B sandbox: The package detection mechanism integrates seamlessly with the Vercel Sandbox:
1. Packages are installed in `/home/user/app/node_modules` 1. Packages are installed in the sandbox's working directory
2. The Vite dev server is automatically restarted after package installation 2. The development 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
## E2B Command Execution Methods ## Vercel Sandbox Command Execution Methods
### Method 1: Using runCode() with Python subprocess ### Using runCommand() (Recommended)
```javascript ```javascript
// Current implementation pattern // Direct command execution using Vercel Sandbox API
await global.activeSandbox.runCode(` const result = await global.activeSandbox.runCommand({
import subprocess cmd: 'npm',
import os args: ['install', 'axios']
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
}); });
console.log(result.stdout); const stdout = await result.stdout();
const stderr = await result.stderr();
console.log(stdout);
``` ```
### Command Execution Options ### Command Execution Options
When using `sandbox.commands.run()`, you can specify: When using `sandbox.runCommand()`, 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
@@ -0,0 +1,21 @@
# 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)
+159 -612
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -8,7 +8,6 @@
"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"
@@ -19,7 +18,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",
"@e2b/code-interpreter": "^1.5.1", "@vercel/sandbox": "^0.0.17",
"@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",
@@ -29,7 +28,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",