This commit is contained in:
Developers Digest
2025-11-19 10:15:21 -05:00
320 changed files with 38446 additions and 7311 deletions
+179
View File
@@ -0,0 +1,179 @@
export interface BuildValidation {
success: boolean;
errors: string[];
isRendering: boolean;
warnings?: string[];
}
/**
* Validates that the sandbox build was successful
* Checks compilation status and verifies app is rendering
*/
export async function validateBuild(sandboxUrl: string, sandboxId: string): Promise<BuildValidation> {
try {
// Step 1: Wait for Vite to process files (give it time to compile)
await new Promise(resolve => setTimeout(resolve, 3000));
// Step 2: Check if the sandbox is actually serving content
const response = await fetch(sandboxUrl, {
headers: {
'User-Agent': 'OpenLovable-Validator',
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
return {
success: false,
errors: [`Sandbox returned ${response.status}`],
isRendering: false
};
}
const html = await response.text();
// Step 3: Check if it's the default page or actual app
const isDefaultPage =
html.includes('Vercel Sandbox Ready') ||
html.includes('Start building your React app with Vite') ||
html.includes('Vite + React') ||
!html.includes('id="root"');
if (isDefaultPage) {
return {
success: false,
errors: ['Sandbox showing default page, app not rendered'],
isRendering: false
};
}
// Step 4: Check for Vite error overlay in HTML
const hasViteError = html.includes('vite-error-overlay');
if (hasViteError) {
// Try to extract error message
const errorMatch = html.match(/Failed to resolve import "([^"]+)"/);
const error = errorMatch
? `Missing package: ${errorMatch[1]}`
: 'Vite compilation error detected';
return {
success: false,
errors: [error],
isRendering: false
};
}
// Success! App is rendering
return {
success: true,
errors: [],
isRendering: true
};
} catch (error) {
console.error('[validateBuild] Error during validation:', error);
return {
success: false,
errors: [error instanceof Error ? error.message : 'Validation failed'],
isRendering: false
};
}
}
/**
* Extracts missing package names from error messages
*/
export function extractMissingPackages(error: any): string[] {
const message = error?.message || String(error);
const packages: string[] = [];
// Pattern 1: "Failed to resolve import 'package-name'"
const importMatches = message.matchAll(/Failed to resolve import ["']([^"']+)["']/g);
for (const match of importMatches) {
packages.push(match[1]);
}
// Pattern 2: "Cannot find module 'package-name'"
const moduleMatches = message.matchAll(/Cannot find module ["']([^"']+)["']/g);
for (const match of moduleMatches) {
packages.push(match[1]);
}
// Pattern 3: "Package 'package-name' not found"
const packageMatches = message.matchAll(/Package ["']([^"']+)["'] not found/g);
for (const match of packageMatches) {
packages.push(match[1]);
}
return [...new Set(packages)]; // Remove duplicates
}
/**
* Classifies error type for targeted recovery
*/
export type ErrorType = 'missing-package' | 'syntax-error' | 'sandbox-timeout' | 'not-rendered' | 'vite-error' | 'unknown';
export function classifyError(error: any): ErrorType {
const message = (error?.message || String(error)).toLowerCase();
if (message.includes('failed to resolve import') ||
message.includes('cannot find module') ||
message.includes('missing package')) {
return 'missing-package';
}
if (message.includes('syntax error') ||
message.includes('unexpected token') ||
message.includes('parsing error')) {
return 'syntax-error';
}
if (message.includes('timeout') ||
message.includes('not responding') ||
message.includes('timed out')) {
return 'sandbox-timeout';
}
if (message.includes('not rendered') ||
message.includes('sandbox ready') ||
message.includes('default page')) {
return 'not-rendered';
}
if (message.includes('vite') ||
message.includes('compilation')) {
return 'vite-error';
}
return 'unknown';
}
/**
* Calculates retry delay based on attempt number and error type
*/
export function calculateRetryDelay(attempt: number, errorType: ErrorType): number {
const baseDelay = 2000; // 2 seconds
// Different strategies for different errors
switch (errorType) {
case 'missing-package':
// Packages need time to install
return baseDelay * 2 * attempt; // 4s, 8s, 12s
case 'not-rendered':
// Vite needs time to compile
return baseDelay * 3 * attempt; // 6s, 12s, 18s
case 'vite-error':
// Vite restart needed
return baseDelay * 2 * attempt;
case 'sandbox-timeout':
// Sandbox might be slow
return baseDelay * 4 * attempt; // 8s, 16s, 24s
default:
// Standard exponential backoff
return baseDelay * attempt;
}
}
+1 -1
View File
@@ -92,7 +92,7 @@ export function executeSearchPlan(
matchedPattern = pattern;
break;
}
} catch (e) {
} catch {
console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
}
}
+219
View File
@@ -0,0 +1,219 @@
// Using direct fetch to Morph's OpenAI-compatible API to avoid SDK type issues
export interface MorphEditBlock {
targetFile: string;
instructions: string;
update: string;
}
export interface MorphApplyResult {
success: boolean;
normalizedPath?: string;
mergedCode?: string;
error?: string;
}
// Normalize project-relative paths to sandbox layout
export function normalizeProjectPath(inputPath: string): { normalizedPath: string; fullPath: string } {
let normalizedPath = inputPath.trim();
if (normalizedPath.startsWith('/')) normalizedPath = normalizedPath.slice(1);
const configFiles = new Set([
'tailwind.config.js',
'vite.config.js',
'package.json',
'package-lock.json',
'tsconfig.json',
'postcss.config.js'
]);
const fileName = normalizedPath.split('/').pop() || '';
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.has(fileName)) {
normalizedPath = 'src/' + normalizedPath;
}
const fullPath = `/home/user/app/${normalizedPath}`;
return { normalizedPath, fullPath };
}
async function morphChatCompletionsCreate(payload: any) {
if (!process.env.MORPH_API_KEY) throw new Error('MORPH_API_KEY is not set');
const res = await fetch('https://api.morphllm.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MORPH_API_KEY}`
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Morph API error ${res.status}: ${text}`);
}
return res.json();
}
// Parse <edit> blocks from LLM output
export function parseMorphEdits(text: string): MorphEditBlock[] {
const edits: MorphEditBlock[] = [];
const editRegex = /<edit\s+target_file="([^"]+)">([\s\S]*?)<\/edit>/g;
let match: RegExpExecArray | null;
while ((match = editRegex.exec(text)) !== null) {
const targetFile = match[1].trim();
const inner = match[2];
const instrMatch = inner.match(/<instructions>([\s\S]*?)<\/instructions>/);
const updateMatch = inner.match(/<update>([\s\S]*?)<\/update>/);
const instructions = instrMatch ? instrMatch[1].trim() : '';
const update = updateMatch ? updateMatch[1].trim() : '';
if (targetFile && update) {
edits.push({ targetFile, instructions, update });
}
}
return edits;
}
// Read a file from sandbox: prefers cache, then sandbox.files, then commands.run("cat ...")
async function readFileFromSandbox(sandbox: any, normalizedPath: string, fullPath: string): Promise<string> {
// Try backend cache first
if ((global as any).sandboxState?.fileCache?.files?.[normalizedPath]?.content) {
return (global as any).sandboxState.fileCache.files[normalizedPath].content as string;
}
// Try E2B files API
if (sandbox?.files?.read) {
return await sandbox.files.read(fullPath);
}
// Try provider runCommand (preferred for provider pattern)
if (typeof sandbox?.runCommand === 'function') {
try {
const res = await sandbox.runCommand(`cat ${normalizedPath}`);
if (res && typeof res.stdout === 'string') {
return res.stdout as string;
}
} catch {}
// fallback to absolute path
try {
const resAbs = await sandbox.runCommand(`cat ${fullPath}`);
if (resAbs && typeof resAbs.stdout === 'string') {
return resAbs.stdout as string;
}
} catch {}
}
// Try shell cat via commands.run
if (sandbox?.commands?.run) {
const result = await sandbox.commands.run(`cat ${fullPath}`, { cwd: '/home/user/app', timeout: 30 });
if (result?.exitCode === 0 && typeof result?.stdout === 'string') {
return result.stdout as string;
}
}
throw new Error(`Unable to read file: ${normalizedPath}`);
}
// Write a file to sandbox and update cache
async function writeFileToSandbox(sandbox: any, normalizedPath: string, fullPath: string, content: string): Promise<void> {
// Provider pattern (writeFile)
if (typeof sandbox?.writeFile === 'function') {
await sandbox.writeFile(normalizedPath, content);
return;
}
// Provider pattern (runCommand redirect)
if (typeof sandbox?.runCommand === 'function') {
// Ensure directory exists
const dir = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
if (dir) {
try { await sandbox.runCommand(`mkdir -p ${dir}`); } catch {}
}
// Write via heredoc with proper escaping
const heredoc = `bash -lc 'cat > ${normalizedPath} <<\"EOF\"\n${content.replace(/\\/g, '\\\\').replace(/\n/g, '\n').replace(/\$/g, '\$')}\nEOF'`;
const result = await sandbox.runCommand(heredoc);
if (result?.stdout || result?.stderr) {
// no-op
}
return;
}
// Prefer E2B files API
if (sandbox?.files?.write) {
await sandbox.files.write(fullPath, content);
} else if (sandbox?.runCode) {
// Use Python to write safely
const escaped = content
.replace(/\\/g, '\\\\')
.replace(/"""/g, '\"\"\"');
await sandbox.runCode(`
import os
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
with open("${fullPath}", 'w') as f:
f.write("""${escaped}""")
print("WROTE:${fullPath}")
`);
} else if (sandbox?.commands?.run) {
// Shell redirection (fallback)
// Note: beware of special chars; this is a last-resort path
const result = await sandbox.commands.run(`bash -lc 'mkdir -p "$(dirname "${fullPath}")" && cat > "${fullPath}" << \EOF\n${content}\nEOF'`, { cwd: '/home/user/app', timeout: 60 });
if (result?.exitCode !== 0) {
throw new Error(`Failed to write file via shell: ${normalizedPath}`);
}
} else {
throw new Error('No available method to write files to sandbox');
}
// Update backend cache if available
if ((global as any).sandboxState?.fileCache) {
(global as any).sandboxState.fileCache.files[normalizedPath] = {
content,
lastModified: Date.now()
};
}
if ((global as any).existingFiles) {
(global as any).existingFiles.add(normalizedPath);
}
}
export async function applyMorphEditToFile(params: {
sandbox: any;
targetPath: string;
instructions: string;
updateSnippet: string;
}): Promise<MorphApplyResult> {
try {
if (!process.env.MORPH_API_KEY) {
return { success: false, error: 'MORPH_API_KEY not set' };
}
const { normalizedPath, fullPath } = normalizeProjectPath(params.targetPath);
// Read original code (existence validation happens here)
const initialCode = await readFileFromSandbox(params.sandbox, normalizedPath, fullPath);
const resp = await morphChatCompletionsCreate({
model: 'morph-v3-large',
messages: [
{
role: 'user',
content: `<instruction>${params.instructions || ''}</instruction>\n<code>${initialCode}</code>\n<update>${params.updateSnippet}</update>`
}
]
});
const mergedCode = (resp as any)?.choices?.[0]?.message?.content || '';
if (!mergedCode) {
return { success: false, error: 'Morph returned empty content', normalizedPath };
}
await writeFileToSandbox(params.sandbox, normalizedPath, fullPath, mergedCode);
return { success: true, normalizedPath, mergedCode };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
+41
View File
@@ -0,0 +1,41 @@
import { SandboxProvider, SandboxProviderConfig } from './types';
import { E2BProvider } from './providers/e2b-provider';
import { VercelProvider } from './providers/vercel-provider';
export class SandboxFactory {
static create(provider?: string, config?: SandboxProviderConfig): SandboxProvider {
// Use environment variable if provider not specified
const selectedProvider = provider || process.env.SANDBOX_PROVIDER || 'e2b';
switch (selectedProvider.toLowerCase()) {
case 'e2b':
return new E2BProvider(config || {});
case 'vercel':
return new VercelProvider(config || {});
default:
throw new Error(`Unknown sandbox provider: ${selectedProvider}. Supported providers: e2b, vercel`);
}
}
static getAvailableProviders(): string[] {
return ['e2b', 'vercel'];
}
static isProviderAvailable(provider: string): boolean {
switch (provider.toLowerCase()) {
case 'e2b':
return !!process.env.E2B_API_KEY;
case 'vercel':
// Vercel can use OIDC (automatic) or PAT
return !!process.env.VERCEL_OIDC_TOKEN ||
(!!process.env.VERCEL_TOKEN && !!process.env.VERCEL_TEAM_ID && !!process.env.VERCEL_PROJECT_ID);
default:
return false;
}
}
}
+512
View File
@@ -0,0 +1,512 @@
import { Sandbox } from '@e2b/code-interpreter';
import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
// SandboxProviderConfig available through parent class
import { appConfig } from '@/config/app.config';
export class E2BProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set();
/**
* Attempt to reconnect to an existing E2B sandbox
*/
async reconnect(sandboxId: string): Promise<boolean> {
try {
// Try to connect to existing sandbox
// Note: E2B SDK doesn't directly support reconnection, but we can try to recreate
// For now, return false to indicate reconnection isn't supported
// In the future, E2B may add this capability
return false;
} catch (error) {
console.error(`[E2BProvider] Failed to reconnect to sandbox ${sandboxId}:`, error);
return false;
}
}
async createSandbox(): Promise<SandboxInfo> {
try {
// Kill existing sandbox if any
if (this.sandbox) {
try {
await this.sandbox.kill();
} catch (e) {
console.error('Failed to close existing sandbox:', e);
}
this.sandbox = null;
}
// Clear existing files tracking
this.existingFiles.clear();
// Create base sandbox
this.sandbox = await Sandbox.create({
apiKey: this.config.e2b?.apiKey || process.env.E2B_API_KEY,
timeoutMs: this.config.e2b?.timeoutMs || appConfig.e2b.timeoutMs
});
const sandboxId = (this.sandbox as any).sandboxId || Date.now().toString();
const host = (this.sandbox as any).getHost(appConfig.e2b.vitePort);
this.sandboxInfo = {
sandboxId,
url: `https://${host}`,
provider: 'e2b',
createdAt: new Date()
};
// Set extended timeout on the sandbox instance if method available
if (typeof this.sandbox.setTimeout === 'function') {
this.sandbox.setTimeout(appConfig.e2b.timeoutMs);
}
return this.sandboxInfo;
} catch (error) {
console.error('[E2BProvider] Error creating sandbox:', error);
throw error;
}
}
async runCommand(command: string): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
result = subprocess.run(${JSON.stringify(command.split(' '))},
capture_output=True,
text=True,
shell=False)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n');
const stderr = result.logs.stderr.join('\n');
return {
stdout: output,
stderr,
exitCode: result.error ? 1 : 0,
success: !result.error
};
}
async writeFile(path: string, content: string): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
// Use the E2B filesystem API to write the file
// Note: E2B SDK uses files.write() method
if ((this.sandbox as any).files && typeof (this.sandbox as any).files.write === 'function') {
// Use the files.write API if available
await (this.sandbox as any).files.write(fullPath, Buffer.from(content));
} else {
// Fallback to Python code execution
await this.sandbox.runCode(`
import os
# Ensure directory exists
dir_path = os.path.dirname("${fullPath}")
os.makedirs(dir_path, exist_ok=True)
# Write file
with open("${fullPath}", 'w') as f:
f.write(${JSON.stringify(content)})
print(f"✓ Written: ${fullPath}")
`);
}
this.existingFiles.add(path);
}
async readFile(path: string): Promise<string> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const fullPath = path.startsWith('/') ? path : `/home/user/app/${path}`;
const result = await this.sandbox.runCode(`
with open("${fullPath}", 'r') as f:
content = f.read()
print(content)
`);
return result.logs.stdout.join('\n');
}
async listFiles(directory: string = '/home/user/app'): Promise<string[]> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCode(`
import os
import json
def list_files(path):
files = []
for root, dirs, filenames in os.walk(path):
# Skip node_modules and .git
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist', 'build']]
for filename in filenames:
rel_path = os.path.relpath(os.path.join(root, filename), path)
files.append(rel_path)
return files
files = list_files("${directory}")
print(json.dumps(files))
`);
try {
return JSON.parse(result.logs.stdout.join(''));
} catch {
return [];
}
}
async installPackages(packages: string[]): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const packageList = packages.join(' ');
const flags = appConfig.packages.useLegacyPeerDeps ? '--legacy-peer-deps' : '';
const result = await this.sandbox.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
# Install packages
result = subprocess.run(
['npm', 'install', ${flags ? `'${flags}',` : ''} ${packages.map(p => `'${p}'`).join(', ')}],
capture_output=True,
text=True
)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("\\nSTDERR:")
print(result.stderr)
print(f"\\nReturn code: {result.returncode}")
`);
const output = result.logs.stdout.join('\n');
const stderr = result.logs.stderr.join('\n');
// Restart Vite if configured
if (appConfig.packages.autoRestartVite && !result.error) {
await this.restartViteServer();
}
return {
stdout: output,
stderr,
exitCode: result.error ? 1 : 0,
success: !result.error
};
}
async setupViteApp(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Write all files in a single Python script
const setupScript = `
import os
import json
print('Setting up React app with Vite and Tailwind...')
# Create directory structure
os.makedirs('/home/user/app/src', exist_ok=True)
# Package.json
package_json = {
"name": "sandbox-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.3.9",
"tailwindcss": "^3.3.0",
"postcss": "^8.4.31",
"autoprefixer": "^10.4.16"
}
}
with open('/home/user/app/package.json', 'w') as f:
json.dump(package_json, f, indent=2)
print('✓ package.json')
# Vite config
vite_config = """import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: false,
allowedHosts: ['.e2b.app', '.e2b.dev', '.vercel.run', 'localhost', '127.0.0.1']
}
})"""
with open('/home/user/app/vite.config.js', 'w') as f:
f.write(vite_config)
print('✓ vite.config.js')
# Tailwind config
tailwind_config = """/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}"""
with open('/home/user/app/tailwind.config.js', 'w') as f:
f.write(tailwind_config)
print('✓ tailwind.config.js')
# PostCSS config
postcss_config = """export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}"""
with open('/home/user/app/postcss.config.js', 'w') as f:
f.write(postcss_config)
print('✓ postcss.config.js')
# Index.html
index_html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>"""
with open('/home/user/app/index.html', 'w') as f:
f.write(index_html)
print('✓ index.html')
# Main.jsx
main_jsx = """import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)"""
with open('/home/user/app/src/main.jsx', 'w') as f:
f.write(main_jsx)
print('✓ src/main.jsx')
# App.jsx
app_jsx = """function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App"""
with open('/home/user/app/src/App.jsx', 'w') as f:
f.write(app_jsx)
print('✓ src/App.jsx')
# Index.css
index_css = """@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}"""
with open('/home/user/app/src/index.css', 'w') as f:
f.write(index_css)
print('✓ src/index.css')
print('\\nAll files created successfully!')
`;
await this.sandbox.runCode(setupScript);
// Install dependencies
await this.sandbox.runCode(`
import subprocess
print('Installing npm packages...')
result = subprocess.run(
['npm', 'install'],
cwd='/home/user/app',
capture_output=True,
text=True
)
if result.returncode == 0:
print('✓ Dependencies installed successfully')
else:
print(f'⚠ Warning: npm install had issues: {result.stderr}')
`);
// Start Vite dev server
await this.sandbox.runCode(`
import subprocess
import os
import time
os.chdir('/home/user/app')
# Kill any existing Vite processes
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(1)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite dev server started with PID: {process.pid}')
print('Waiting for server to be ready...')
`);
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
// Track initial files
this.existingFiles.add('src/App.jsx');
this.existingFiles.add('src/main.jsx');
this.existingFiles.add('src/index.css');
this.existingFiles.add('index.html');
this.existingFiles.add('package.json');
this.existingFiles.add('vite.config.js');
this.existingFiles.add('tailwind.config.js');
this.existingFiles.add('postcss.config.js');
}
async restartViteServer(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
await this.sandbox.runCode(`
import subprocess
import time
import os
os.chdir('/home/user/app')
# Kill existing Vite process
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
time.sleep(2)
# Start Vite dev server
env = os.environ.copy()
env['FORCE_COLOR'] = '0'
process = subprocess.Popen(
['npm', 'run', 'dev'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env
)
print(f'✓ Vite restarted with PID: {process.pid}')
`);
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
}
getSandboxUrl(): string | null {
return this.sandboxInfo?.url || null;
}
getSandboxInfo(): SandboxInfo | null {
return this.sandboxInfo;
}
async terminate(): Promise<void> {
if (this.sandbox) {
try {
await this.sandbox.kill();
} catch (e) {
console.error('Failed to terminate sandbox:', e);
}
this.sandbox = null;
this.sandboxInfo = null;
}
}
isAlive(): boolean {
return !!this.sandbox;
}
}
+600
View File
@@ -0,0 +1,600 @@
import { Sandbox } from '@vercel/sandbox';
import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
// SandboxProviderConfig available through parent class
export class VercelProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set();
async createSandbox(): Promise<SandboxInfo> {
try {
// Kill existing sandbox if any
if (this.sandbox) {
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to stop existing sandbox:', e);
}
this.sandbox = null;
}
// Clear existing files tracking
this.existingFiles.clear();
// Create Vercel sandbox
const sandboxConfig: any = {
timeout: 300000, // 5 minutes in ms
runtime: 'node22', // Use node22 runtime for Vercel sandboxes
ports: [5173] // Vite port
};
// Add authentication based on environment variables
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
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) {
sandboxConfig.oidcToken = process.env.VERCEL_OIDC_TOKEN;
}
this.sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = this.sandbox.sandboxId;
// Sandbox created successfully
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = this.sandbox.domain(5173);
this.sandboxInfo = {
sandboxId,
url: sandboxUrl,
provider: 'vercel',
createdAt: new Date()
};
return this.sandboxInfo;
} catch (error) {
console.error('[VercelProvider] Error creating sandbox:', error);
throw error;
}
}
async runCommand(command: string): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
try {
// Parse command into cmd and args (matching PR syntax)
const parts = command.split(' ');
const cmd = parts[0];
const args = parts.slice(1);
// Vercel uses runCommand with cmd and args object (based on PR)
const result = await this.sandbox.runCommand({
cmd: cmd,
args: args,
cwd: '/vercel/sandbox',
env: {}
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
return {
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
} catch (error: any) {
return {
stdout: '',
stderr: error.message || 'Command failed',
exitCode: 1,
success: false
};
}
}
async writeFile(path: string, content: string): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
// Writing file to sandbox
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
try {
const buffer = Buffer.from(content, 'utf-8');
// Writing file with buffer
await this.sandbox.writeFiles([{
path: fullPath,
content: buffer
}]);
this.existingFiles.add(path);
} catch (writeError: any) {
// Log detailed error information
console.error(`[VercelProvider] writeFiles failed for ${fullPath}:`, {
error: writeError,
message: writeError?.message,
response: writeError?.response,
statusCode: writeError?.response?.status,
responseData: writeError?.response?.data
});
// Fallback to command-based approach if writeFiles fails
// Falling back to command-based file write
// Ensure directory exists
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
if (dir) {
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', dir]
});
// Directory created
}
// Write file using echo and redirection
const escapedContent = content
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`')
.replace(/\n/g, '\\n');
const writeResult = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
});
// File written
if (writeResult.exitCode === 0) {
this.existingFiles.add(path);
} else {
throw new Error(`Failed to write file via command: ${writeResult.stderr}`);
}
}
}
async readFile(path: string): Promise<string> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
const result = await this.sandbox.runCommand({
cmd: 'cat',
args: [fullPath]
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
if (result.exitCode !== 0) {
throw new Error(`Failed to read file: ${stderr}`);
}
return stdout;
}
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`],
cwd: '/'
});
// Handle stdout - it might be a function in Vercel SDK
let stdout = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
if (result.exitCode !== 0) {
return [];
}
return stdout.split('\n').filter((line: string) => line.trim() !== '');
}
async installPackages(packages: string[]): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const flags = process.env.NPM_FLAGS || '';
// Installing packages
// Build args array
const args = ['install'];
if (flags) {
args.push(...flags.split(' '));
}
args.push(...packages);
const result = await this.sandbox.runCommand({
cmd: 'npm',
args: args,
cwd: '/vercel/sandbox'
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
// Restart Vite if configured and successful
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
await this.restartViteServer();
}
return {
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
}
async setupViteApp(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Setting up Vite app for sandbox
// Create directory structure
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', '/vercel/sandbox/src']
});
// Directory structure created
// Create package.json
const packageJson = {
name: "sandbox-app",
version: "1.0.0",
type: "module",
scripts: {
dev: "vite --host",
build: "vite build",
preview: "vite preview"
},
dependencies: {
react: "^18.2.0",
"react-dom": "^18.2.0"
},
devDependencies: {
"@vitejs/plugin-react": "^4.0.0",
vite: "^4.3.9",
tailwindcss: "^3.3.0",
postcss: "^8.4.31",
autoprefixer: "^10.4.16"
}
};
await this.writeFile('package.json', JSON.stringify(packageJson, null, 2));
// Create vite.config.js
const viteConfig = `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
allowedHosts: [
'.vercel.run', // Allow all Vercel sandbox domains
'.e2b.dev', // Allow all E2B sandbox domains
'localhost'
],
hmr: {
clientPort: 443,
protocol: 'wss'
}
}
})`;
await this.writeFile('vite.config.js', viteConfig);
// Create tailwind.config.js
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}`;
await this.writeFile('tailwind.config.js', tailwindConfig);
// Create postcss.config.js
const postcssConfig = `export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}`;
await this.writeFile('postcss.config.js', postcssConfig);
// Create index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>`;
await this.writeFile('index.html', indexHtml);
// Create src/main.jsx
const mainJsx = `import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)`;
await this.writeFile('src/main.jsx', mainJsx);
// Create src/App.jsx
const appJsx = `function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Vercel Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App`;
await this.writeFile('src/App.jsx', appJsx);
// Create src/index.css
const indexCss = `@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}`;
await this.writeFile('src/index.css', indexCss);
// Installing npm dependencies
// Install dependencies
try {
const installResult = await this.sandbox.runCommand({
cmd: 'npm',
args: ['install'],
cwd: '/vercel/sandbox'
});
// npm install completed
if (installResult.exitCode === 0) {
// Dependencies installed successfully
} else {
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
}
} catch (error: any) {
console.error('[VercelProvider] npm install error:', {
message: error?.message,
response: error?.response?.status,
responseText: error?.text
});
// Try alternative approach - run as shell command
try {
const altResult = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'cd /vercel/sandbox && npm install'],
cwd: '/vercel/sandbox'
});
if (altResult.exitCode === 0) {
// Alternative npm install succeeded
} else {
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
}
} catch (altError) {
console.error('[VercelProvider] Alternative npm install also failed:', altError);
console.warn('[VercelProvider] Continuing without npm install - packages may need to be installed manually');
}
}
// Start Vite dev server
// Starting Vite dev server
// Kill any existing Vite processes
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
// Track initial files
this.existingFiles.add('src/App.jsx');
this.existingFiles.add('src/main.jsx');
this.existingFiles.add('src/index.css');
this.existingFiles.add('index.html');
this.existingFiles.add('package.json');
this.existingFiles.add('vite.config.js');
this.existingFiles.add('tailwind.config.js');
this.existingFiles.add('postcss.config.js');
}
async restartViteServer(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Restarting Vite server
// Kill existing Vite process
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
}
getSandboxUrl(): string | null {
return this.sandboxInfo?.url || null;
}
getSandboxInfo(): SandboxInfo | null {
return this.sandboxInfo;
}
async terminate(): Promise<void> {
if (this.sandbox) {
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to terminate sandbox:', e);
}
this.sandbox = null;
this.sandboxInfo = null;
}
}
isAlive(): boolean {
return !!this.sandbox;
}
}
+172
View File
@@ -0,0 +1,172 @@
import { SandboxProvider } from './types';
import { SandboxFactory } from './factory';
interface SandboxInfo {
sandboxId: string;
provider: SandboxProvider;
createdAt: Date;
lastAccessed: Date;
}
class SandboxManager {
private sandboxes: Map<string, SandboxInfo> = new Map();
private activeSandboxId: string | null = null;
/**
* Get or create a sandbox provider for the given sandbox ID
*/
async getOrCreateProvider(sandboxId: string): Promise<SandboxProvider> {
// Check if we already have this sandbox
const existing = this.sandboxes.get(sandboxId);
if (existing) {
existing.lastAccessed = new Date();
return existing.provider;
}
// Try to reconnect to existing sandbox
try {
const provider = SandboxFactory.create();
// For E2B provider, try to reconnect
if (provider.constructor.name === 'E2BProvider') {
// E2B sandboxes can be reconnected using the sandbox ID
const reconnected = await (provider as any).reconnect(sandboxId);
if (reconnected) {
this.sandboxes.set(sandboxId, {
sandboxId,
provider,
createdAt: new Date(),
lastAccessed: new Date()
});
this.activeSandboxId = sandboxId;
return provider;
}
}
// For Vercel or if reconnection failed, return the new provider
// The caller will need to handle creating a new sandbox
return provider;
} catch (error) {
console.error(`[SandboxManager] Error reconnecting to sandbox ${sandboxId}:`, error);
throw error;
}
}
/**
* Register a new sandbox
*/
registerSandbox(sandboxId: string, provider: SandboxProvider): void {
this.sandboxes.set(sandboxId, {
sandboxId,
provider,
createdAt: new Date(),
lastAccessed: new Date()
});
this.activeSandboxId = sandboxId;
}
/**
* Get the active sandbox provider
*/
getActiveProvider(): SandboxProvider | null {
if (!this.activeSandboxId) {
return null;
}
const sandbox = this.sandboxes.get(this.activeSandboxId);
if (sandbox) {
sandbox.lastAccessed = new Date();
return sandbox.provider;
}
return null;
}
/**
* Get a specific sandbox provider
*/
getProvider(sandboxId: string): SandboxProvider | null {
const sandbox = this.sandboxes.get(sandboxId);
if (sandbox) {
sandbox.lastAccessed = new Date();
return sandbox.provider;
}
return null;
}
/**
* Set the active sandbox
*/
setActiveSandbox(sandboxId: string): boolean {
if (this.sandboxes.has(sandboxId)) {
this.activeSandboxId = sandboxId;
return true;
}
return false;
}
/**
* Terminate a sandbox
*/
async terminateSandbox(sandboxId: string): Promise<void> {
const sandbox = this.sandboxes.get(sandboxId);
if (sandbox) {
try {
await sandbox.provider.terminate();
} catch (error) {
console.error(`[SandboxManager] Error terminating sandbox ${sandboxId}:`, error);
}
this.sandboxes.delete(sandboxId);
if (this.activeSandboxId === sandboxId) {
this.activeSandboxId = null;
}
}
}
/**
* Terminate all sandboxes
*/
async terminateAll(): Promise<void> {
const promises = Array.from(this.sandboxes.values()).map(sandbox =>
sandbox.provider.terminate().catch(err =>
console.error(`[SandboxManager] Error terminating sandbox ${sandbox.sandboxId}:`, err)
)
);
await Promise.all(promises);
this.sandboxes.clear();
this.activeSandboxId = null;
}
/**
* Clean up old sandboxes (older than maxAge milliseconds)
*/
async cleanup(maxAge: number = 3600000): Promise<void> {
const now = new Date();
const toDelete: string[] = [];
for (const [id, info] of this.sandboxes.entries()) {
const age = now.getTime() - info.lastAccessed.getTime();
if (age > maxAge) {
toDelete.push(id);
}
}
for (const id of toDelete) {
await this.terminateSandbox(id);
}
}
}
// Export singleton instance
export const sandboxManager = new SandboxManager();
// Also maintain backward compatibility with global state
declare global {
var sandboxManager: SandboxManager;
}
// Ensure the global reference points to our singleton
global.sandboxManager = sandboxManager;
+65
View File
@@ -0,0 +1,65 @@
export interface SandboxFile {
path: string;
content: string;
lastModified?: number;
}
export interface SandboxInfo {
sandboxId: string;
url: string;
provider: 'e2b' | 'vercel';
createdAt: Date;
}
export interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
success: boolean;
}
export interface SandboxProviderConfig {
e2b?: {
apiKey: string;
timeoutMs?: number;
template?: string;
};
vercel?: {
teamId?: string;
projectId?: string;
token?: string;
authMethod?: 'oidc' | 'pat';
};
}
export abstract class SandboxProvider {
protected config: SandboxProviderConfig;
protected sandbox: any;
protected sandboxInfo: SandboxInfo | null = null;
constructor(config: SandboxProviderConfig) {
this.config = config;
}
abstract createSandbox(): Promise<SandboxInfo>;
abstract runCommand(command: string): Promise<CommandResult>;
abstract writeFile(path: string, content: string): Promise<void>;
abstract readFile(path: string): Promise<string>;
abstract listFiles(directory?: string): Promise<string[]>;
abstract installPackages(packages: string[]): Promise<CommandResult>;
abstract getSandboxUrl(): string | null;
abstract getSandboxInfo(): SandboxInfo | null;
abstract terminate(): Promise<void>;
abstract isAlive(): boolean;
// Optional methods that providers can override
async setupViteApp(): Promise<void> {
// Default implementation for setting up a Vite React app
throw new Error('setupViteApp not implemented for this provider');
}
async restartViteServer(): Promise<void> {
// Default implementation for restarting Vite
throw new Error('restartViteServer not implemented for this provider');
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
return twMerge(clsx(inputs));
}