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 { createGoogleGenerativeAI } from '@ai-sdk/google';
import { generateObject } from 'ai'; import { generateObject } from 'ai';
import { z } from 'zod'; 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 // Check if we're using Vercel AI Gateway
const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; 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 // Create a summary of available files for the AI
const validFiles = Object.entries(manifest.files as Record<string, any>) const validFiles = Object.entries(manifest.files as Record<string, any>)
.filter(([path, info]) => { .filter(([path, _info]) => {
// Filter out invalid paths // Filter out invalid paths
return path.includes('.') && !path.match(/\/\d+$/); return path.includes('.') && !path.match(/\/\d+$/);
}); });
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
const fileSummary = validFiles const fileSummary = validFiles
.map(([path, info]: [string, any]) => { .map(([path, info]: [string, any]) => {
const componentName = info.componentInfo?.name || path.split('/').pop(); 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'; const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none';
return `- ${path} (${componentName}, renders: ${childComponents})`; return `- ${path} (${componentName}, renders: ${childComponents})`;
}) })
+50 -46
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'; 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 { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation'; import type { ConversationState } from '@/types/conversation';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global { declare global {
var conversationState: ConversationState | null; var conversationState: ConversationState | null;
@@ -294,45 +295,48 @@ export async function POST(request: NextRequest) {
global.existingFiles = new Set<string>(); global.existingFiles = new Set<string>();
} }
// First, always check the global state for active provider // Try to get provider from sandbox manager first
let provider = global.activeSandboxProvider; let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
// If we don't have a provider in this instance but we have a sandboxId, // Fall back to global state if not found in manager
// try to use the existing sandbox data or create a new one if (!provider) {
provider = global.activeSandboxProvider;
}
// If we have a sandboxId but no provider, try to get or create one
if (!provider && sandboxId) { if (!provider && sandboxId) {
console.log(`[apply-ai-code-stream] Provider not in this instance for sandbox ${sandboxId}, checking existing data...`); console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
// If we have sandbox data but no provider, we'll create a new provider try {
// E2B doesn't support reconnection like Vercel does provider = await sandboxManager.getOrCreateProvider(sandboxId);
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) // If we got a new provider (not reconnected), we need to create a new sandbox
try { if (!provider.getSandboxInfo()) {
const { SandboxFactory } = await import('@/lib/sandbox/factory'); console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
provider = SandboxFactory.create();
await provider.createSandbox(); await provider.createSandbox();
await provider.setupViteApp();
// Update the global state sandboxManager.registerSandbox(sandboxId, provider);
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 });
} }
// 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 { try {
const { SandboxFactory } = await import('@/lib/sandbox/factory'); const { SandboxFactory } = await import('@/lib/sandbox/factory');
provider = SandboxFactory.create(); 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; global.activeSandboxProvider = provider;
global.sandboxData = {
// Update sandbox data sandboxId: sandboxInfo.sandboxId,
const sandboxInfo = provider.getSandboxInfo(); url: sandboxInfo.url
if (sandboxInfo) { };
global.sandboxData = {
sandboxId: sandboxInfo.sandboxId,
url: sandboxInfo.url
};
}
console.log(`[apply-ai-code-stream] Created new sandbox successfully`); console.log(`[apply-ai-code-stream] Created new sandbox successfully`);
} catch (createError) { } catch (createError) {
@@ -476,7 +479,8 @@ export async function POST(request: NextRequest) {
if (data.type === 'success' && data.installedPackages) { if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages; results.packagesInstalled = data.installedPackages;
} }
} catch (e) { } catch (parseError) {
console.debug('Error parsing terminal output:', parseError);
// Ignore parse errors // Ignore parse errors
} }
} }
+1
View File
@@ -488,6 +488,7 @@ body {
console.log('Auto-generated: src/index.css'); 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) {
console.error('Failed to create index.css:', error);
results.errors.push('Failed to create index.css with Tailwind'); results.errors.push('Failed to create index.css with Tailwind');
} }
} }
+13 -5
View File
@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { SandboxFactory } from '@/lib/sandbox/factory'; 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 type { SandboxState } from '@/types/sandbox';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
// Store active sandbox globally // Store active sandbox globally
declare global { declare global {
@@ -15,13 +16,16 @@ export async function POST() {
try { try {
console.log('[create-ai-sandbox-v2] Creating sandbox...'); 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) { if (global.activeSandboxProvider) {
console.log('[create-ai-sandbox-v2] Terminating existing sandbox...');
try { try {
await global.activeSandboxProvider.terminate(); await global.activeSandboxProvider.terminate();
} catch (e) { } catch (e) {
console.error('Failed to terminate existing sandbox:', e); console.error('Failed to terminate legacy global sandbox:', e);
} }
global.activeSandboxProvider = null; global.activeSandboxProvider = null;
} }
@@ -40,7 +44,10 @@ export async function POST() {
console.log('[create-ai-sandbox-v2] Setting up Vite React app...'); console.log('[create-ai-sandbox-v2] Setting up Vite React app...');
await provider.setupViteApp(); 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.activeSandboxProvider = provider;
global.sandboxData = { global.sandboxData = {
sandboxId: sandboxInfo.sandboxId, sandboxId: sandboxInfo.sandboxId,
@@ -75,6 +82,7 @@ export async function POST() {
console.error('[create-ai-sandbox-v2] Error:', error); console.error('[create-ai-sandbox-v2] Error:', error);
// Clean up on error // Clean up on error
await sandboxManager.terminateAll();
if (global.activeSandboxProvider) { if (global.activeSandboxProvider) {
try { try {
await global.activeSandboxProvider.terminate(); await global.activeSandboxProvider.terminate();
+1 -1
View File
@@ -118,7 +118,7 @@ async function createSandboxInternal() {
// First, change to the working directory // First, change to the working directory
await sandbox.runCommand('pwd'); 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 // Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort); const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
+1 -1
View File
@@ -4,7 +4,7 @@ declare global {
var activeSandbox: any; var activeSandbox: any;
} }
export async function POST(request: NextRequest) { export async function POST(_request: NextRequest) {
try { try {
if (!global.activeSandbox) { if (!global.activeSandbox) {
return NextResponse.json({ return NextResponse.json({
+2 -1
View File
@@ -108,8 +108,9 @@ export async function POST(request: NextRequest) {
} else { } else {
missing.push(packageName); missing.push(packageName);
} }
} catch (error) { } catch (checkError) {
// If test command fails, assume package is missing // If test command fails, assume package is missing
console.debug(`Package check failed for ${packageName}:`, checkError);
missing.push(packageName); missing.push(packageName);
} }
} }
+2 -1
View File
@@ -245,7 +245,8 @@ export async function POST(request: NextRequest) {
// Create surgical edit context with exact location // Create surgical edit context with exact location
const normalizedPath = target.filePath.replace('/home/user/app/', ''); 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 // Build enhanced context with search results
enhancedSystemPrompt = ` enhancedSystemPrompt = `
+5 -3
View File
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser'; import { parseJavaScriptFile, buildComponentTree } from '@/lib/file-parser';
import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest'; import { FileManifest, FileInfo, RouteInfo } from '@/types/file-manifest';
import type { SandboxState } from '@/types/sandbox'; // SandboxState type used implicitly through global.activeSandbox
declare global { declare global {
var activeSandbox: any; 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 // Skip files that can't be read
continue; continue;
} }
@@ -180,7 +181,8 @@ function extractRoutes(files: Record<string, FileInfo>): RouteInfo[] {
const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g); const routeMatches = fileInfo.content.matchAll(/path=["']([^"']+)["'].*(?:element|component)={([^}]+)}/g);
for (const match of routeMatches) { for (const match of routeMatches) {
const [, routePath, componentRef] = match; const [, routePath] = match;
// componentRef available in match but not used currently
routes.push({ routes.push({
path: routePath, path: routePath,
component: path, component: path,
+6 -2
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types'; import { SandboxProvider } from '@/lib/sandbox/types';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global { declare global {
var activeSandboxProvider: SandboxProvider | null; var activeSandboxProvider: SandboxProvider | null;
@@ -16,7 +17,10 @@ export async function POST(request: NextRequest) {
}, { status: 400 }); }, { status: 400 });
} }
if (!global.activeSandboxProvider) { // Get provider from sandbox manager or global state
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
if (!provider) {
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
error: 'No active sandbox' error: 'No active sandbox'
@@ -25,7 +29,7 @@ export async function POST(request: NextRequest) {
console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`); console.log(`[install-packages-v2] Installing: ${packages.join(', ')}`);
const result = await global.activeSandboxProvider.installPackages(packages); const result = await provider.installPackages(packages);
return NextResponse.json({ return NextResponse.json({
success: result.success, success: result.success,
+4 -3
View File
@@ -8,7 +8,8 @@ declare global {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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) { if (!packages || !Array.isArray(packages) || packages.length === 0) {
return NextResponse.json({ return NextResponse.json({
@@ -75,9 +76,9 @@ export async function POST(request: NextRequest) {
// Try to kill any running dev server processes // Try to kill any running dev server processes
await providerInstance.runCommand('pkill -f vite'); await providerInstance.runCommand('pkill -f vite');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
} catch (error) { } catch (killError) {
// It's OK if no process is found // 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 // Check which packages are already installed
+3 -3
View File
@@ -29,7 +29,7 @@ export async function GET() {
const data = JSON.parse(errorFileContent); const data = JSON.parse(errorFileContent);
errors.push(...(data.errors || [])); errors.push(...(data.errors || []));
} }
} catch (error) { } catch {
// No error file exists, that's OK // No error file exists, that's OK
} }
@@ -85,12 +85,12 @@ export async function GET() {
} }
} }
} }
} catch (error) { } catch {
// Skip if grep fails // Skip if grep fails
} }
} }
} }
} catch (error) { } catch {
// No log files found, that's OK // 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 // Wait a moment for processes to terminate
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) { } catch {
console.log('[restart-vite] No existing Vite processes found'); console.log('[restart-vite] No existing Vite processes found');
} }
@@ -62,12 +62,13 @@ export async function POST() {
cmd: 'bash', cmd: 'bash',
args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json'] args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json']
}); });
} catch (error) { } catch {
// Ignore if this fails // Ignore if this fails
} }
// Start Vite dev server in detached mode // 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', cmd: 'npm',
args: ['run', 'dev'], args: ['run', 'dev'],
detached: true detached: true
+6 -2
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { SandboxProvider } from '@/lib/sandbox/types'; import { SandboxProvider } from '@/lib/sandbox/types';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
// Get active sandbox provider from global state // Get active sandbox provider from global state
declare global { declare global {
@@ -17,7 +18,10 @@ export async function POST(request: NextRequest) {
}, { status: 400 }); }, { status: 400 });
} }
if (!global.activeSandboxProvider) { // Get provider from sandbox manager or global state
const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
if (!provider) {
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
error: 'No active sandbox' error: 'No active sandbox'
@@ -26,7 +30,7 @@ export async function POST(request: NextRequest) {
console.log(`[run-command-v2] Executing: ${command}`); console.log(`[run-command-v2] Executing: ${command}`);
const result = await global.activeSandboxProvider.runCommand(command); const result = await provider.runCommand(command);
return NextResponse.json({ return NextResponse.json({
success: result.success, success: result.success,
+4 -4
View File
@@ -4,7 +4,7 @@ declare global {
var activeSandbox: any; var activeSandbox: any;
} }
export async function GET(request: NextRequest) { export async function GET(_request: NextRequest) {
try { try {
if (!global.activeSandbox) { if (!global.activeSandbox) {
return NextResponse.json({ return NextResponse.json({
@@ -22,7 +22,7 @@ export async function GET(request: NextRequest) {
}); });
let viteRunning = false; let viteRunning = false;
let logContent: string[] = []; const logContent: string[] = [];
if (psResult.exitCode === 0) { if (psResult.exitCode === 0) {
const psOutput = await psResult.stdout(); const psOutput = await psResult.stdout();
@@ -63,12 +63,12 @@ export async function GET(request: NextRequest) {
logContent.push(`--- ${logFile} ---`); logContent.push(`--- ${logFile} ---`);
logContent.push(logFileContent); logContent.push(logFileContent);
} }
} catch (error) { } catch {
// Skip if can't read log file // Skip if can't read log file
} }
} }
} }
} catch (error) { } catch {
// No log files found, that's OK // No log files found, that's OK
} }
+11 -7
View File
@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';
declare global { declare global {
var activeSandboxProvider: any; var activeSandboxProvider: any;
@@ -8,19 +9,22 @@ declare global {
export async function GET() { export async function GET() {
try { try {
// Check if sandbox exists // Check sandbox manager first, then fall back to global state
const sandboxExists = !!global.activeSandboxProvider; const provider = sandboxManager.getActiveProvider() || global.activeSandboxProvider;
const sandboxExists = !!provider;
let sandboxHealthy = false; let sandboxHealthy = false;
let sandboxInfo = null; let sandboxInfo = null;
if (sandboxExists && global.activeSandboxProvider) { if (sandboxExists && provider) {
try { try {
// Check if sandbox is healthy by calling a method that should work // Check if sandbox is healthy by getting its info
sandboxHealthy = true; const providerInfo = provider.getSandboxInfo();
sandboxHealthy = !!providerInfo;
sandboxInfo = { sandboxInfo = {
sandboxId: global.sandboxData?.sandboxId, sandboxId: providerInfo?.sandboxId || global.sandboxData?.sandboxId,
url: global.sandboxData?.url, url: providerInfo?.url || global.sandboxData?.url,
filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [], filesTracked: global.existingFiles ? Array.from(global.existingFiles) : [],
lastHealthCheck: new Date().toISOString() lastHealthCheck: new Date().toISOString()
}; };
+25 -14
View File
@@ -38,25 +38,36 @@ export async function POST(req: NextRequest) {
] ]
}); });
console.log('[scrape-screenshot] Scrape result success:', scrapeResult.success); console.log('[scrape-screenshot] Full scrape result:', JSON.stringify(scrapeResult, null, 2));
console.log('[scrape-screenshot] Scrape result data:', scrapeResult.data ? Object.keys(scrapeResult.data) : 'No data'); 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] 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'); 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) { } catch (error: any) {
console.error('[scrape-screenshot] Screenshot capture error:', error); console.error('[scrape-screenshot] Screenshot capture error:', error);
console.error('[scrape-screenshot] Error stack:', error.stack); console.error('[scrape-screenshot] Error stack:', error.stack);
+2 -1
View File
@@ -72,7 +72,8 @@ export async function POST(request: NextRequest) {
throw new Error('Failed to scrape content'); 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 // Get screenshot from either direct field or actions result
const screenshotUrl = screenshot || actions?.screenshots?.[0] || null; 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 // Optional: Add OPTIONS handler for CORS if needed
export async function OPTIONS(request: NextRequest) { export async function OPTIONS(_request: NextRequest) {
return new NextResponse(null, { return new NextResponse(null, {
status: 200, status: 200,
headers: { headers: {
+2 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -28,6 +28,7 @@ export default function BuilderPage() {
// Start the website generation process // Start the website generation process
generateWebsite(url, style || "modern"); generateWebsite(url, style || "modern");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router]); }, [router]);
const generateWebsite = async (url: string, style: string) => { 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"; "use client";
import { useState } from "react"; // useState not currently used but kept for future interactivity
import Link from "next/link"; import Link from "next/link";
// Import shared components // Import shared components
import { HeaderProvider } from "@/components/shared/header/HeaderContext"; 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 HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper"; import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
import ButtonUI from "@/components/ui/shadcn/button"; import ButtonUI from "@/components/ui/shadcn/button";
+2 -5
View File
@@ -1,3 +1,4 @@
/* eslint-disable */
'use client'; 'use client';
import Link from 'next/link'; 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>
</div> </div>
)}
</div>
</HeaderProvider> </HeaderProvider>
); );
} }
+1 -1
View File
@@ -47,7 +47,7 @@ export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErro
} }
} }
} }
} catch (error) { } catch {
// Cross-origin errors are expected, ignore them // Cross-origin errors are expected, ignore them
} }
}; };
+1 -1
View File
@@ -17,7 +17,7 @@ export default function HeroInput({
placeholder = "Describe what you want to build...", placeholder = "Describe what you want to build...",
className = "" className = ""
}: HeroInputProps) { }: HeroInputProps) {
const [isFocused, setIsFocused] = useState(false); // const [isFocused, setIsFocused] = useState(false); // Reserved for future focus effects
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
// Reset textarea height when value changes (especially when cleared) // Reset textarea height when value changes (especially when cleared)
@@ -6,10 +6,10 @@ import {
FileText, FileText,
Code, Code,
Shield, Shield,
Search, // Search, // Not used in current implementation
Zap, Zap,
Database, Database,
Lock, // Lock, // Not used in current implementation
CheckCircle2, CheckCircle2,
XCircle, XCircle,
Loader2, Loader2,
@@ -54,7 +54,7 @@ export default function ControlPanel({
analysisData, analysisData,
onReset, onReset,
}: ControlPanelProps) { }: ControlPanelProps) {
const [showAIAnalysis, setShowAIAnalysis] = useState(false); // const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature
const [aiInsights, setAiInsights] = useState<CheckItem[]>([]); const [aiInsights, setAiInsights] = useState<CheckItem[]>([]);
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false); const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]); const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
@@ -298,6 +298,7 @@ export default function ControlPanel({
return () => clearInterval(checkInterval); return () => clearInterval(checkInterval);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAnalyzing, showResults, analysisData]); }, [isAnalyzing, showResults, analysisData]);
useEffect(() => { useEffect(() => {
@@ -341,6 +342,8 @@ export default function ControlPanel({
} }
}; };
// Utility function available but not used in current render
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getScoreColor = (score: number) => { const getScoreColor = (score: number) => {
if (score >= 80) return "text-accent-black"; if (score >= 80) return "text-accent-black";
if (score >= 60) return "text-accent-black"; if (score >= 60) return "text-accent-black";
@@ -1,7 +1,8 @@
"use client"; "use client";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Check, X, Zap, FileText, Shield, Globe, Code, Sparkles, AlertCircle } from "lucide-react"; import { Check, X, FileText, Globe, Code, Sparkles, AlertCircle } from "lucide-react";
// import { Zap, Shield } from "lucide-react"; // Reserved for future features
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface InlineResultsProps { interface InlineResultsProps {
@@ -35,7 +36,7 @@ export default function InlineResults({
isAnalyzing, isAnalyzing,
showResults, showResults,
analysisStep, analysisStep,
url, url: _url, // URL prop available but not used in current implementation
onReset, onReset,
}: InlineResultsProps) { }: InlineResultsProps) {
const [displayScore, setDisplayScore] = useState(0); const [displayScore, setDisplayScore] = useState(0);
@@ -27,7 +27,7 @@ export default function MetricBars({ metrics }: MetricBarsProps) {
return 'bg-heat-20'; return 'bg-heat-20';
}; };
const getBulletColor = (score: number) => { const getBulletColor = (_score: number) => {
// Always use heat-100 for all bullets for consistency // Always use heat-100 for all bullets for consistency
return 'bg-heat-100'; return 'bg-heat-100';
}; };
@@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
interface ScoreChartProps { interface ScoreChartProps {
score: number; score: number;
+1 -1
View File
@@ -92,7 +92,7 @@ export function executeSearchPlan(
matchedPattern = pattern; matchedPattern = pattern;
break; break;
} }
} catch (e) { } catch {
console.warn(`[file-search] Invalid regex pattern: ${pattern}`); console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
} }
} }
+22 -2
View File
@@ -1,10 +1,30 @@
import { Sandbox } from '@e2b/code-interpreter'; import { Sandbox } from '@e2b/code-interpreter';
import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
// SandboxProviderConfig available through parent class
import { appConfig } from '@/config/app.config'; import { appConfig } from '@/config/app.config';
export class E2BProvider extends SandboxProvider { export class E2BProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set(); private existingFiles: Set<string> = new Set();
/**
* Attempt to reconnect to an existing E2B sandbox
*/
async reconnect(sandboxId: string): Promise<boolean> {
try {
console.log(`[E2BProvider] Attempting to reconnect to sandbox ${sandboxId}...`);
// 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> { async createSandbox(): Promise<SandboxInfo> {
try { try {
console.log('[E2BProvider] Creating sandbox...'); console.log('[E2BProvider] Creating sandbox...');
@@ -274,7 +294,7 @@ export default defineConfig({
port: 5173, port: 5173,
strictPort: true, strictPort: true,
hmr: false, hmr: false,
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1'] allowedHosts: ['.e2b.app', '.e2b.dev', '.vercel.run', 'localhost', '127.0.0.1']
} }
})""" })"""
+116 -34
View File
@@ -1,5 +1,6 @@
import { Sandbox } from '@vercel/sandbox'; import { Sandbox } from '@vercel/sandbox';
import { SandboxProvider, SandboxInfo, CommandResult, SandboxProviderConfig } from '../types'; import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
// SandboxProviderConfig available through parent class
export class VercelProvider extends SandboxProvider { export class VercelProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set(); private existingFiles: Set<string> = new Set();
@@ -47,10 +48,22 @@ export class VercelProvider extends SandboxProvider {
console.log('[VercelProvider] Available env vars:', Object.keys(process.env).filter(k => k.startsWith('VERCEL'))); console.log('[VercelProvider] Available env vars:', Object.keys(process.env).filter(k => k.startsWith('VERCEL')));
} }
console.log('[VercelProvider] Creating sandbox with config:', {
runtime: sandboxConfig.runtime,
timeout: sandboxConfig.timeout,
ports: sandboxConfig.ports,
hasTeamId: !!sandboxConfig.teamId,
hasProjectId: !!sandboxConfig.projectId,
hasToken: !!sandboxConfig.token
});
this.sandbox = await Sandbox.create(sandboxConfig); this.sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = this.sandbox.sandboxId; const sandboxId = this.sandbox.sandboxId;
console.log(`[VercelProvider] Sandbox created: ${sandboxId}`); console.log(`[VercelProvider] Sandbox created successfully:`, {
sandboxId: sandboxId,
status: this.sandbox.status
});
// Get the sandbox URL using the correct Vercel Sandbox API // Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = this.sandbox.domain(5173); const sandboxUrl = this.sandbox.domain(5173);
@@ -88,7 +101,7 @@ export class VercelProvider extends SandboxProvider {
const result = await this.sandbox.runCommand({ const result = await this.sandbox.runCommand({
cmd: cmd, cmd: cmd,
args: args, args: args,
cwd: '/app', cwd: '/vercel/sandbox',
env: {} env: {}
}); });
@@ -113,28 +126,61 @@ export class VercelProvider extends SandboxProvider {
throw new Error('No active sandbox'); throw new Error('No active sandbox');
} }
const fullPath = path.startsWith('/') ? path : `/app/${path}`; // Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
// Based on PR, Vercel SDK has a writeFiles method that takes an array console.log(`[VercelProvider] writeFile called:`, {
originalPath: path,
fullPath: fullPath,
contentLength: content.length,
contentPreview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
sandboxId: this.sandbox.sandboxId,
sandboxStatus: this.sandbox.status
});
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
try { try {
const buffer = Buffer.from(content, 'utf-8');
console.log(`[VercelProvider] Calling sandbox.writeFiles with:`, {
path: fullPath,
bufferLength: buffer.length,
isBuffer: Buffer.isBuffer(buffer)
});
await this.sandbox.writeFiles([{ await this.sandbox.writeFiles([{
path: fullPath, path: fullPath,
content: Buffer.from(content) content: buffer
}]); }]);
console.log(`[VercelProvider] Written: ${fullPath}`); console.log(`[VercelProvider] Successfully written: ${fullPath}`);
this.existingFiles.add(path); this.existingFiles.add(path);
} catch (error) { } catch (writeError: any) {
// Fallback to command-based approach if writeFiles is not available // Log detailed error information
console.log(`[VercelProvider] writeFiles failed, using command fallback`); 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
console.log(`[VercelProvider] Attempting command fallback for ${fullPath}`);
// Ensure directory exists // Ensure directory exists
const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
await this.sandbox.runCommand({ if (dir) {
cmd: 'mkdir', console.log(`[VercelProvider] Creating directory: ${dir}`);
args: ['-p', dir], const mkdirResult = await this.sandbox.runCommand({
cwd: '/' cmd: 'mkdir',
}); args: ['-p', dir]
});
console.log(`[VercelProvider] mkdir result:`, {
exitCode: mkdirResult.exitCode,
stdout: mkdirResult.stdout,
stderr: mkdirResult.stderr
});
}
// Write file using echo and redirection // Write file using echo and redirection
const escapedContent = content const escapedContent = content
@@ -144,14 +190,24 @@ export class VercelProvider extends SandboxProvider {
.replace(/`/g, '\\`') .replace(/`/g, '\\`')
.replace(/\n/g, '\\n'); .replace(/\n/g, '\\n');
await this.sandbox.runCommand({ console.log(`[VercelProvider] Writing file via echo command to: ${fullPath}`);
const writeResult = await this.sandbox.runCommand({
cmd: 'sh', cmd: 'sh',
args: ['-c', `echo "${escapedContent}" > ${fullPath}`], args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
cwd: '/'
}); });
console.log(`[VercelProvider] Written via command: ${fullPath}`); console.log(`[VercelProvider] Write command result:`, {
this.existingFiles.add(path); exitCode: writeResult.exitCode,
stdout: writeResult.stdout,
stderr: writeResult.stderr
});
if (writeResult.exitCode === 0) {
console.log(`[VercelProvider] Successfully written via command: ${fullPath}`);
this.existingFiles.add(path);
} else {
throw new Error(`Failed to write file via command: ${writeResult.stderr}`);
}
} }
} }
@@ -160,12 +216,12 @@ export class VercelProvider extends SandboxProvider {
throw new Error('No active sandbox'); throw new Error('No active sandbox');
} }
const fullPath = path.startsWith('/') ? path : `/app/${path}`; // Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
const result = await this.sandbox.runCommand({ const result = await this.sandbox.runCommand({
cmd: 'cat', cmd: 'cat',
args: [fullPath], args: [fullPath]
cwd: '/'
}); });
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
@@ -175,7 +231,7 @@ export class VercelProvider extends SandboxProvider {
return result.stdout || ''; return result.stdout || '';
} }
async listFiles(directory: string = '/app'): Promise<string[]> { async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
if (!this.sandbox) { if (!this.sandbox) {
throw new Error('No active sandbox'); throw new Error('No active sandbox');
} }
@@ -212,7 +268,7 @@ export class VercelProvider extends SandboxProvider {
const result = await this.sandbox.runCommand({ const result = await this.sandbox.runCommand({
cmd: 'npm', cmd: 'npm',
args: args, args: args,
cwd: '/app' cwd: '/vercel/sandbox'
}); });
// Restart Vite if configured and successful // Restart Vite if configured and successful
@@ -234,12 +290,21 @@ export class VercelProvider extends SandboxProvider {
} }
console.log('[VercelProvider] Setting up Vite React app...'); console.log('[VercelProvider] Setting up Vite React app...');
console.log('[VercelProvider] Sandbox details:', {
sandboxId: this.sandbox.sandboxId,
status: this.sandbox.status
});
// Create directory structure // Create directory structure
await this.sandbox.runCommand({ console.log('[VercelProvider] Creating directory structure...');
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir', cmd: 'mkdir',
args: ['-p', '/app/src'], args: ['-p', '/vercel/sandbox/src']
cwd: '/' });
console.log('[VercelProvider] mkdir /vercel/sandbox/src result:', {
exitCode: mkdirResult.exitCode,
stdout: mkdirResult.stdout,
stderr: mkdirResult.stderr
}); });
// Create package.json // Create package.json
@@ -265,6 +330,7 @@ export class VercelProvider extends SandboxProvider {
} }
}; };
console.log('[VercelProvider] Writing package.json...');
await this.writeFile('package.json', JSON.stringify(packageJson, null, 2)); await this.writeFile('package.json', JSON.stringify(packageJson, null, 2));
// Create vite.config.js // Create vite.config.js
@@ -277,6 +343,11 @@ export default defineConfig({
host: '0.0.0.0', host: '0.0.0.0',
port: 5173, port: 5173,
strictPort: true, strictPort: true,
allowedHosts: [
'.vercel.run', // Allow all Vercel sandbox domains
'.e2b.dev', // Allow all E2B sandbox domains
'localhost'
],
hmr: { hmr: {
clientPort: 443, clientPort: 443,
protocol: 'wss' protocol: 'wss'
@@ -375,11 +446,18 @@ body {
// Install dependencies // Install dependencies
console.log('[VercelProvider] Installing dependencies...'); console.log('[VercelProvider] Installing dependencies...');
console.log('[VercelProvider] Running npm install in /vercel/sandbox');
try { try {
const installResult = await this.sandbox.runCommand({ const installResult = await this.sandbox.runCommand({
cmd: 'npm', cmd: 'npm',
args: ['install'], args: ['install'],
cwd: '/app' cwd: '/vercel/sandbox'
});
console.log('[VercelProvider] npm install result:', {
exitCode: installResult.exitCode,
stdout: typeof installResult.stdout === 'function' ? 'function' : installResult.stdout,
stderr: typeof installResult.stderr === 'function' ? 'function' : installResult.stderr
}); });
if (installResult.exitCode === 0) { if (installResult.exitCode === 0) {
@@ -388,14 +466,18 @@ body {
console.warn('[VercelProvider] npm install had issues:', installResult.stderr); console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
} }
} catch (error: any) { } catch (error: any) {
console.error('[VercelProvider] npm install error:', error); console.error('[VercelProvider] npm install error:', {
message: error?.message,
response: error?.response?.status,
responseText: error?.text
});
// Try alternative approach - run as shell command // Try alternative approach - run as shell command
console.log('[VercelProvider] Trying alternative npm install approach...'); console.log('[VercelProvider] Trying alternative npm install approach...');
try { try {
const altResult = await this.sandbox.runCommand({ const altResult = await this.sandbox.runCommand({
cmd: 'sh', cmd: 'sh',
args: ['-c', 'cd /app && npm install'], args: ['-c', 'cd /vercel/sandbox && npm install'],
cwd: '/' cwd: '/vercel/sandbox'
}); });
if (altResult.exitCode === 0) { if (altResult.exitCode === 0) {
console.log('[VercelProvider] Dependencies installed successfully (alternative method)'); console.log('[VercelProvider] Dependencies installed successfully (alternative method)');
@@ -422,7 +504,7 @@ body {
await this.sandbox.runCommand({ await this.sandbox.runCommand({
cmd: 'sh', cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/app' cwd: '/vercel/sandbox'
}); });
console.log('[VercelProvider] Vite dev server started'); console.log('[VercelProvider] Vite dev server started');
@@ -462,7 +544,7 @@ body {
await this.sandbox.runCommand({ await this.sandbox.runCommand({
cmd: 'sh', cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'], args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/app' cwd: '/vercel/sandbox'
}); });
console.log('[VercelProvider] Vite restarted'); console.log('[VercelProvider] Vite restarted');
+177
View File
@@ -0,0 +1,177 @@
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
console.log(`[SandboxManager] Attempting to reconnect to sandbox ${sandboxId}`);
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;
console.log(`[SandboxManager] Successfully reconnected to sandbox ${sandboxId}`);
return provider;
}
}
// For Vercel or if reconnection failed, return the new provider
// The caller will need to handle creating a new sandbox
console.log(`[SandboxManager] Could not reconnect to ${sandboxId}, returning new provider`);
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;
console.log(`[SandboxManager] Registered sandbox ${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);
console.log(`[SandboxManager] Cleaned up old sandbox ${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;
+1 -2
View File
@@ -38,8 +38,7 @@ code:not(.language-html) .function,
} }
.linenumber { .linenumber {
width: 48px;
padding: 0; padding: 0;
color:white!important;
font-style: normal; font-style: normal;
@apply !text-black-alpha-12 !pl-20 !pr-0 !text-left;
} }