import { Sandbox } from '@vercel/sandbox'; import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; export class VercelProvider extends SandboxProvider { private existingFiles: Set = new Set(); async createSandbox(): Promise { try { console.log('[VercelProvider] Creating sandbox...'); // Kill existing sandbox if any if (this.sandbox) { console.log('[VercelProvider] Stopping existing sandbox...'); try { await this.sandbox.stop(); } catch (e) { console.error('Failed to stop existing sandbox:', e); } this.sandbox = null; } // Clear existing files tracking this.existingFiles.clear(); // Create Vercel sandbox console.log('[VercelProvider] Creating Vercel sandbox...'); const sandboxConfig: any = { timeout: 300000, // 5 minutes in ms runtime: 'node22', // Use node22 runtime for Vercel sandboxes ports: [5173] // Vite port }; // Add authentication based on environment variables if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) { console.log('[VercelProvider] Using personal access token authentication'); sandboxConfig.teamId = process.env.VERCEL_TEAM_ID; sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID; sandboxConfig.token = process.env.VERCEL_TOKEN; } else if (process.env.VERCEL_OIDC_TOKEN) { console.log('[VercelProvider] Using OIDC token authentication'); } else { console.log('[VercelProvider] No authentication found - relying on default Vercel authentication'); } this.sandbox = await Sandbox.create(sandboxConfig); const sandboxId = this.sandbox.sandboxId; console.log(`[VercelProvider] Sandbox created: ${sandboxId}`); // Get the sandbox URL using the correct Vercel Sandbox API const sandboxUrl = this.sandbox.domain(5173); console.log(`[VercelProvider] Sandbox URL: ${sandboxUrl}`); this.sandboxInfo = { sandboxId, url: sandboxUrl, provider: 'vercel', createdAt: new Date() }; return this.sandboxInfo; } catch (error) { console.error('[VercelProvider] Error creating sandbox:', error); throw error; } } async runCommand(command: string): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } console.log(`[VercelProvider] Executing: ${command}`); try { // Parse command into cmd and args (matching PR syntax) const parts = command.split(' '); const cmd = parts[0]; const args = parts.slice(1); // Vercel uses runCommand with cmd and args object (based on PR) const result = await this.sandbox.runCommand({ cmd: cmd, args: args, cwd: '/app', env: {} }); return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode || 0, success: result.exitCode === 0 }; } catch (error: any) { return { stdout: '', stderr: error.message || 'Command failed', exitCode: 1, success: false }; } } async writeFile(path: string, content: string): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } const fullPath = path.startsWith('/') ? path : `/app/${path}`; // Based on PR, Vercel SDK has a writeFiles method that takes an array try { await this.sandbox.writeFiles([{ path: fullPath, content: Buffer.from(content) }]); console.log(`[VercelProvider] Written: ${fullPath}`); this.existingFiles.add(path); } catch (error) { // Fallback to command-based approach if writeFiles is not available console.log(`[VercelProvider] writeFiles failed, using command fallback`); // Ensure directory exists const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); await this.sandbox.runCommand({ cmd: 'mkdir', args: ['-p', dir], cwd: '/' }); // Write file using echo and redirection const escapedContent = content .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\$/g, '\\$') .replace(/`/g, '\\`') .replace(/\n/g, '\\n'); await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', `echo "${escapedContent}" > ${fullPath}`], cwd: '/' }); console.log(`[VercelProvider] Written via command: ${fullPath}`); this.existingFiles.add(path); } } async readFile(path: string): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } const fullPath = path.startsWith('/') ? path : `/app/${path}`; const result = await this.sandbox.runCommand({ cmd: 'cat', args: [fullPath], cwd: '/' }); if (result.exitCode !== 0) { throw new Error(`Failed to read file: ${result.stderr}`); } return result.stdout || ''; } async listFiles(directory: string = '/app'): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } const result = await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`], cwd: '/' }); if (result.exitCode !== 0) { return []; } return (result.stdout || '').split('\n').filter((line: string) => line.trim() !== ''); } async installPackages(packages: string[]): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } const flags = process.env.NPM_FLAGS || ''; console.log(`[VercelProvider] Installing packages: ${packages.join(' ')}`); // Build args array const args = ['install']; if (flags) { args.push(...flags.split(' ')); } args.push(...packages); const result = await this.sandbox.runCommand({ cmd: 'npm', args: args, cwd: '/app' }); // Restart Vite if configured and successful if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') { await this.restartViteServer(); } return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode || 0, success: result.exitCode === 0 }; } async setupViteApp(): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } console.log('[VercelProvider] Setting up Vite React app...'); // Create directory structure await this.sandbox.runCommand({ cmd: 'mkdir', args: ['-p', '/app/src'], cwd: '/' }); // Create package.json const packageJson = { name: "sandbox-app", version: "1.0.0", type: "module", scripts: { dev: "vite --host", build: "vite build", preview: "vite preview" }, dependencies: { react: "^18.2.0", "react-dom": "^18.2.0" }, devDependencies: { "@vitejs/plugin-react": "^4.0.0", vite: "^4.3.9", tailwindcss: "^3.3.0", postcss: "^8.4.31", autoprefixer: "^10.4.16" } }; await this.writeFile('package.json', JSON.stringify(packageJson, null, 2)); // Create vite.config.js const viteConfig = `import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { host: '0.0.0.0', port: 5173, strictPort: true, hmr: { clientPort: 443, protocol: 'wss' } } })`; await this.writeFile('vite.config.js', viteConfig); // Create tailwind.config.js const tailwindConfig = `/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }`; await this.writeFile('tailwind.config.js', tailwindConfig); // Create postcss.config.js const postcssConfig = `export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }`; await this.writeFile('postcss.config.js', postcssConfig); // Create index.html const indexHtml = ` Sandbox App
`; await this.writeFile('index.html', indexHtml); // Create src/main.jsx const mainJsx = `import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( , )`; await this.writeFile('src/main.jsx', mainJsx); // Create src/App.jsx const appJsx = `function App() { return (

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

) } export default App`; await this.writeFile('src/App.jsx', appJsx); // Create src/index.css const indexCss = `@tailwind base; @tailwind components; @tailwind utilities; body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background-color: rgb(17 24 39); }`; await this.writeFile('src/index.css', indexCss); console.log('[VercelProvider] All files created successfully!'); // Install dependencies console.log('[VercelProvider] Installing dependencies...'); const installResult = await this.sandbox.runCommand({ cmd: 'npm', args: ['install'], cwd: '/app' }); if (installResult.exitCode === 0) { console.log('[VercelProvider] Dependencies installed successfully'); } else { console.warn('[VercelProvider] npm install had issues:', installResult.stderr); } // Start Vite dev server console.log('[VercelProvider] Starting Vite dev server...'); // Kill any existing Vite processes await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'pkill -f vite || true'], cwd: '/' }); // Start Vite in background await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], cwd: '/app' }); console.log('[VercelProvider] Vite dev server started'); // Wait for Vite to be ready await new Promise(resolve => setTimeout(resolve, 7000)); // Track initial files this.existingFiles.add('src/App.jsx'); this.existingFiles.add('src/main.jsx'); this.existingFiles.add('src/index.css'); this.existingFiles.add('index.html'); this.existingFiles.add('package.json'); this.existingFiles.add('vite.config.js'); this.existingFiles.add('tailwind.config.js'); this.existingFiles.add('postcss.config.js'); } async restartViteServer(): Promise { if (!this.sandbox) { throw new Error('No active sandbox'); } console.log('[VercelProvider] Restarting Vite server...'); // Kill existing Vite process await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'pkill -f vite || true'], cwd: '/' }); // Wait a moment await new Promise(resolve => setTimeout(resolve, 2000)); // Start Vite in background await this.sandbox.runCommand({ cmd: 'sh', args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], cwd: '/app' }); console.log('[VercelProvider] Vite restarted'); // Wait for Vite to be ready await new Promise(resolve => setTimeout(resolve, 7000)); } getSandboxUrl(): string | null { return this.sandboxInfo?.url || null; } async terminate(): Promise { if (this.sandbox) { console.log('[VercelProvider] Terminating sandbox...'); try { await this.sandbox.stop(); } catch (e) { console.error('Failed to terminate sandbox:', e); } this.sandbox = null; this.sandboxInfo = null; } } isAlive(): boolean { return !!this.sandbox; } }