update vercel sandbox support

This commit is contained in:
Developers Digest
2025-09-08 15:15:18 -04:00
parent 9d71ae77e7
commit cb1b0a9f64
34 changed files with 1001 additions and 588 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { generateObject } from 'ai';
import { z } from 'zod';
import type { FileManifest } from '@/types/file-manifest';
// import type { FileManifest } from '@/types/file-manifest'; // Type is used implicitly through manifest parameter
// Check if we're using Vercel AI Gateway
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY;
@@ -76,7 +76,7 @@ export async function POST(request: NextRequest) {
// Create a summary of available files for the AI
const validFiles = Object.entries(manifest.files as Record<string, any>)
.filter(([path, info]) => {
.filter(([path, _info]) => {
// Filter out invalid paths
return path.includes('.') && !path.match(/\/\d+$/);
});
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
const fileSummary = validFiles
.map(([path, info]: [string, any]) => {
const componentName = info.componentInfo?.name || path.split('/').pop();
const hasImports = info.imports?.length > 0;
// const hasImports = info.imports?.length > 0; // Kept for future use
const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
return `- ${path} (${componentName}, renders: ${childComponents})`;
})
+52 -48
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
// Sandbox import not needed - using global sandbox from sandbox-manager
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global {
var conversationState: ConversationState | null;
@@ -294,45 +295,48 @@ export async function POST(request: NextRequest) {
global.existingFiles = new Set<string>();
}
// First, always check the global state for active provider
let provider = global.activeSandboxProvider;
// Try to get provider from sandbox manager first
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
// Fall back to global state if not found in manager
if (!provider) {
provider = global.activeSandboxProvider;
}
// If we don't have a provider in this instance but we have a sandboxId,
// try to use the existing sandbox data or create a new one
// If we have a sandboxId but no provider, try to get or create one
if (!provider && sandboxId) {
console.log(`[apply-ai-code-stream] Provider not in this instance for sandbox ${sandboxId}, checking existing data...`);
// If we have sandbox data but no provider, we'll create a new provider
// E2B doesn't support reconnection like Vercel does
if (global.sandboxData && global.sandboxData.sandboxId === sandboxId) {
console.log(`[apply-ai-code-stream] Creating new provider for existing sandbox ${sandboxId}`);
// Create a new provider instance (this will create a new sandbox since E2B doesn't support reconnection)
try {
const { SandboxFactory } = await import('@/lib/sandbox/factory');
provider = SandboxFactory.create();
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
try {
provider = await sandboxManager.getOrCreateProvider(sandboxId);
// If we got a new provider (not reconnected), we need to create a new sandbox
if (!provider.getSandboxInfo()) {
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
await provider.createSandbox();
// Update the global state
global.activeSandboxProvider = provider;
console.log(`[apply-ai-code-stream] Created new provider for sandbox ${sandboxId}`);
} catch (providerError) {
console.error(`[apply-ai-code-stream] Failed to create provider for sandbox ${sandboxId}:`, providerError);
return NextResponse.json({
success: false,
error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`,
results: {
filesCreated: [],
packagesInstalled: [],
commandsExecuted: [],
errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`]
},
explanation: parsed.explanation,
structure: parsed.structure,
parsedFiles: parsed.files,
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.`
}, { status: 500 });
await provider.setupViteApp();
sandboxManager.registerSandbox(sandboxId, provider);
}
// Update legacy global state
global.activeSandboxProvider = provider;
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
} catch (providerError) {
console.error(`[apply-ai-code-stream] Failed to get or create provider for sandbox ${sandboxId}:`, providerError);
return NextResponse.json({
success: false,
error: `Failed to create sandbox provider for ${sandboxId}. The sandbox may have expired.`,
results: {
filesCreated: [],
packagesInstalled: [],
commandsExecuted: [],
errors: [`Sandbox provider creation failed: ${(providerError as Error).message}`]
},
explanation: parsed.explanation,
structure: parsed.structure,
parsedFiles: parsed.files,
message: `Parsed ${parsed.files.length} files but couldn't apply them - sandbox reconnection failed.`
}, { status: 500 });
}
}
@@ -342,19 +346,18 @@ export async function POST(request: NextRequest) {
try {
const { SandboxFactory } = await import('@/lib/sandbox/factory');
provider = SandboxFactory.create();
await provider.createSandbox();
const sandboxInfo = await provider.createSandbox();
await provider.setupViteApp();
// Store the provider globally
// Register with sandbox manager
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
// Store in legacy global state
global.activeSandboxProvider = provider;
// Update sandbox data
const sandboxInfo = provider.getSandboxInfo();
if (sandboxInfo) {
global.sandboxData = {
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url
};
}
global.sandboxData = {
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url
};
console.log(`[apply-ai-code-stream] Created new sandbox successfully`);
} catch (createError) {
@@ -476,7 +479,8 @@ export async function POST(request: NextRequest) {
if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages;
}
} catch (e) {
} catch (parseError) {
console.debug('Error parsing terminal output:', parseError);
// Ignore parse errors
}
}
+1
View File
@@ -488,6 +488,7 @@ body {
console.log('Auto-generated: src/index.css');
results.filesCreated.push('src/index.css (with Tailwind)');
} catch (error) {
console.error('Failed to create index.css:', error);
results.errors.push('Failed to create index.css with Tailwind');
}
}
+13 -5
View File
@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import { SandboxFactory } from '@/lib/sandbox/factory';
import { SandboxProvider } from '@/lib/sandbox/types';
// SandboxProvider type is used through SandboxFactory
import type { SandboxState } from '@/types/sandbox';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
// Store active sandbox globally
declare global {
@@ -15,13 +16,16 @@ export async function POST() {
try {
console.log('[create-ai-sandbox-v2] Creating sandbox...');
// Clean up existing sandbox if any
// Clean up all existing sandboxes
console.log('[create-ai-sandbox-v2] Cleaning up existing sandboxes...');
await sandboxManager.terminateAll();
// Also clean up legacy global state
if (global.activeSandboxProvider) {
console.log('[create-ai-sandbox-v2] Terminating existing sandbox...');
try {
await global.activeSandboxProvider.terminate();
} catch (e) {
console.error('Failed to terminate existing sandbox:', e);
console.error('Failed to terminate legacy global sandbox:', e);
}
global.activeSandboxProvider = null;
}
@@ -40,7 +44,10 @@ export async function POST() {
console.log('[create-ai-sandbox-v2] Setting up Vite React app...');
await provider.setupViteApp();
// Store provider globally
// Register with sandbox manager
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
// Also store in legacy global state for backward compatibility
global.activeSandboxProvider = provider;
global.sandboxData = {
sandboxId: sandboxInfo.sandboxId,
@@ -75,6 +82,7 @@ export async function POST() {
console.error('[create-ai-sandbox-v2] Error:', error);
// Clean up on error
await sandboxManager.terminateAll();
if (global.activeSandboxProvider) {
try {
await global.activeSandboxProvider.terminate();
+1 -1
View File
@@ -118,7 +118,7 @@ async function createSandboxInternal() {
// First, change to the working directory
await sandbox.runCommand('pwd');
const workDir = appConfig.vercelSandbox.workingDirectory;
// workDir is defined in appConfig - not needed here
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
+1 -1
View File
@@ -4,7 +4,7 @@ declare global {
var activeSandbox: any;
}
export async function POST(request: NextRequest) {
export async function POST(_request: NextRequest) {
try {
if (!global.activeSandbox) {
return NextResponse.json({
+2 -1
View File
@@ -108,8 +108,9 @@ export async function POST(request: NextRequest) {
} else {
missing.push(packageName);
}
} catch (error) {
} catch (checkError) {
// If test command fails, assume package is missing
console.debug(`Package check failed for ${packageName}:`, checkError);
missing.push(packageName);
}
}
+2 -1
View File
@@ -245,7 +245,8 @@ export async function POST(request: NextRequest) {
// Create surgical edit context with exact location
const normalizedPath = target.filePath.replace('/home/user/app/', '');
const fileContent = fileContents[normalizedPath]?.content || '';
// fileContent available but not used in current implementation
// const fileContent = fileContents[normalizedPath]?.content || '';
// Build enhanced context with search results
enhancedSystemPrompt = `
+5 -3
View File
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server';
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
import type { SandboxState } from '@/types/sandbox';
// SandboxState type used implicitly through global.activeSandbox
declare global {
var activeSandbox: any;
@@ -76,7 +76,8 @@ export async function GET() {
}
}
}
} catch (error) {
} catch (parseError) {
console.debug('Error parsing component info:', parseError);
// Skip files that can't be read
continue;
}
@@ -180,7 +181,8 @@ function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
for (const match of routeMatches) {
const [, routePath, componentRef] = match;
const [, routePath] = match;
// componentRef available in match but not used currently
routes.push({
path: routePath,
component: path,
+6 -2
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global {
var activeSandboxProvider: SandboxProvider | null;
@@ -16,7 +17,10 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
if (!global.activeSandboxProvider) {
// Get provider from sandbox manager or global state
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
if (!provider) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
@@ -25,7 +29,7 @@ export async function POST(request: NextRequest) {
console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`);
const result = await global.activeSandboxProvider.installPackages(packages);
const result = await provider.installPackages(packages);
return NextResponse.json({
success: result.success,
+4 -3
View File
@@ -8,7 +8,8 @@ declare global {
export async function POST(request: NextRequest) {
try {
const { packages, sandboxId } = await request.json();
const { packages } = await request.json();
// sandboxId not used - using global sandbox
if (!packages || !Array.isArray(packages) || packages.length === 0) {
return NextResponse.json({
@@ -75,9 +76,9 @@ export async function POST(request: NextRequest) {
// Try to kill any running dev server processes
await providerInstance.runCommand('pkill -f vite');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
} catch (error) {
} catch (killError) {
// It's OK if no process is found
console.log('[install-packages] No existing dev server found');
console.debug('[install-packages] No existing dev server found:', killError);
}
// Check which packages are already installed
+3 -3
View File
@@ -29,7 +29,7 @@ export async function GET() {
const data = JSON.parse(errorFileContent);
errors.push(...(data.errors || []));
}
} catch (error) {
} catch {
// No error file exists, that's OK
}
@@ -85,12 +85,12 @@ export async function GET() {
}
}
}
} catch (error) {
} catch {
// Skip if grep fails
}
}
}
} catch (error) {
} catch {
// No log files found, that's OK
}
+4 -3
View File
@@ -52,7 +52,7 @@ export async function POST() {
// Wait a moment for processes to terminate
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
} catch {
console.log('[restart-vite] No existing Vite processes found');
}
@@ -62,12 +62,13 @@ export async function POST() {
cmd: 'bash',
args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json']
});
} catch (error) {
} catch {
// Ignore if this fails
}
// Start Vite dev server in detached mode
const viteProcess = await global.activeSandbox.runCommand({
// Start Vite dev server in detached mode
await global.activeSandbox.runCommand({
cmd: 'npm',
args: ['run', 'dev'],
detached: true
+6 -2
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
// Get active sandbox provider from global state
declare global {
@@ -17,7 +18,10 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
if (!global.activeSandboxProvider) {
// Get provider from sandbox manager or global state
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
if (!provider) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
@@ -26,7 +30,7 @@ export async function POST(request: NextRequest) {
console.log(`[run-command-v2] Executing: ${command}`);
const result = await global.activeSandboxProvider.runCommand(command);
const result = await provider.runCommand(command);
return NextResponse.json({
success: result.success,
+4 -4
View File
@@ -4,7 +4,7 @@ declare global {
var activeSandbox: any;
}
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
try {
if (!global.activeSandbox) {
return NextResponse.json({
@@ -22,7 +22,7 @@ export async function GET(request: NextRequest) {
});
let viteRunning = false;
let logContent: string[] = [];
const logContent: string[] = [];
if (psResult.exitCode === 0) {
const psOutput = await psResult.stdout();
@@ -63,12 +63,12 @@ export async function GET(request: NextRequest) {
logContent.push(`--- ${logFile} ---`);
logContent.push(logFileContent);
}
} catch (error) {
} catch {
// Skip if can't read log file
}
}
}
} catch (error) {
} catch {
// No log files found, that's OK
}
+11 -7
View File
@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global {
var activeSandboxProvider: any;
@@ -8,19 +9,22 @@ declare global {
export async function GET() {
try {
// Check if sandbox exists
const sandboxExists = !!global.activeSandboxProvider;
// Check sandbox manager first, then fall back to global state
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
const sandboxExists = !!provider;
let sandboxHealthy = false;
let sandboxInfo = null;
if (sandboxExists && global.activeSandboxProvider) {
if (sandboxExists && provider) {
try {
// Check if sandbox is healthy by calling a method that should work
sandboxHealthy = true;
// Check if sandbox is healthy by getting its info
const providerInfo = provider.getSandboxInfo();
sandboxHealthy = !!providerInfo;
sandboxInfo = {
sandboxId: global.sandboxData?.sandboxId,
url: global.sandboxData?.url,
sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId,
url: providerInfo?.url || global.sandboxData?.url,
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
lastHealthCheck: new Date().toISOString()
};
+25 -14
View File
@@ -38,24 +38,35 @@ export async function POST(req: NextRequest) {
]
});
console.log('[scrape-screenshot] Scrape result success:', scrapeResult.success);
console.log('[scrape-screenshot] Scrape result data:', scrapeResult.data ? Object.keys(scrapeResult.data) : 'No data');
console.log('[scrape-screenshot] Full scrape result:', JSON.stringify(scrapeResult, null, 2));
console.log('[scrape-screenshot] Scrape result type:', typeof scrapeResult);
console.log('[scrape-screenshot] Scrape result keys:', Object.keys(scrapeResult));
if (!scrapeResult.success) {
// The Firecrawl v4 API might return data directly without a success flag
// Check if we have data with screenshot
if (scrapeResult && scrapeResult.screenshot) {
// Direct screenshot response
return NextResponse.json({
success: true,
screenshot: scrapeResult.screenshot,
metadata: scrapeResult.metadata || {}
});
} else if (scrapeResult?.data?.screenshot) {
// Nested data structure
return NextResponse.json({
success: true,
screenshot: scrapeResult.data.screenshot,
metadata: scrapeResult.data.metadata || {}
});
} else if (scrapeResult?.success === false) {
// Explicit failure
console.error('[scrape-screenshot] Firecrawl API error:', scrapeResult.error);
console.error('[scrape-screenshot] Full scrapeResult:', JSON.stringify(scrapeResult, null, 2));
throw new Error(scrapeResult.error || 'Failed to capture screenshot');
} else {
// No screenshot in response
console.error('[scrape-screenshot] No screenshot in response. Full response:', JSON.stringify(scrapeResult, null, 2));
throw new Error('Screenshot not available in response - check console for full response structure');
}
if (!scrapeResult.data?.screenshot) {
throw new Error('Screenshot not available in response');
}
return NextResponse.json({
success: true,
screenshot: scrapeResult.data.screenshot,
metadata: scrapeResult.data.metadata || {}
});
} catch (error: any) {
console.error('[scrape-screenshot] Screenshot capture error:', error);
+2 -1
View File
@@ -72,7 +72,8 @@ export async function POST(request: NextRequest) {
throw new Error('Failed to scrape content');
}
const { markdown, html, metadata, screenshot, actions } = data.data;
const { markdown, metadata, screenshot, actions } = data.data;
// html available but not used in current implementation
// Get screenshot from either direct field or actions result
const screenshotUrl = screenshot || actions?.screenshots?.[0] || null;
+1 -1
View File
@@ -94,7 +94,7 @@ export async function POST(request: NextRequest) {
}
// Optional: Add OPTIONS handler for CORS if needed
export async function OPTIONS(request: NextRequest) {
export async function OPTIONS(_request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
+2 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
@@ -28,6 +28,7 @@ export default function BuilderPage() {
// Start the website generation process
generateWebsite(url, style || "modern");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router]);
const generateWebsite = async (url: string, style: string) => {
+519 -429
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,11 +1,11 @@
"use client";
import { useState } from "react";
// useState not currently used but kept for future interactivity
import Link from "next/link";
// Import shared components
import { HeaderProvider } from "@/components/shared/header/HeaderContext";
import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit";
// import HeaderBrandKit from "@/components/shared/header/BrandKit/BrandKit"; // Not used in current implementation
import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
import ButtonUI from "@/components/ui/shadcn/button";
+2 -5
View File
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client';
import Link from 'next/link';
@@ -3649,11 +3650,7 @@ Focus on the key sections and content, making it clean and modern.`;
</div>
</div>
</div>
</div>
)}
</HeaderProvider>
);
}