update vercel sandbox support
This commit is contained in:
@@ -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})`;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = `
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']
|
||||||
}
|
}
|
||||||
})"""
|
})"""
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user