Merge pull request #109 from MFCo/vercel-sandbox
Vercel sandbox support
This commit is contained in:
+16
-16
@@ -1,20 +1,20 @@
|
|||||||
# REQUIRED - Sandboxes for code execution
|
# Required
|
||||||
# Get yours at https://e2b.dev
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
||||||
E2B_API_KEY=your_e2b_api_key_here
|
|
||||||
|
|
||||||
# REQUIRED - Web scraping for cloning websites
|
# Vercel Sandbox Authentication (choose one method)
|
||||||
# Get yours at https://firecrawl.dev
|
# See: https://vercel.com/docs/vercel-sandbox#authentication
|
||||||
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
|
|
||||||
|
|
||||||
# OPTIONAL - AI Providers (need at least one)
|
# Method 1: OIDC Token (recommended for development)
|
||||||
# Get yours at https://console.anthropic.com
|
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
# Get yours at https://platform.openai.com
|
# Method 2: Personal Access Token (for production or when OIDC unavailable)
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
||||||
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
||||||
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
||||||
|
|
||||||
# Get yours at https://aistudio.google.com/app/apikey
|
# Optional (need at least one AI provider)
|
||||||
GEMINI_API_KEY=your_gemini_api_key_here
|
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
||||||
|
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
||||||
# Get yours at https://console.groq.com
|
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
|
||||||
GROQ_API_KEY=your_groq_api_key_here
|
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended)
|
||||||
@@ -56,3 +56,4 @@ e2b-template-*
|
|||||||
*.temp
|
*.temp
|
||||||
repomix-output.txt
|
repomix-output.txt
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
.env*.local
|
||||||
|
|||||||
@@ -16,11 +16,23 @@ npm install
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Add `.env.local`**
|
2. **Add `.env.local`**
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Required
|
# Required
|
||||||
E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes)
|
|
||||||
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
||||||
|
|
||||||
|
# Vercel Sandbox Authentication (choose one method)
|
||||||
|
# See: https://vercel.com/docs/vercel-sandbox#authentication
|
||||||
|
|
||||||
|
# Method 1: OIDC Token (recommended for development)
|
||||||
|
# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
|
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
|
# Method 2: Personal Access Token (for production or when OIDC unavailable)
|
||||||
|
# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID
|
||||||
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID
|
||||||
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard
|
||||||
|
|
||||||
# Optional (need at least one AI provider)
|
# Optional (need at least one AI provider)
|
||||||
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
||||||
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
import { Sandbox } from '@vercel/sandbox';
|
||||||
import type { SandboxState } from '@/types/sandbox';
|
import type { SandboxState } from '@/types/sandbox';
|
||||||
import type { ConversationState } from '@/types/conversation';
|
import type { ConversationState } from '@/types/conversation';
|
||||||
|
|
||||||
@@ -525,7 +525,6 @@ export async function POST(request: NextRequest) {
|
|||||||
normalizedPath = 'src/' + normalizedPath;
|
normalizedPath = 'src/' + normalizedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = `/home/user/app/${normalizedPath}`;
|
|
||||||
const isUpdate = global.existingFiles.has(normalizedPath);
|
const isUpdate = global.existingFiles.has(normalizedPath);
|
||||||
|
|
||||||
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
|
||||||
@@ -534,19 +533,20 @@ export async function POST(request: NextRequest) {
|
|||||||
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the file using Python (code-interpreter SDK)
|
// Create directory if needed
|
||||||
const escapedContent = fileContent
|
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
|
||||||
.replace(/\\/g, '\\\\')
|
if (dirPath) {
|
||||||
.replace(/"""/g, '\\"\\"\\"')
|
await sandboxInstance.runCommand({
|
||||||
.replace(/\$/g, '\\$');
|
cmd: 'mkdir',
|
||||||
|
args: ['-p', dirPath]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await sandboxInstance.runCode(`
|
// Write the file using Vercel Sandbox writeFiles
|
||||||
import os
|
await sandboxInstance.writeFiles([{
|
||||||
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
|
path: normalizedPath,
|
||||||
with open("${fullPath}", 'w') as f:
|
content: Buffer.from(fileContent)
|
||||||
f.write("""${escapedContent}""")
|
}]);
|
||||||
print(f"File written: ${fullPath}")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Update file cache
|
// Update file cache
|
||||||
if (global.sandboxState?.fileCache) {
|
if (global.sandboxState?.fileCache) {
|
||||||
@@ -599,27 +599,38 @@ print(f"File written: ${fullPath}")
|
|||||||
action: 'executing'
|
action: 'executing'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use E2B commands.run() for cleaner execution
|
// Parse command and arguments for Vercel Sandbox
|
||||||
const result = await sandboxInstance.commands.run(cmd, {
|
const commandParts = cmd.trim().split(/\s+/);
|
||||||
cwd: '/home/user/app',
|
const cmdName = commandParts[0];
|
||||||
timeout: 60,
|
const args = commandParts.slice(1);
|
||||||
on_stdout: async (data: string) => {
|
|
||||||
|
// Use Vercel Sandbox runCommand
|
||||||
|
const result = await sandboxInstance.runCommand({
|
||||||
|
cmd: cmdName,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get command output
|
||||||
|
const stdout = await result.stdout();
|
||||||
|
const stderr = await result.stderr();
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'command-output',
|
type: 'command-output',
|
||||||
command: cmd,
|
command: cmd,
|
||||||
output: data,
|
output: stdout,
|
||||||
stream: 'stdout'
|
stream: 'stdout'
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
on_stderr: async (data: string) => {
|
|
||||||
|
if (stderr) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'command-output',
|
type: 'command-output',
|
||||||
command: cmd,
|
command: cmd,
|
||||||
output: data,
|
output: stderr,
|
||||||
stream: 'stderr'
|
stream: 'stderr'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (results.commandsExecuted) {
|
if (results.commandsExecuted) {
|
||||||
results.commandsExecuted.push(cmd);
|
results.commandsExecuted.push(cmd);
|
||||||
|
|||||||
@@ -432,15 +432,12 @@ function App() {
|
|||||||
export default App;`;
|
export default App;`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
await global.activeSandbox.writeFiles([{
|
||||||
file_path = "/home/user/app/src/App.jsx"
|
path: 'src/App.jsx',
|
||||||
file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"""
|
content: Buffer.from(appContent)
|
||||||
|
}]);
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
console.log('Auto-generated: src/App.jsx');
|
||||||
f.write(file_content)
|
|
||||||
|
|
||||||
print(f"Auto-generated: {file_path}")
|
|
||||||
`);
|
|
||||||
results.filesCreated.push('src/App.jsx (auto-generated)');
|
results.filesCreated.push('src/App.jsx (auto-generated)');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
|
results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`);
|
||||||
@@ -459,9 +456,7 @@ print(f"Auto-generated: {file_path}")
|
|||||||
|
|
||||||
if (!isEdit && !indexCssInParsed && !indexCssExists) {
|
if (!isEdit && !indexCssInParsed && !indexCssExists) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
const indexCssContent = `@tailwind base;
|
||||||
file_path = "/home/user/app/src/index.css"
|
|
||||||
file_content = """@tailwind base;
|
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@@ -483,13 +478,14 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}"""
|
}`;
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
await global.activeSandbox.writeFiles([{
|
||||||
f.write(file_content)
|
path: 'src/index.css',
|
||||||
|
content: Buffer.from(indexCssContent)
|
||||||
|
}]);
|
||||||
|
|
||||||
print(f"Auto-generated: {file_path}")
|
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) {
|
||||||
results.errors.push('Failed to create index.css with Tailwind');
|
results.errors.push('Failed to create index.css with Tailwind');
|
||||||
@@ -500,15 +496,24 @@ print(f"Auto-generated: {file_path}")
|
|||||||
// Execute commands
|
// Execute commands
|
||||||
for (const cmd of parsed.commands) {
|
for (const cmd of parsed.commands) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.runCode(`
|
// Parse command and arguments
|
||||||
import subprocess
|
const commandParts = cmd.trim().split(/\s+/);
|
||||||
os.chdir('/home/user/app')
|
const cmdName = commandParts[0];
|
||||||
result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True)
|
const args = commandParts.slice(1);
|
||||||
print(f"Executed: ${cmd}")
|
|
||||||
print(result.stdout)
|
// Execute command using Vercel Sandbox
|
||||||
if result.stderr:
|
const result = await global.activeSandbox.runCommand({
|
||||||
print(f"Errors: {result.stderr}")
|
cmd: cmdName,
|
||||||
`);
|
args
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Executed: ${cmd}`);
|
||||||
|
const stdout = await result.stdout();
|
||||||
|
const stderr = await result.stderr();
|
||||||
|
|
||||||
|
if (stdout) console.log(stdout);
|
||||||
|
if (stderr) console.log(`Errors: ${stderr}`);
|
||||||
|
|
||||||
results.commandsExecuted.push(cmd);
|
results.commandsExecuted.push(cmd);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`);
|
||||||
|
|||||||
+138
-176
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
import { Sandbox } from '@vercel/sandbox';
|
||||||
import type { SandboxState } from '@/types/sandbox';
|
import type { SandboxState } from '@/types/sandbox';
|
||||||
import { appConfig } from '@/config/app.config';
|
import { appConfig } from '@/config/app.config';
|
||||||
|
|
||||||
@@ -15,15 +15,15 @@ export async function POST() {
|
|||||||
let sandbox: any = null;
|
let sandbox: any = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[create-ai-sandbox] Creating base sandbox...');
|
console.log('[create-ai-sandbox] Creating Vercel sandbox...');
|
||||||
|
|
||||||
// Kill existing sandbox if any
|
// Kill existing sandbox if any
|
||||||
if (global.activeSandbox) {
|
if (global.activeSandbox) {
|
||||||
console.log('[create-ai-sandbox] Killing existing sandbox...');
|
console.log('[create-ai-sandbox] Stopping existing sandbox...');
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.kill();
|
await global.activeSandbox.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to close existing sandbox:', e);
|
console.error('Failed to stop existing sandbox:', e);
|
||||||
}
|
}
|
||||||
global.activeSandbox = null;
|
global.activeSandbox = null;
|
||||||
}
|
}
|
||||||
@@ -35,39 +35,79 @@ export async function POST() {
|
|||||||
global.existingFiles = new Set<string>();
|
global.existingFiles = new Set<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create base sandbox - we'll set up Vite ourselves for full control
|
// Create Vercel sandbox with flexible authentication
|
||||||
console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`);
|
console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`);
|
||||||
sandbox = await Sandbox.create({
|
|
||||||
apiKey: process.env.E2B_API_KEY,
|
|
||||||
timeoutMs: appConfig.e2b.timeoutMs
|
|
||||||
});
|
|
||||||
|
|
||||||
const sandboxId = (sandbox as any).sandboxId || Date.now().toString();
|
// Prepare sandbox configuration
|
||||||
const host = (sandbox as any).getHost(appConfig.e2b.vitePort);
|
const sandboxConfig: any = {
|
||||||
|
timeout: appConfig.vercelSandbox.timeoutMs,
|
||||||
|
runtime: appConfig.vercelSandbox.runtime,
|
||||||
|
ports: [appConfig.vercelSandbox.devPort]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add authentication parameters if using personal access token
|
||||||
|
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
|
||||||
|
console.log('[create-ai-sandbox] Using personal access token authentication');
|
||||||
|
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
|
||||||
|
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
|
||||||
|
sandboxConfig.token = process.env.VERCEL_TOKEN;
|
||||||
|
} else if (process.env.VERCEL_OIDC_TOKEN) {
|
||||||
|
console.log('[create-ai-sandbox] Using OIDC token authentication');
|
||||||
|
} else {
|
||||||
|
console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication');
|
||||||
|
}
|
||||||
|
|
||||||
|
sandbox = await Sandbox.create(sandboxConfig);
|
||||||
|
|
||||||
|
const sandboxId = sandbox.sandboxId;
|
||||||
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
|
console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`);
|
||||||
console.log(`[create-ai-sandbox] Sandbox host: ${host}`);
|
|
||||||
|
|
||||||
// Set up a basic Vite React app using Python to write files
|
// Set up a basic Vite React app
|
||||||
console.log('[create-ai-sandbox] Setting up Vite React app...');
|
console.log('[create-ai-sandbox] Setting up Vite React app...');
|
||||||
|
|
||||||
// Write all files in a single Python script to avoid multiple executions
|
// First, change to the working directory
|
||||||
const setupScript = `
|
await sandbox.runCommand('pwd');
|
||||||
import os
|
const workDir = appConfig.vercelSandbox.workingDirectory;
|
||||||
import json
|
|
||||||
|
|
||||||
print('Setting up React app with Vite and Tailwind...')
|
// Get the sandbox URL using the correct Vercel Sandbox API
|
||||||
|
const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort);
|
||||||
|
|
||||||
# Create directory structure
|
// Extract the hostname from the sandbox URL for Vite config
|
||||||
os.makedirs('/home/user/app/src', exist_ok=True)
|
const sandboxHostname = new URL(sandboxUrl).hostname;
|
||||||
|
console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`);
|
||||||
|
|
||||||
# Package.json
|
// Create the Vite config content with the proper hostname (using string concatenation)
|
||||||
package_json = {
|
const viteConfigContent = `import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// Vercel Sandbox compatible Vite configuration
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: ${appConfig.vercelSandbox.devPort},
|
||||||
|
strictPort: true,
|
||||||
|
hmr: true,
|
||||||
|
allowedHosts: [
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'` + sandboxHostname + `', // Allow the Vercel Sandbox domain
|
||||||
|
'.vercel.run', // Allow all Vercel sandbox domains
|
||||||
|
'.vercel-sandbox.dev' // Fallback pattern
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
|
||||||
|
// Create the project files (now we have the sandbox hostname)
|
||||||
|
const projectFiles = [
|
||||||
|
{
|
||||||
|
path: 'package.json',
|
||||||
|
content: Buffer.from(JSON.stringify({
|
||||||
"name": "sandbox-app",
|
"name": "sandbox-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@@ -82,34 +122,15 @@ package_json = {
|
|||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"autoprefixer": "^10.4.16"
|
"autoprefixer": "^10.4.16"
|
||||||
}
|
}
|
||||||
}
|
}, null, 2))
|
||||||
|
},
|
||||||
with open('/home/user/app/package.json', 'w') as f:
|
{
|
||||||
json.dump(package_json, f, indent=2)
|
path: 'vite.config.js',
|
||||||
print('✓ package.json')
|
content: Buffer.from(viteConfigContent)
|
||||||
|
},
|
||||||
# Vite config for E2B - with allowedHosts
|
{
|
||||||
vite_config = """import { defineConfig } from 'vite'
|
path: 'tailwind.config.js',
|
||||||
import react from '@vitejs/plugin-react'
|
content: Buffer.from(`/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
// E2B-compatible Vite configuration
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 5173,
|
|
||||||
strictPort: true,
|
|
||||||
hmr: false,
|
|
||||||
allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1']
|
|
||||||
}
|
|
||||||
})"""
|
|
||||||
|
|
||||||
with open('/home/user/app/vite.config.js', 'w') as f:
|
|
||||||
f.write(vite_config)
|
|
||||||
print('✓ vite.config.js')
|
|
||||||
|
|
||||||
# Tailwind config - standard without custom design tokens
|
|
||||||
tailwind_config = """/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
"./index.html",
|
||||||
@@ -119,26 +140,20 @@ export default {
|
|||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}"""
|
}`)
|
||||||
|
},
|
||||||
with open('/home/user/app/tailwind.config.js', 'w') as f:
|
{
|
||||||
f.write(tailwind_config)
|
path: 'postcss.config.js',
|
||||||
print('✓ tailwind.config.js')
|
content: Buffer.from(`export default {
|
||||||
|
|
||||||
# PostCSS config
|
|
||||||
postcss_config = """export default {
|
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}"""
|
}`)
|
||||||
|
},
|
||||||
with open('/home/user/app/postcss.config.js', 'w') as f:
|
{
|
||||||
f.write(postcss_config)
|
path: 'index.html',
|
||||||
print('✓ postcss.config.js')
|
content: Buffer.from(`<!DOCTYPE html>
|
||||||
|
|
||||||
# Index.html
|
|
||||||
index_html = """<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -149,14 +164,11 @@ index_html = """<!DOCTYPE html>
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>`)
|
||||||
|
},
|
||||||
with open('/home/user/app/index.html', 'w') as f:
|
{
|
||||||
f.write(index_html)
|
path: 'src/main.jsx',
|
||||||
print('✓ index.html')
|
content: Buffer.from(`import React from 'react'
|
||||||
|
|
||||||
# Main.jsx
|
|
||||||
main_jsx = """import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
@@ -165,19 +177,18 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)"""
|
)`)
|
||||||
|
},
|
||||||
with open('/home/user/app/src/main.jsx', 'w') as f:
|
{
|
||||||
f.write(main_jsx)
|
path: 'src/App.jsx',
|
||||||
print('✓ src/main.jsx')
|
content: Buffer.from(`function App() {
|
||||||
|
|
||||||
# App.jsx with explicit Tailwind test
|
|
||||||
app_jsx = """function App() {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
|
||||||
<div className="text-center max-w-2xl">
|
<div className="text-center max-w-2xl">
|
||||||
|
<h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
|
||||||
|
Sandbox Ready
|
||||||
|
</h1>
|
||||||
<p className="text-lg text-gray-400">
|
<p className="text-lg text-gray-400">
|
||||||
Sandbox Ready<br/>
|
|
||||||
Start building your React app with Vite and Tailwind CSS!
|
Start building your React app with Vite and Tailwind CSS!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,14 +196,11 @@ app_jsx = """function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App"""
|
export default App`)
|
||||||
|
},
|
||||||
with open('/home/user/app/src/App.jsx', 'w') as f:
|
{
|
||||||
f.write(app_jsx)
|
path: 'src/index.css',
|
||||||
print('✓ src/App.jsx')
|
content: Buffer.from(`@tailwind base;
|
||||||
|
|
||||||
# Index.css with explicit Tailwind directives
|
|
||||||
index_css = """@tailwind base;
|
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@@ -216,99 +224,53 @@ index_css = """@tailwind base;
|
|||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
background-color: rgb(17 24 39);
|
background-color: rgb(17 24 39);
|
||||||
}"""
|
}`)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
with open('/home/user/app/src/index.css', 'w') as f:
|
// Create directory structure first
|
||||||
f.write(index_css)
|
await sandbox.runCommand({
|
||||||
print('✓ src/index.css')
|
cmd: 'mkdir',
|
||||||
|
args: ['-p', 'src']
|
||||||
|
});
|
||||||
|
|
||||||
print('\\nAll files created successfully!')
|
// Write all files
|
||||||
`;
|
await sandbox.writeFiles(projectFiles);
|
||||||
|
console.log('[create-ai-sandbox] ✓ Project files created');
|
||||||
// Execute the setup script
|
|
||||||
await sandbox.runCode(setupScript);
|
|
||||||
|
|
||||||
// Install dependencies
|
// Install dependencies
|
||||||
console.log('[create-ai-sandbox] Installing dependencies...');
|
console.log('[create-ai-sandbox] Installing dependencies...');
|
||||||
await sandbox.runCode(`
|
const installResult = await sandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import sys
|
args: ['install', '--loglevel', 'info']
|
||||||
|
});
|
||||||
|
if (installResult.exitCode === 0) {
|
||||||
|
console.log('[create-ai-sandbox] ✓ Dependencies installed successfully');
|
||||||
|
} else {
|
||||||
|
console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...');
|
||||||
|
}
|
||||||
|
|
||||||
print('Installing npm packages...')
|
// Start Vite dev server in detached mode
|
||||||
result = subprocess.run(
|
|
||||||
['npm', 'install'],
|
|
||||||
cwd='/home/user/app',
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print('✓ Dependencies installed successfully')
|
|
||||||
else:
|
|
||||||
print(f'⚠ Warning: npm install had issues: {result.stderr}')
|
|
||||||
# Continue anyway as it might still work
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Start Vite dev server
|
|
||||||
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
console.log('[create-ai-sandbox] Starting Vite dev server...');
|
||||||
await sandbox.runCode(`
|
const viteProcess = await sandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import os
|
args: ['run', 'dev'],
|
||||||
import time
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
console.log('[create-ai-sandbox] ✓ Vite dev server started');
|
||||||
|
|
||||||
# Kill any existing Vite processes
|
|
||||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Start Vite dev server
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['FORCE_COLOR'] = '0'
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
|
||||||
['npm', 'run', 'dev'],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f'✓ Vite dev server started with PID: {process.pid}')
|
|
||||||
print('Waiting for server to be ready...')
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Wait for Vite to be fully ready
|
// Wait for Vite to be fully ready
|
||||||
await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay));
|
await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay));
|
||||||
|
|
||||||
// Force Tailwind CSS to rebuild by touching the CSS file
|
|
||||||
await sandbox.runCode(`
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Touch the CSS file to trigger rebuild
|
|
||||||
css_file = '/home/user/app/src/index.css'
|
|
||||||
if os.path.exists(css_file):
|
|
||||||
os.utime(css_file, None)
|
|
||||||
print('✓ Triggered CSS rebuild')
|
|
||||||
|
|
||||||
# Also ensure PostCSS processes it
|
|
||||||
time.sleep(2)
|
|
||||||
print('✓ Tailwind CSS should be loaded')
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Store sandbox globally
|
// Store sandbox globally
|
||||||
global.activeSandbox = sandbox;
|
global.activeSandbox = sandbox;
|
||||||
global.sandboxData = {
|
global.sandboxData = {
|
||||||
sandboxId,
|
sandboxId,
|
||||||
url: `https://${host}`
|
url: sandboxUrl,
|
||||||
|
viteProcess
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set extended timeout on the sandbox instance if method available
|
|
||||||
if (typeof sandbox.setTimeout === 'function') {
|
|
||||||
sandbox.setTimeout(appConfig.e2b.timeoutMs);
|
|
||||||
console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize sandbox state
|
// Initialize sandbox state
|
||||||
global.sandboxState = {
|
global.sandboxState = {
|
||||||
fileCache: {
|
fileCache: {
|
||||||
@@ -319,7 +281,7 @@ print('✓ Tailwind CSS should be loaded')
|
|||||||
sandbox,
|
sandbox,
|
||||||
sandboxData: {
|
sandboxData: {
|
||||||
sandboxId,
|
sandboxId,
|
||||||
url: `https://${host}`
|
url: sandboxUrl
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -333,13 +295,13 @@ print('✓ Tailwind CSS should be loaded')
|
|||||||
global.existingFiles.add('tailwind.config.js');
|
global.existingFiles.add('tailwind.config.js');
|
||||||
global.existingFiles.add('postcss.config.js');
|
global.existingFiles.add('postcss.config.js');
|
||||||
|
|
||||||
console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`);
|
console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
sandboxId,
|
sandboxId,
|
||||||
url: `https://${host}`,
|
url: sandboxUrl,
|
||||||
message: 'Sandbox created and Vite React app initialized'
|
message: 'Vercel sandbox created and Vite React app initialized'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -348,9 +310,9 @@ print('✓ Tailwind CSS should be loaded')
|
|||||||
// Clean up on error
|
// Clean up on error
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
try {
|
try {
|
||||||
await sandbox.kill();
|
await sandbox.stop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to close sandbox on error:', e);
|
console.error('Failed to stop sandbox on error:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-31
@@ -15,41 +15,37 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log('[create-zip] Creating project zip...');
|
console.log('[create-zip] Creating project zip...');
|
||||||
|
|
||||||
// Create zip file in sandbox
|
// Create zip file in sandbox using standard commands
|
||||||
const result = await global.activeSandbox.runCode(`
|
const zipResult = await global.activeSandbox.runCommand({
|
||||||
import zipfile
|
cmd: 'bash',
|
||||||
import os
|
args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`]
|
||||||
import json
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
if (zipResult.exitCode !== 0) {
|
||||||
|
const error = await zipResult.stderr();
|
||||||
|
throw new Error(`Failed to create zip: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
# Create zip file
|
const sizeResult = await global.activeSandbox.runCommand({
|
||||||
with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf:
|
cmd: 'bash',
|
||||||
for root, dirs, files in os.walk('.'):
|
args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`]
|
||||||
# Skip node_modules and .git
|
});
|
||||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']]
|
|
||||||
|
|
||||||
for file in files:
|
const fileSize = await sizeResult.stdout();
|
||||||
file_path = os.path.join(root, file)
|
console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`);
|
||||||
arcname = os.path.relpath(file_path, '.')
|
|
||||||
zipf.write(file_path, arcname)
|
|
||||||
|
|
||||||
# Get file size
|
|
||||||
file_size = os.path.getsize('/tmp/project.zip')
|
|
||||||
print(f" Created project.zip ({file_size} bytes)")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Read the zip file and convert to base64
|
// Read the zip file and convert to base64
|
||||||
const readResult = await global.activeSandbox.runCode(`
|
const readResult = await global.activeSandbox.runCommand({
|
||||||
import base64
|
cmd: 'base64',
|
||||||
|
args: ['/tmp/project.zip']
|
||||||
|
});
|
||||||
|
|
||||||
with open('/tmp/project.zip', 'rb') as f:
|
if (readResult.exitCode !== 0) {
|
||||||
content = f.read()
|
const error = await readResult.stderr();
|
||||||
encoded = base64.b64encode(content).decode('utf-8')
|
throw new Error(`Failed to read zip file: ${error}`);
|
||||||
print(encoded)
|
}
|
||||||
`);
|
|
||||||
|
|
||||||
const base64Content = readResult.logs.stdout.join('').trim();
|
const base64Content = (await readResult.stdout()).trim();
|
||||||
|
|
||||||
// Create a data URL for download
|
// Create a data URL for download
|
||||||
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
const dataUrl = `data:application/zip;base64,${base64Content}`;
|
||||||
@@ -57,15 +53,18 @@ with open('/tmp/project.zip', 'rb') as f:
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
dataUrl,
|
dataUrl,
|
||||||
fileName: 'e2b-project.zip',
|
fileName: 'vercel-sandbox-project.zip',
|
||||||
message: 'Zip file created successfully'
|
message: 'Zip file created successfully'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[create-zip] Error:', error);
|
console.error('[create-zip] Error:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: (error as Error).message
|
error: (error as Error).message
|
||||||
}, { status: 500 });
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,15 +64,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
|
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
|
||||||
if (builtins.includes(imp)) return false;
|
if (builtins.includes(imp)) return false;
|
||||||
|
|
||||||
// Extract package name (handle scoped packages and subpaths)
|
|
||||||
const parts = imp.split('/');
|
|
||||||
if (imp.startsWith('@')) {
|
|
||||||
// Scoped package like @vitejs/plugin-react
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
// Regular package, return just the first part
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract just the package names (without subpaths)
|
// Extract just the package names (without subpaths)
|
||||||
@@ -101,153 +93,89 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check which packages are already installed
|
// Check which packages are already installed
|
||||||
const checkResult = await global.activeSandbox.runCode(`
|
const installed: string[] = [];
|
||||||
import os
|
const missing: string[] = [];
|
||||||
import json
|
|
||||||
|
|
||||||
installed = []
|
for (const packageName of uniquePackages) {
|
||||||
missing = []
|
try {
|
||||||
|
const checkResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'test',
|
||||||
|
args: ['-d', `node_modules/${packageName}`]
|
||||||
|
});
|
||||||
|
|
||||||
packages = ${JSON.stringify(uniquePackages)}
|
if (checkResult.exitCode === 0) {
|
||||||
|
installed.push(packageName);
|
||||||
|
} else {
|
||||||
|
missing.push(packageName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If test command fails, assume package is missing
|
||||||
|
missing.push(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for package in packages:
|
console.log('[detect-and-install-packages] Package status:', { installed, missing });
|
||||||
# Handle scoped packages
|
|
||||||
if package.startswith('@'):
|
|
||||||
package_path = f"/home/user/app/node_modules/{package}"
|
|
||||||
else:
|
|
||||||
package_path = f"/home/user/app/node_modules/{package}"
|
|
||||||
|
|
||||||
if os.path.exists(package_path):
|
if (missing.length === 0) {
|
||||||
installed.append(package)
|
|
||||||
else:
|
|
||||||
missing.append(package)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'installed': installed,
|
|
||||||
'missing': missing
|
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(result))
|
|
||||||
`);
|
|
||||||
|
|
||||||
const status = JSON.parse(checkResult.logs.stdout.join(''));
|
|
||||||
console.log('[detect-and-install-packages] Package status:', status);
|
|
||||||
|
|
||||||
if (status.missing.length === 0) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
packagesInstalled: [],
|
packagesInstalled: [],
|
||||||
packagesAlreadyInstalled: status.installed,
|
packagesAlreadyInstalled: installed,
|
||||||
message: 'All packages already installed'
|
message: 'All packages already installed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install missing packages
|
// Install missing packages
|
||||||
console.log('[detect-and-install-packages] Installing packages:', status.missing);
|
console.log('[detect-and-install-packages] Installing packages:', missing);
|
||||||
|
|
||||||
const installResult = await global.activeSandbox.runCode(`
|
const installResult = await global.activeSandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import os
|
args: ['install', '--save', ...missing]
|
||||||
import json
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
const stdout = await installResult.stdout();
|
||||||
packages_to_install = ${JSON.stringify(status.missing)}
|
const stderr = await installResult.stderr();
|
||||||
|
|
||||||
# Join packages into a single install command
|
console.log('[detect-and-install-packages] Install stdout:', stdout);
|
||||||
packages_str = ' '.join(packages_to_install)
|
if (stderr) {
|
||||||
cmd = f'npm install {packages_str} --save'
|
console.log('[detect-and-install-packages] Install stderr:', stderr);
|
||||||
|
}
|
||||||
|
|
||||||
print(f"Running: {cmd}")
|
// Verify installation
|
||||||
|
const finalInstalled: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
|
||||||
# Run npm install with explicit save flag
|
for (const packageName of missing) {
|
||||||
result = subprocess.run(['npm', 'install', '--save'] + packages_to_install,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd='/home/user/app',
|
|
||||||
timeout=60)
|
|
||||||
|
|
||||||
print("stdout:", result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print("stderr:", result.stderr)
|
|
||||||
|
|
||||||
# Verify installation
|
|
||||||
installed = []
|
|
||||||
failed = []
|
|
||||||
|
|
||||||
for package in packages_to_install:
|
|
||||||
# Handle scoped packages correctly
|
|
||||||
if package.startswith('@'):
|
|
||||||
# For scoped packages like @heroicons/react
|
|
||||||
package_path = f"/home/user/app/node_modules/{package}"
|
|
||||||
else:
|
|
||||||
package_path = f"/home/user/app/node_modules/{package}"
|
|
||||||
|
|
||||||
if os.path.exists(package_path):
|
|
||||||
installed.append(package)
|
|
||||||
print(f"✓ Verified installation of {package}")
|
|
||||||
else:
|
|
||||||
# Check if it's a submodule of an installed package
|
|
||||||
base_package = package.split('/')[0]
|
|
||||||
if package.startswith('@'):
|
|
||||||
# For @scope/package, the base is @scope/package
|
|
||||||
base_package = '/'.join(package.split('/')[:2])
|
|
||||||
|
|
||||||
base_path = f"/home/user/app/node_modules/{base_package}"
|
|
||||||
if os.path.exists(base_path):
|
|
||||||
installed.append(package)
|
|
||||||
print(f"✓ Verified installation of {package} (via {base_package})")
|
|
||||||
else:
|
|
||||||
failed.append(package)
|
|
||||||
print(f"✗ Failed to verify installation of {package}")
|
|
||||||
|
|
||||||
result_data = {
|
|
||||||
'installed': installed,
|
|
||||||
'failed': failed,
|
|
||||||
'returncode': result.returncode
|
|
||||||
}
|
|
||||||
|
|
||||||
print("\\nResult:", json.dumps(result_data))
|
|
||||||
`, { timeout: 60000 });
|
|
||||||
|
|
||||||
// Parse the result more safely
|
|
||||||
let installStatus;
|
|
||||||
try {
|
try {
|
||||||
const stdout = installResult.logs.stdout.join('');
|
const verifyResult = await global.activeSandbox.runCommand({
|
||||||
const resultMatch = stdout.match(/Result:\s*({.*})/);
|
cmd: 'test',
|
||||||
if (resultMatch) {
|
args: ['-d', `node_modules/${packageName}`]
|
||||||
installStatus = JSON.parse(resultMatch[1]);
|
});
|
||||||
|
|
||||||
|
if (verifyResult.exitCode === 0) {
|
||||||
|
finalInstalled.push(packageName);
|
||||||
|
console.log(`✓ Verified installation of ${packageName}`);
|
||||||
} else {
|
} else {
|
||||||
// Fallback parsing
|
failed.push(packageName);
|
||||||
const lines = stdout.split('\n');
|
console.log(`✗ Failed to verify installation of ${packageName}`);
|
||||||
const resultLine = lines.find((line: string) => line.includes('Result:'));
|
|
||||||
if (resultLine) {
|
|
||||||
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
|
|
||||||
} else {
|
|
||||||
throw new Error('Could not find Result in output');
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failed.push(packageName);
|
||||||
|
console.log(`✗ Error verifying ${packageName}:`, error);
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
|
||||||
console.error('[detect-and-install-packages] Failed to parse install result:', parseError);
|
|
||||||
console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join(''));
|
|
||||||
// Fallback to assuming all packages were installed
|
|
||||||
installStatus = {
|
|
||||||
installed: status.missing,
|
|
||||||
failed: [],
|
|
||||||
returncode: 0
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installStatus.failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
|
console.error('[detect-and-install-packages] Failed to install:', failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
packagesInstalled: installStatus.installed,
|
packagesInstalled: finalInstalled,
|
||||||
packagesFailed: installStatus.failed,
|
packagesFailed: failed,
|
||||||
packagesAlreadyInstalled: status.installed,
|
packagesAlreadyInstalled: installed,
|
||||||
message: `Installed ${installStatus.installed.length} packages`,
|
message: `Installed ${finalInstalled.length} packages`,
|
||||||
logs: installResult.logs.stdout.join('\n')
|
logs: stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { FileManifest } from '@/types/file-manifest';
|
|||||||
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
|
||||||
import { appConfig } from '@/config/app.config';
|
import { appConfig } from '@/config/app.config';
|
||||||
|
|
||||||
|
// Force dynamic route to enable streaming
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const groq = createGroq({
|
const groq = createGroq({
|
||||||
apiKey: process.env.GROQ_API_KEY,
|
apiKey: process.env.GROQ_API_KEY,
|
||||||
});
|
});
|
||||||
@@ -1156,9 +1159,21 @@ CRITICAL: When files are provided in the context:
|
|||||||
const isGoogle = model.startsWith('google/');
|
const isGoogle = model.startsWith('google/');
|
||||||
const isOpenAI = model.startsWith('openai/gpt-5');
|
const isOpenAI = model.startsWith('openai/gpt-5');
|
||||||
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
|
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
|
||||||
const actualModel = isAnthropic ? model.replace('anthropic/', '') :
|
|
||||||
(model === 'openai/gpt-5') ? 'gpt-5' :
|
// Fix model name transformation for different providers
|
||||||
(isGoogle ? model.replace('google/', '') : model);
|
let actualModel: string;
|
||||||
|
if (isAnthropic) {
|
||||||
|
actualModel = model.replace('anthropic/', '');
|
||||||
|
} else if (model === 'openai/gpt-5') {
|
||||||
|
actualModel = 'gpt-5';
|
||||||
|
} else if (isGoogle) {
|
||||||
|
// Google uses specific model names - convert our naming to theirs
|
||||||
|
actualModel = model.replace('google/', '');
|
||||||
|
} else {
|
||||||
|
actualModel = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`);
|
||||||
|
|
||||||
// Make streaming API call with appropriate provider
|
// Make streaming API call with appropriate provider
|
||||||
const streamOptions: any = {
|
const streamOptions: any = {
|
||||||
@@ -1243,7 +1258,28 @@ It's better to have 3 complete files than 10 incomplete files.`
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await streamText(streamOptions);
|
let result;
|
||||||
|
try {
|
||||||
|
result = await streamText(streamOptions);
|
||||||
|
} catch (streamError) {
|
||||||
|
console.error('[generate-ai-code-stream] Error calling streamText:', streamError);
|
||||||
|
|
||||||
|
// Send specific error for debugging
|
||||||
|
await sendProgress({
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'Groq'} streaming: ${(streamError as Error).message}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this is a Google model error, provide helpful info
|
||||||
|
if (isGoogle) {
|
||||||
|
await sendProgress({
|
||||||
|
type: 'info',
|
||||||
|
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw streamError;
|
||||||
|
}
|
||||||
|
|
||||||
// Stream the response and parse in real-time
|
// Stream the response and parse in real-time
|
||||||
let generatedCode = '';
|
let generatedCode = '';
|
||||||
@@ -1715,12 +1751,18 @@ Provide the complete file content without any truncation. Include all necessary
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Return the stream
|
// Return the stream with proper headers for streaming support
|
||||||
return new Response(stream.readable, {
|
return new Response(stream.readable, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
|
'Transfer-Encoding': 'chunked',
|
||||||
|
'Content-Encoding': 'none', // Prevent compression that can break streaming
|
||||||
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,58 +18,81 @@ export async function GET() {
|
|||||||
|
|
||||||
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
console.log('[get-sandbox-files] Fetching and analyzing file structure...');
|
||||||
|
|
||||||
// Get all React/JS/CSS files
|
// Get list of all relevant files
|
||||||
const result = await global.activeSandbox.runCode(`
|
const findResult = await global.activeSandbox.runCommand({
|
||||||
import os
|
cmd: 'find',
|
||||||
import json
|
args: [
|
||||||
|
'.',
|
||||||
|
'-name', 'node_modules', '-prune', '-o',
|
||||||
|
'-name', '.git', '-prune', '-o',
|
||||||
|
'-name', 'dist', '-prune', '-o',
|
||||||
|
'-name', 'build', '-prune', '-o',
|
||||||
|
'-type', 'f',
|
||||||
|
'(',
|
||||||
|
'-name', '*.jsx',
|
||||||
|
'-o', '-name', '*.js',
|
||||||
|
'-o', '-name', '*.tsx',
|
||||||
|
'-o', '-name', '*.ts',
|
||||||
|
'-o', '-name', '*.css',
|
||||||
|
'-o', '-name', '*.json',
|
||||||
|
')',
|
||||||
|
'-print'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']):
|
if (findResult.exitCode !== 0) {
|
||||||
files_content = {}
|
throw new Error('Failed to list files');
|
||||||
|
}
|
||||||
|
|
||||||
for root, dirs, files in os.walk(directory):
|
const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
||||||
# Skip node_modules and other unwanted directories
|
console.log('[get-sandbox-files] Found', fileList.length, 'files');
|
||||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']]
|
|
||||||
|
|
||||||
for file in files:
|
// Read content of each file (limit to reasonable sizes)
|
||||||
if any(file.endswith(ext) for ext in extensions):
|
const filesContent: Record<string, string> = {};
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
relative_path = os.path.relpath(file_path, '/home/user/app')
|
|
||||||
|
|
||||||
try:
|
for (const filePath of fileList) {
|
||||||
with open(file_path, 'r') as f:
|
try {
|
||||||
content = f.read()
|
// Check file size first
|
||||||
# Only include files under 10KB to avoid huge responses
|
const statResult = await global.activeSandbox.runCommand({
|
||||||
if len(content) < 10000:
|
cmd: 'stat',
|
||||||
files_content[relative_path] = content
|
args: ['-f', '%z', filePath]
|
||||||
except:
|
});
|
||||||
pass
|
|
||||||
|
|
||||||
return files_content
|
if (statResult.exitCode === 0) {
|
||||||
|
const fileSize = parseInt(await statResult.stdout());
|
||||||
|
|
||||||
# Get the files
|
// Only read files smaller than 10KB
|
||||||
files = get_files_content()
|
if (fileSize < 10000) {
|
||||||
|
const catResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'cat',
|
||||||
|
args: [filePath]
|
||||||
|
});
|
||||||
|
|
||||||
# Also get the directory structure
|
if (catResult.exitCode === 0) {
|
||||||
structure = []
|
const content = await catResult.stdout();
|
||||||
for root, dirs, files in os.walk('/home/user/app'):
|
// Remove leading './' from path
|
||||||
level = root.replace('/home/user/app', '').count(os.sep)
|
const relativePath = filePath.replace(/^\.\//, '');
|
||||||
indent = ' ' * 2 * level
|
filesContent[relativePath] = content;
|
||||||
structure.append(f"{indent}{os.path.basename(root)}/")
|
}
|
||||||
sub_indent = ' ' * 2 * (level + 1)
|
}
|
||||||
for file in files:
|
}
|
||||||
if not any(skip in root for skip in ['node_modules', '.git', 'dist', 'build']):
|
} catch (error) {
|
||||||
structure.append(f"{sub_indent}{file}")
|
// Skip files that can't be read
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = {
|
// Get directory structure
|
||||||
'files': files,
|
const treeResult = await global.activeSandbox.runCommand({
|
||||||
'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines
|
cmd: 'find',
|
||||||
}
|
args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*']
|
||||||
|
});
|
||||||
|
|
||||||
print(json.dumps(result))
|
let structure = '';
|
||||||
`);
|
if (treeResult.exitCode === 0) {
|
||||||
|
const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim());
|
||||||
const output = result.logs.stdout.join('');
|
structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines
|
||||||
const parsedResult = JSON.parse(output);
|
}
|
||||||
|
|
||||||
// Build enhanced file manifest
|
// Build enhanced file manifest
|
||||||
const fileManifest: FileManifest = {
|
const fileManifest: FileManifest = {
|
||||||
@@ -82,12 +105,12 @@ print(json.dumps(result))
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Process each file
|
// Process each file
|
||||||
for (const [relativePath, content] of Object.entries(parsedResult.files)) {
|
for (const [relativePath, content] of Object.entries(filesContent)) {
|
||||||
const fullPath = `/home/user/app/${relativePath}`;
|
const fullPath = `/${relativePath}`;
|
||||||
|
|
||||||
// Create base file info
|
// Create base file info
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: FileInfo = {
|
||||||
content: content as string,
|
content: content,
|
||||||
type: 'utility',
|
type: 'utility',
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
relativePath,
|
relativePath,
|
||||||
@@ -96,7 +119,7 @@ print(json.dumps(result))
|
|||||||
|
|
||||||
// Parse JavaScript/JSX files
|
// Parse JavaScript/JSX files
|
||||||
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
if (relativePath.match(/\.(jsx?|tsx?)$/)) {
|
||||||
const parseResult = parseJavaScriptFile(content as string, fullPath);
|
const parseResult = parseJavaScriptFile(content, fullPath);
|
||||||
Object.assign(fileInfo, parseResult);
|
Object.assign(fileInfo, parseResult);
|
||||||
|
|
||||||
// Identify entry point
|
// Identify entry point
|
||||||
@@ -132,9 +155,9 @@ print(json.dumps(result))
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
files: parsedResult.files,
|
files: filesContent,
|
||||||
structure: parsedResult.structure,
|
structure,
|
||||||
fileCount: Object.keys(parsedResult.files).length,
|
fileCount: Object.keys(filesContent).length,
|
||||||
manifest: fileManifest,
|
manifest: fileManifest,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+117
-215
@@ -1,5 +1,4 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var activeSandbox: any;
|
var activeSandbox: any;
|
||||||
@@ -36,23 +35,8 @@ export async function POST(request: NextRequest) {
|
|||||||
console.log(`[install-packages] Cleaned:`, validPackages);
|
console.log(`[install-packages] Cleaned:`, validPackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get sandbox - either from global or reconnect
|
// Get active sandbox
|
||||||
let sandbox = global.activeSandbox;
|
const sandbox = global.activeSandbox;
|
||||||
|
|
||||||
if (!sandbox && sandboxId) {
|
|
||||||
console.log(`[install-packages] Reconnecting to sandbox ${sandboxId}...`);
|
|
||||||
try {
|
|
||||||
sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
|
|
||||||
global.activeSandbox = sandbox;
|
|
||||||
console.log(`[install-packages] Successfully reconnected to sandbox ${sandboxId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[install-packages] Failed to reconnect to sandbox:`, error);
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: `Failed to reconnect to sandbox: ${(error as Error).message}`
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sandbox) {
|
if (!sandbox) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -61,7 +45,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[install-packages] Installing packages:', packages);
|
console.log('[install-packages] Installing packages:', validPackages);
|
||||||
|
|
||||||
// Create a response stream for real-time updates
|
// Create a response stream for real-time updates
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
@@ -83,23 +67,20 @@ export async function POST(request: NextRequest) {
|
|||||||
packages: validPackages
|
packages: validPackages
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kill any existing Vite process first
|
// Stop any existing development server first
|
||||||
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
await sendProgress({ type: 'status', message: 'Stopping development server...' });
|
||||||
|
|
||||||
await sandboxInstance.runCode(`
|
try {
|
||||||
import subprocess
|
// Try to kill any running dev server processes
|
||||||
import os
|
await sandboxInstance.runCommand({
|
||||||
import signal
|
cmd: 'pkill',
|
||||||
|
args: ['-f', 'vite']
|
||||||
# Try to kill any existing Vite process
|
});
|
||||||
try:
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit
|
||||||
with open('/tmp/vite-process.pid', 'r') as f:
|
} catch (error) {
|
||||||
pid = int(f.read().strip())
|
// It's OK if no process is found
|
||||||
os.kill(pid, signal.SIGTERM)
|
console.log('[install-packages] No existing dev server found');
|
||||||
print("Stopped existing Vite process")
|
}
|
||||||
except:
|
|
||||||
print("No existing Vite process found")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Check which packages are already installed
|
// Check which packages are already installed
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
@@ -107,70 +88,51 @@ except:
|
|||||||
message: 'Checking installed packages...'
|
message: 'Checking installed packages...'
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkResult = await sandboxInstance.runCode(`
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
|
||||||
|
|
||||||
# Read package.json to check installed packages
|
|
||||||
try:
|
|
||||||
with open('package.json', 'r') as f:
|
|
||||||
package_json = json.load(f)
|
|
||||||
|
|
||||||
dependencies = package_json.get('dependencies', {})
|
|
||||||
dev_dependencies = package_json.get('devDependencies', {})
|
|
||||||
all_deps = {**dependencies, **dev_dependencies}
|
|
||||||
|
|
||||||
# Check which packages need to be installed
|
|
||||||
packages_to_check = ${JSON.stringify(validPackages)}
|
|
||||||
already_installed = []
|
|
||||||
need_install = []
|
|
||||||
|
|
||||||
for pkg in packages_to_check:
|
|
||||||
# Handle scoped packages
|
|
||||||
if pkg.startswith('@'):
|
|
||||||
pkg_name = pkg
|
|
||||||
else:
|
|
||||||
# Extract package name without version
|
|
||||||
pkg_name = pkg.split('@')[0]
|
|
||||||
|
|
||||||
if pkg_name in all_deps:
|
|
||||||
already_installed.append(pkg_name)
|
|
||||||
else:
|
|
||||||
need_install.append(pkg)
|
|
||||||
|
|
||||||
print(f"Already installed: {already_installed}")
|
|
||||||
print(f"Need to install: {need_install}")
|
|
||||||
print(f"NEED_INSTALL:{json.dumps(need_install)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error checking packages: {e}")
|
|
||||||
print(f"NEED_INSTALL:{json.dumps(packages_to_check)}")
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Parse packages that need installation
|
|
||||||
let packagesToInstall = validPackages;
|
let packagesToInstall = validPackages;
|
||||||
|
|
||||||
// Check if checkResult has the expected structure
|
|
||||||
if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) {
|
|
||||||
const outputLines = checkResult.results[0].text.split('\n');
|
|
||||||
for (const line of outputLines) {
|
|
||||||
if (line.startsWith('NEED_INSTALL:')) {
|
|
||||||
try {
|
try {
|
||||||
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
|
// Read package.json to check existing dependencies
|
||||||
} catch (e) {
|
const catResult = await sandboxInstance.runCommand({
|
||||||
console.error('Failed to parse packages to install:', e);
|
cmd: 'cat',
|
||||||
}
|
args: ['package.json']
|
||||||
}
|
});
|
||||||
}
|
if (catResult.exitCode === 0) {
|
||||||
|
const packageJsonContent = await catResult.stdout();
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
|
||||||
|
const dependencies = packageJson.dependencies || {};
|
||||||
|
const devDependencies = packageJson.devDependencies || {};
|
||||||
|
const allDeps = { ...dependencies, ...devDependencies };
|
||||||
|
|
||||||
|
const alreadyInstalled = [];
|
||||||
|
const needInstall = [];
|
||||||
|
|
||||||
|
for (const pkg of validPackages) {
|
||||||
|
// Handle scoped packages
|
||||||
|
const pkgName = pkg.startsWith('@') ? pkg : pkg.split('@')[0];
|
||||||
|
|
||||||
|
if (allDeps[pkgName]) {
|
||||||
|
alreadyInstalled.push(pkgName);
|
||||||
} else {
|
} else {
|
||||||
console.error('[install-packages] Invalid checkResult structure:', checkResult);
|
needInstall.push(pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagesToInstall = needInstall;
|
||||||
|
|
||||||
|
if (alreadyInstalled.length > 0) {
|
||||||
|
await sendProgress({
|
||||||
|
type: 'info',
|
||||||
|
message: `Already installed: ${alreadyInstalled.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[install-packages] Error checking existing packages:', error);
|
||||||
// If we can't check, just try to install all packages
|
// If we can't check, just try to install all packages
|
||||||
packagesToInstall = validPackages;
|
packagesToInstall = validPackages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (packagesToInstall.length === 0) {
|
if (packagesToInstall.length === 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -178,164 +140,104 @@ except Exception as e:
|
|||||||
installedPackages: [],
|
installedPackages: [],
|
||||||
alreadyInstalled: validPackages
|
alreadyInstalled: validPackages
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restart dev server
|
||||||
|
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
||||||
|
|
||||||
|
const devServerProcess = await sandboxInstance.runCommand({
|
||||||
|
cmd: 'npm',
|
||||||
|
args: ['run', 'dev'],
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendProgress({
|
||||||
|
type: 'complete',
|
||||||
|
message: 'Dev server restarted!',
|
||||||
|
installedPackages: []
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install only packages that aren't already installed
|
// Install only packages that aren't already installed
|
||||||
const packageList = packagesToInstall.join(' ');
|
|
||||||
// Only send the npm install command message if we're actually installing new packages
|
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const installResult = await sandboxInstance.runCode(`
|
// Run npm install
|
||||||
import subprocess
|
const installArgs = ['install', '--legacy-peer-deps', ...packagesToInstall];
|
||||||
import os
|
const installResult = await sandboxInstance.runCommand({
|
||||||
|
cmd: 'npm',
|
||||||
os.chdir('/home/user/app')
|
args: installArgs
|
||||||
|
|
||||||
# Run npm install with output capture
|
|
||||||
packages_to_install = ${JSON.stringify(packagesToInstall)}
|
|
||||||
cmd_args = ['npm', 'install', '--legacy-peer-deps'] + packages_to_install
|
|
||||||
|
|
||||||
print(f"Running command: {' '.join(cmd_args)}")
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
|
||||||
cmd_args,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stream output
|
|
||||||
while True:
|
|
||||||
output = process.stdout.readline()
|
|
||||||
if output == '' and process.poll() is not None:
|
|
||||||
break
|
|
||||||
if output:
|
|
||||||
print(output.strip())
|
|
||||||
|
|
||||||
# Get the return code
|
|
||||||
rc = process.poll()
|
|
||||||
|
|
||||||
# Capture any stderr
|
|
||||||
stderr = process.stderr.read()
|
|
||||||
if stderr:
|
|
||||||
print("STDERR:", stderr)
|
|
||||||
if 'ERESOLVE' in stderr:
|
|
||||||
print("ERESOLVE_ERROR: Dependency conflict detected - using --legacy-peer-deps flag")
|
|
||||||
|
|
||||||
print(f"\\nInstallation completed with code: {rc}")
|
|
||||||
|
|
||||||
# Verify packages were installed
|
|
||||||
import json
|
|
||||||
with open('/home/user/app/package.json', 'r') as f:
|
|
||||||
package_json = json.load(f)
|
|
||||||
|
|
||||||
installed = []
|
|
||||||
for pkg in ${JSON.stringify(packagesToInstall)}:
|
|
||||||
if pkg in package_json.get('dependencies', {}):
|
|
||||||
installed.append(pkg)
|
|
||||||
print(f"✓ Verified {pkg}")
|
|
||||||
else:
|
|
||||||
print(f"✗ Package {pkg} not found in dependencies")
|
|
||||||
|
|
||||||
print(f"\\nVerified installed packages: {installed}")
|
|
||||||
`, { timeout: 60000 }); // 60 second timeout for npm install
|
|
||||||
|
|
||||||
// Send npm output
|
|
||||||
const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || '';
|
|
||||||
const npmOutputLines = output.split('\n').filter((line: string) => line.trim());
|
|
||||||
for (const line of npmOutputLines) {
|
|
||||||
if (line.includes('STDERR:')) {
|
|
||||||
const errorMsg = line.replace('STDERR:', '').trim();
|
|
||||||
if (errorMsg && errorMsg !== 'undefined') {
|
|
||||||
await sendProgress({ type: 'error', message: errorMsg });
|
|
||||||
}
|
|
||||||
} else if (line.includes('ERESOLVE_ERROR:')) {
|
|
||||||
const msg = line.replace('ERESOLVE_ERROR:', '').trim();
|
|
||||||
await sendProgress({
|
|
||||||
type: 'warning',
|
|
||||||
message: `Dependency conflict resolved with --legacy-peer-deps: ${msg}`
|
|
||||||
});
|
});
|
||||||
} else if (line.includes('npm WARN')) {
|
|
||||||
|
// Get install output
|
||||||
|
const stdout = await installResult.stdout();
|
||||||
|
const stderr = await installResult.stderr();
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
const lines = stdout.split('\n').filter(line => line.trim());
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('npm WARN')) {
|
||||||
await sendProgress({ type: 'warning', message: line });
|
await sendProgress({ type: 'warning', message: line });
|
||||||
} else if (line.trim() && !line.includes('undefined')) {
|
} else if (line.trim()) {
|
||||||
await sendProgress({ type: 'output', message: line });
|
await sendProgress({ type: 'output', message: line });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if installation was successful
|
|
||||||
const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/);
|
|
||||||
let installedPackages: string[] = [];
|
|
||||||
|
|
||||||
if (installedMatch && installedMatch[1]) {
|
|
||||||
installedPackages = installedMatch[1]
|
|
||||||
.split(',')
|
|
||||||
.map((p: string) => p.trim().replace(/'/g, ''))
|
|
||||||
.filter((p: string) => p.length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installedPackages.length > 0) {
|
if (stderr) {
|
||||||
|
const errorLines = stderr.split('\n').filter(line => line.trim());
|
||||||
|
for (const line of errorLines) {
|
||||||
|
if (line.includes('ERESOLVE')) {
|
||||||
|
await sendProgress({
|
||||||
|
type: 'warning',
|
||||||
|
message: `Dependency conflict resolved with --legacy-peer-deps: ${line}`
|
||||||
|
});
|
||||||
|
} else if (line.trim()) {
|
||||||
|
await sendProgress({ type: 'error', message: line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installResult.exitCode === 0) {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: `Successfully installed: ${installedPackages.join(', ')}`,
|
message: `Successfully installed: ${packagesToInstall.join(', ')}`,
|
||||||
installedPackages
|
installedPackages: packagesToInstall
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: 'Failed to verify package installation'
|
message: 'Package installation failed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart Vite dev server
|
// Restart development server
|
||||||
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
await sendProgress({ type: 'status', message: 'Restarting development server...' });
|
||||||
|
|
||||||
await sandboxInstance.runCode(`
|
try {
|
||||||
import subprocess
|
const devServerProcess = await sandboxInstance.runCommand({
|
||||||
import os
|
cmd: 'npm',
|
||||||
import time
|
args: ['run', 'dev'],
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
// Wait a bit for the server to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
# Kill any existing Vite processes
|
|
||||||
subprocess.run(['pkill', '-f', 'vite'], capture_output=True)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Start Vite dev server
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['FORCE_COLOR'] = '0'
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
|
||||||
['npm', 'run', 'dev'],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f'✓ Vite dev server restarted with PID: {process.pid}')
|
|
||||||
|
|
||||||
# Store process info for later
|
|
||||||
with open('/tmp/vite-process.pid', 'w') as f:
|
|
||||||
f.write(str(process.pid))
|
|
||||||
|
|
||||||
# Wait a bit for Vite to start up
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
# Touch files to trigger Vite reload
|
|
||||||
subprocess.run(['touch', '/home/user/app/package.json'])
|
|
||||||
subprocess.run(['touch', '/home/user/app/vite.config.js'])
|
|
||||||
|
|
||||||
print("Vite restarted and should now recognize all packages")
|
|
||||||
`);
|
|
||||||
|
|
||||||
await sendProgress({
|
await sendProgress({
|
||||||
type: 'complete',
|
type: 'complete',
|
||||||
message: 'Package installation complete and dev server restarted!',
|
message: 'Package installation complete and dev server restarted!',
|
||||||
installedPackages
|
installedPackages: packagesToInstall
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await sendProgress({
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to restart dev server: ${(error as Error).message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = (error as Error).message;
|
const errorMessage = (error as Error).message;
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ declare global {
|
|||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
console.log('[kill-sandbox] Killing active sandbox...');
|
console.log('[kill-sandbox] Stopping active sandbox...');
|
||||||
|
|
||||||
let sandboxKilled = false;
|
let sandboxKilled = false;
|
||||||
|
|
||||||
// Kill existing sandbox if any
|
// Stop existing sandbox if any
|
||||||
if (global.activeSandbox) {
|
if (global.activeSandbox) {
|
||||||
try {
|
try {
|
||||||
await global.activeSandbox.close();
|
await global.activeSandbox.stop();
|
||||||
sandboxKilled = true;
|
sandboxKilled = true;
|
||||||
console.log('[kill-sandbox] Sandbox closed successfully');
|
console.log('[kill-sandbox] Sandbox stopped successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[kill-sandbox] Failed to close sandbox:', e);
|
console.error('[kill-sandbox] Failed to stop sandbox:', e);
|
||||||
}
|
}
|
||||||
global.activeSandbox = null;
|
global.activeSandbox = null;
|
||||||
global.sandboxData = null;
|
global.sandboxData = null;
|
||||||
|
|||||||
@@ -15,97 +15,100 @@ export async function GET() {
|
|||||||
|
|
||||||
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
console.log('[monitor-vite-logs] Checking Vite process logs...');
|
||||||
|
|
||||||
// Check both the error file and recent logs
|
const errors: any[] = [];
|
||||||
const result = await global.activeSandbox.runCode(`
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
|
|
||||||
errors = []
|
// Check if there's an error file from previous runs
|
||||||
|
try {
|
||||||
|
const catResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'cat',
|
||||||
|
args: ['/tmp/vite-errors.json']
|
||||||
|
});
|
||||||
|
|
||||||
# First check the error file
|
if (catResult.exitCode === 0) {
|
||||||
try:
|
const errorFileContent = await catResult.stdout();
|
||||||
with open('/tmp/vite-errors.json', 'r') as f:
|
const data = JSON.parse(errorFileContent);
|
||||||
data = json.load(f)
|
errors.push(...(data.errors || []));
|
||||||
errors.extend(data.get('errors', []))
|
}
|
||||||
except:
|
} catch (error) {
|
||||||
pass
|
// No error file exists, that's OK
|
||||||
|
|
||||||
# Also check if we can get recent Vite logs
|
|
||||||
try:
|
|
||||||
# Try to get the Vite process PID
|
|
||||||
with open('/tmp/vite-process.pid', 'r') as f:
|
|
||||||
pid = int(f.read().strip())
|
|
||||||
|
|
||||||
# Check if process is still running and get its logs
|
|
||||||
# This is a bit hacky but works for our use case
|
|
||||||
result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True)
|
|
||||||
if result.returncode == 0:
|
|
||||||
# Process is running, try to check for errors in output
|
|
||||||
# Note: We can't easily get stdout/stderr from a running process
|
|
||||||
# but we can check if there are new errors
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Also scan the current console output for any HMR errors
|
|
||||||
# This won't catch everything but helps with recent errors
|
|
||||||
try:
|
|
||||||
# Check if there's a log file we can read
|
|
||||||
import os
|
|
||||||
log_files = []
|
|
||||||
for root, dirs, files in os.walk('/tmp'):
|
|
||||||
for file in files:
|
|
||||||
if 'vite' in file.lower() and file.endswith('.log'):
|
|
||||||
log_files.append(os.path.join(root, file))
|
|
||||||
|
|
||||||
for log_file in log_files[:5]: # Check up to 5 log files
|
|
||||||
try:
|
|
||||||
with open(log_file, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
# Look for import errors
|
|
||||||
import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content)
|
|
||||||
for pkg in import_errors:
|
|
||||||
if not pkg.startswith('.'):
|
|
||||||
# Extract base package name
|
|
||||||
if pkg.startswith('@'):
|
|
||||||
parts = pkg.split('/')
|
|
||||||
final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg
|
|
||||||
else:
|
|
||||||
final_pkg = pkg.split('/')[0]
|
|
||||||
|
|
||||||
error_obj = {
|
|
||||||
"type": "npm-missing",
|
|
||||||
"package": final_pkg,
|
|
||||||
"message": f"Failed to resolve import \\"{pkg}\\"",
|
|
||||||
"file": "Unknown"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Avoid duplicates
|
// Look for any Vite-related log files that might contain errors
|
||||||
if not any(e['package'] == error_obj['package'] for e in errors):
|
try {
|
||||||
errors.append(error_obj)
|
const findResult = await global.activeSandbox.runCommand({
|
||||||
except:
|
cmd: 'find',
|
||||||
pass
|
args: ['/tmp', '-name', '*vite*', '-type', 'f']
|
||||||
except Exception as e:
|
});
|
||||||
print(f"Error scanning logs: {e}")
|
|
||||||
|
|
||||||
# Deduplicate errors
|
if (findResult.exitCode === 0) {
|
||||||
unique_errors = []
|
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
||||||
seen_packages = set()
|
|
||||||
for error in errors:
|
|
||||||
if error.get('package') and error['package'] not in seen_packages:
|
|
||||||
seen_packages.add(error['package'])
|
|
||||||
unique_errors.append(error)
|
|
||||||
|
|
||||||
print(json.dumps({"errors": unique_errors}))
|
for (const logFile of logFiles.slice(0, 3)) {
|
||||||
`, { timeout: 5000 });
|
try {
|
||||||
|
const grepResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'grep',
|
||||||
|
args: ['-i', 'failed to resolve import', logFile]
|
||||||
|
});
|
||||||
|
|
||||||
const data = JSON.parse(result.output || '{"errors": []}');
|
if (grepResult.exitCode === 0) {
|
||||||
|
const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
for (const line of errorLines) {
|
||||||
|
// Extract package name from error line
|
||||||
|
const importMatch = line.match(/"([^"]+)"/);
|
||||||
|
if (importMatch) {
|
||||||
|
const importPath = importMatch[1];
|
||||||
|
|
||||||
|
// Skip relative imports
|
||||||
|
if (!importPath.startsWith('.')) {
|
||||||
|
// Extract base package name
|
||||||
|
let packageName;
|
||||||
|
if (importPath.startsWith('@')) {
|
||||||
|
const parts = importPath.split('/');
|
||||||
|
packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath;
|
||||||
|
} else {
|
||||||
|
packageName = importPath.split('/')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorObj = {
|
||||||
|
type: "npm-missing",
|
||||||
|
package: packageName,
|
||||||
|
message: `Failed to resolve import "${importPath}"`,
|
||||||
|
file: "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid duplicates
|
||||||
|
if (!errors.some(e => e.package === errorObj.package)) {
|
||||||
|
errors.push(errorObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip if grep fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// No log files found, that's OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate errors by package name
|
||||||
|
const uniqueErrors: any[] = [];
|
||||||
|
const seenPackages = new Set<string>();
|
||||||
|
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error.package && !seenPackages.has(error.package)) {
|
||||||
|
seenPackages.add(error.package);
|
||||||
|
uniqueErrors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
hasErrors: data.errors.length > 0,
|
hasErrors: uniqueErrors.length > 0,
|
||||||
errors: data.errors
|
errors: uniqueErrors
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+30
-100
@@ -15,115 +15,45 @@ export async function POST() {
|
|||||||
|
|
||||||
console.log('[restart-vite] Forcing Vite restart...');
|
console.log('[restart-vite] Forcing Vite restart...');
|
||||||
|
|
||||||
// Kill existing Vite process and restart
|
// Kill existing Vite processes
|
||||||
const result = await global.activeSandbox.runCode(`
|
try {
|
||||||
import subprocess
|
await global.activeSandbox.runCommand({
|
||||||
import os
|
cmd: 'pkill',
|
||||||
import signal
|
args: ['-f', 'vite']
|
||||||
import time
|
});
|
||||||
import threading
|
console.log('[restart-vite] Killed existing Vite processes');
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Kill existing Vite process
|
// Wait a moment for processes to terminate
|
||||||
try:
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
with open('/tmp/vite-process.pid', 'r') as f:
|
} catch (error) {
|
||||||
pid = int(f.read().strip())
|
console.log('[restart-vite] No existing Vite processes found');
|
||||||
os.kill(pid, signal.SIGTERM)
|
|
||||||
print("Killed existing Vite process")
|
|
||||||
time.sleep(1)
|
|
||||||
except:
|
|
||||||
print("No existing Vite process found")
|
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
|
||||||
|
|
||||||
# Clear error file
|
|
||||||
error_file = '/tmp/vite-errors.json'
|
|
||||||
with open(error_file, 'w') as f:
|
|
||||||
json.dump({"errors": [], "lastChecked": time.time()}, f)
|
|
||||||
|
|
||||||
# Function to monitor Vite output for errors
|
|
||||||
def monitor_output(proc, error_file):
|
|
||||||
while True:
|
|
||||||
line = proc.stderr.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
|
|
||||||
sys.stdout.write(line) # Also print to console
|
|
||||||
|
|
||||||
# Check for import resolution errors
|
|
||||||
if "Failed to resolve import" in line:
|
|
||||||
try:
|
|
||||||
# Extract package name from error
|
|
||||||
import_match = line.find('"')
|
|
||||||
if import_match != -1:
|
|
||||||
end_match = line.find('"', import_match + 1)
|
|
||||||
if end_match != -1:
|
|
||||||
package_name = line[import_match + 1:end_match]
|
|
||||||
# Skip relative imports
|
|
||||||
if not package_name.startswith('.'):
|
|
||||||
with open(error_file, 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
# Handle scoped packages correctly
|
|
||||||
if package_name.startswith('@'):
|
|
||||||
# For @scope/package, keep the scope
|
|
||||||
pkg_parts = package_name.split('/')
|
|
||||||
if len(pkg_parts) >= 2:
|
|
||||||
final_package = '/'.join(pkg_parts[:2])
|
|
||||||
else:
|
|
||||||
final_package = package_name
|
|
||||||
else:
|
|
||||||
# For regular packages, just take the first part
|
|
||||||
final_package = package_name.split('/')[0]
|
|
||||||
|
|
||||||
error_obj = {
|
|
||||||
"type": "npm-missing",
|
|
||||||
"package": final_package,
|
|
||||||
"message": line.strip(),
|
|
||||||
"timestamp": time.time()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Avoid duplicates
|
// Clear any error tracking files
|
||||||
if not any(e['package'] == error_obj['package'] for e in data['errors']):
|
try {
|
||||||
data['errors'].append(error_obj)
|
await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'bash',
|
||||||
|
args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json']
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore if this fails
|
||||||
|
}
|
||||||
|
|
||||||
with open(error_file, 'w') as f:
|
// Start Vite dev server in detached mode
|
||||||
json.dump(data, f)
|
const viteProcess = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'npm',
|
||||||
|
args: ['run', 'dev'],
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
print(f"WARNING: Detected missing package: {error_obj['package']}")
|
console.log('[restart-vite] Vite dev server restarted');
|
||||||
except Exception as e:
|
|
||||||
print(f"Error parsing Vite error: {e}")
|
|
||||||
|
|
||||||
# Start Vite with error monitoring
|
// Wait for Vite to start up
|
||||||
process = subprocess.Popen(
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
['npm', 'run', 'dev'],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
bufsize=1
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start monitoring thread
|
|
||||||
monitor_thread = threading.Thread(target=monitor_output, args=(process, error_file))
|
|
||||||
monitor_thread.daemon = True
|
|
||||||
monitor_thread.start()
|
|
||||||
|
|
||||||
print("Vite restarted successfully!")
|
|
||||||
|
|
||||||
# Store process info for later
|
|
||||||
with open('/tmp/vite-process.pid', 'w') as f:
|
|
||||||
f.write(str(process.pid))
|
|
||||||
|
|
||||||
# Wait for Vite to fully start
|
|
||||||
time.sleep(5)
|
|
||||||
print("Vite is ready")
|
|
||||||
`);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Vite restarted successfully',
|
message: 'Vite restarted successfully'
|
||||||
output: result.output
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { Sandbox } from '@e2b/code-interpreter';
|
|
||||||
|
|
||||||
// Get active sandbox from global state (in production, use a proper state management solution)
|
// Get active sandbox from global state (in production, use a proper state management solution)
|
||||||
declare global {
|
declare global {
|
||||||
@@ -26,30 +25,32 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log(`[run-command] Executing: ${command}`);
|
console.log(`[run-command] Executing: ${command}`);
|
||||||
|
|
||||||
const result = await global.activeSandbox.runCode(`
|
// Parse command and arguments
|
||||||
import subprocess
|
const commandParts = command.trim().split(/\s+/);
|
||||||
import os
|
const cmd = commandParts[0];
|
||||||
|
const args = commandParts.slice(1);
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
// Execute command using Vercel Sandbox
|
||||||
result = subprocess.run(${JSON.stringify(command.split(' '))},
|
const result = await global.activeSandbox.runCommand({
|
||||||
capture_output=True,
|
cmd,
|
||||||
text=True,
|
args
|
||||||
shell=False)
|
});
|
||||||
|
|
||||||
print("STDOUT:")
|
// Get output streams
|
||||||
print(result.stdout)
|
const stdout = await result.stdout();
|
||||||
if result.stderr:
|
const stderr = await result.stderr();
|
||||||
print("\\nSTDERR:")
|
|
||||||
print(result.stderr)
|
|
||||||
print(f"\\nReturn code: {result.returncode}")
|
|
||||||
`);
|
|
||||||
|
|
||||||
const output = result.logs.stdout.join('\n');
|
const output = [
|
||||||
|
stdout ? `STDOUT:\n${stdout}` : '',
|
||||||
|
stderr ? `\nSTDERR:\n${stderr}` : '',
|
||||||
|
`\nExit code: ${result.exitCode}`
|
||||||
|
].filter(Boolean).join('');
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
output,
|
output,
|
||||||
message: 'Command executed successfully'
|
exitCode: result.exitCode,
|
||||||
|
message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -15,54 +15,69 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
console.log('[sandbox-logs] Fetching Vite dev server logs...');
|
||||||
|
|
||||||
// Get the last N lines of the Vite dev server output
|
// Check if Vite processes are running
|
||||||
const result = await global.activeSandbox.runCode(`
|
const psResult = await global.activeSandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'ps',
|
||||||
import os
|
args: ['aux']
|
||||||
|
|
||||||
# Try to get the Vite process output
|
|
||||||
try:
|
|
||||||
# Read the last 100 lines of any log files
|
|
||||||
log_content = []
|
|
||||||
|
|
||||||
# Check if there are any node processes running
|
|
||||||
ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
|
|
||||||
vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()]
|
|
||||||
|
|
||||||
if vite_processes:
|
|
||||||
log_content.append("Vite is running")
|
|
||||||
else:
|
|
||||||
log_content.append("Vite process not found")
|
|
||||||
|
|
||||||
# Try to capture recent console output (this is a simplified approach)
|
|
||||||
# In a real implementation, you'd want to capture the Vite process output directly
|
|
||||||
print(json.dumps({
|
|
||||||
"hasErrors": False,
|
|
||||||
"logs": log_content,
|
|
||||||
"status": "running" if vite_processes else "stopped"
|
|
||||||
}))
|
|
||||||
except Exception as e:
|
|
||||||
print(json.dumps({
|
|
||||||
"hasErrors": True,
|
|
||||||
"logs": [str(e)],
|
|
||||||
"status": "error"
|
|
||||||
}))
|
|
||||||
`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const logData = JSON.parse(result.output || '{}');
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
...logData
|
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
|
let viteRunning = false;
|
||||||
|
let logContent: string[] = [];
|
||||||
|
|
||||||
|
if (psResult.exitCode === 0) {
|
||||||
|
const psOutput = await psResult.stdout();
|
||||||
|
const viteProcesses = psOutput.split('\n').filter(line =>
|
||||||
|
line.toLowerCase().includes('vite') ||
|
||||||
|
line.toLowerCase().includes('npm run dev')
|
||||||
|
);
|
||||||
|
|
||||||
|
viteRunning = viteProcesses.length > 0;
|
||||||
|
|
||||||
|
if (viteRunning) {
|
||||||
|
logContent.push("Vite is running");
|
||||||
|
logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes
|
||||||
|
} else {
|
||||||
|
logContent.push("Vite process not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read any recent log files
|
||||||
|
try {
|
||||||
|
const findResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'find',
|
||||||
|
args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (findResult.exitCode === 0) {
|
||||||
|
const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim());
|
||||||
|
|
||||||
|
for (const logFile of logFiles.slice(0, 2)) {
|
||||||
|
try {
|
||||||
|
const catResult = await global.activeSandbox.runCommand({
|
||||||
|
cmd: 'tail',
|
||||||
|
args: ['-n', '10', logFile]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (catResult.exitCode === 0) {
|
||||||
|
const logFileContent = await catResult.stdout();
|
||||||
|
logContent.push(`--- ${logFile} ---`);
|
||||||
|
logContent.push(logFileContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip if can't read log file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// No log files found, that's OK
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
logs: [result.output],
|
logs: logContent,
|
||||||
status: 'unknown'
|
status: viteRunning ? 'running' : 'stopped'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[sandbox-logs] Error:', error);
|
console.error('[sandbox-logs] Error:', error);
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
|
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
|
||||||
|
|
||||||
interface SandboxPreviewProps {
|
interface SandboxPreviewProps {
|
||||||
sandboxId: string;
|
|
||||||
port: number;
|
|
||||||
type: 'vite' | 'nextjs' | 'console';
|
type: 'vite' | 'nextjs' | 'console';
|
||||||
output?: string;
|
output?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
sandboxUrl?: string; // Real URL from Vercel Sandbox API
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SandboxPreview({
|
export default function SandboxPreview({
|
||||||
sandboxId,
|
|
||||||
port,
|
|
||||||
type,
|
type,
|
||||||
output,
|
output,
|
||||||
isLoading = false
|
isLoading = false,
|
||||||
|
sandboxUrl
|
||||||
}: SandboxPreviewProps) {
|
}: SandboxPreviewProps) {
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
|
||||||
const [showConsole, setShowConsole] = useState(false);
|
const [showConsole, setShowConsole] = useState(false);
|
||||||
const [iframeKey, setIframeKey] = useState(0);
|
const [iframeKey, setIframeKey] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
// Use the real sandbox URL passed from the API
|
||||||
if (sandboxId && type !== 'console') {
|
const previewUrl = sandboxUrl || '';
|
||||||
// In production, this would be the actual E2B sandbox URL
|
|
||||||
// Format: https://{sandboxId}-{port}.e2b.dev
|
|
||||||
setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`);
|
|
||||||
}
|
|
||||||
}, [sandboxId, port, type]);
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setIframeKey(prev => prev + 1);
|
setIframeKey(prev => prev + 1);
|
||||||
@@ -50,9 +42,13 @@ export default function SandboxPreview({
|
|||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
|
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
|
||||||
</span>
|
</span>
|
||||||
|
{previewUrl ? (
|
||||||
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
|
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
|
||||||
{previewUrl}
|
{previewUrl}
|
||||||
</code>
|
</code>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-500">Waiting for sandbox URL...</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -69,6 +65,7 @@ export default function SandboxPreview({
|
|||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
{previewUrl && (
|
||||||
<a
|
<a
|
||||||
href={previewUrl}
|
href={previewUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -78,22 +75,29 @@ export default function SandboxPreview({
|
|||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Preview */}
|
{/* Main Preview */}
|
||||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
||||||
{isLoading && (
|
{(isLoading || !previewUrl) && (
|
||||||
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
|
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
{type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'}
|
{!previewUrl
|
||||||
|
? 'Setting up sandbox environment...'
|
||||||
|
: type === 'vite'
|
||||||
|
? 'Starting Vite dev server...'
|
||||||
|
: 'Starting Next.js dev server...'
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{previewUrl && (
|
||||||
<iframe
|
<iframe
|
||||||
key={iframeKey}
|
key={iframeKey}
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
@@ -101,6 +105,7 @@ export default function SandboxPreview({
|
|||||||
title={`${type} preview`}
|
title={`${type} preview`}
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Console Output (Toggle) */}
|
{/* Console Output (Toggle) */}
|
||||||
|
|||||||
+14
-11
@@ -2,27 +2,30 @@
|
|||||||
// This file contains all configurable settings for the application
|
// This file contains all configurable settings for the application
|
||||||
|
|
||||||
export const appConfig = {
|
export const appConfig = {
|
||||||
// E2B Sandbox Configuration
|
// Vercel Sandbox Configuration
|
||||||
e2b: {
|
vercelSandbox: {
|
||||||
// Sandbox timeout in minutes
|
// Sandbox timeout in minutes
|
||||||
timeoutMinutes: 15,
|
timeoutMinutes: 15,
|
||||||
|
|
||||||
// Convert to milliseconds for E2B API
|
// Convert to milliseconds for Vercel Sandbox API
|
||||||
get timeoutMs() {
|
get timeoutMs() {
|
||||||
return this.timeoutMinutes * 60 * 1000;
|
return this.timeoutMinutes * 60 * 1000;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Vite development server port
|
// Development server port (Vercel Sandbox typically uses 3000 for Next.js/React)
|
||||||
vitePort: 5173,
|
devPort: 3000,
|
||||||
|
|
||||||
// Time to wait for Vite to be ready (in milliseconds)
|
// Time to wait for dev server to be ready (in milliseconds)
|
||||||
viteStartupDelay: 7000,
|
devServerStartupDelay: 7000,
|
||||||
|
|
||||||
// Time to wait for CSS rebuild (in milliseconds)
|
// Time to wait for CSS rebuild (in milliseconds)
|
||||||
cssRebuildDelay: 2000,
|
cssRebuildDelay: 2000,
|
||||||
|
|
||||||
// Default sandbox template (if using templates)
|
// Working directory in sandbox
|
||||||
defaultTemplate: undefined, // or specify a template ID
|
workingDirectory: '/app',
|
||||||
|
|
||||||
|
// Default runtime for sandbox
|
||||||
|
runtime: 'node22' // Available: node22, python3.13, v0-next-shadcn, cua-ubuntu-xfce
|
||||||
},
|
},
|
||||||
|
|
||||||
// AI Model Configuration
|
// AI Model Configuration
|
||||||
@@ -35,7 +38,7 @@ export const appConfig = {
|
|||||||
'openai/gpt-5',
|
'openai/gpt-5',
|
||||||
'moonshotai/kimi-k2-instruct',
|
'moonshotai/kimi-k2-instruct',
|
||||||
'anthropic/claude-sonnet-4-20250514',
|
'anthropic/claude-sonnet-4-20250514',
|
||||||
'google/gemini-2.5-pro'
|
'google/gemini-2.0-flash-exp'
|
||||||
],
|
],
|
||||||
|
|
||||||
// Model display names
|
// Model display names
|
||||||
@@ -43,7 +46,7 @@ export const appConfig = {
|
|||||||
'openai/gpt-5': 'GPT-5',
|
'openai/gpt-5': 'GPT-5',
|
||||||
'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct',
|
'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct',
|
||||||
'anthropic/claude-sonnet-4-20250514': 'Sonnet 4',
|
'anthropic/claude-sonnet-4-20250514': 'Sonnet 4',
|
||||||
'google/gemini-2.5-pro': 'Gemini 2.5 Pro'
|
'google/gemini-2.0-flash-exp': 'Gemini 2.0 Flash (Experimental)'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Temperature settings for non-reasoning models
|
// Temperature settings for non-reasoning models
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Package Detection and Installation Guide
|
# Package Detection and Installation Guide
|
||||||
|
|
||||||
This document explains how to use the XML-based package detection and installation mechanism in the E2B sandbox environment.
|
This document explains how to use the XML-based package detection and installation mechanism in the Vercel Sandbox environment.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The E2B sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system.
|
The Vercel Sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system.
|
||||||
|
|
||||||
## XML Tag Formats
|
## XML Tag Formats
|
||||||
|
|
||||||
@@ -196,43 +196,37 @@ Directly installs packages in the sandbox.
|
|||||||
3. **Order matters**: Packages are installed before files are created
|
3. **Order matters**: Packages are installed before files are created
|
||||||
4. **Use commands** for post-installation tasks like building or testing
|
4. **Use commands** for post-installation tasks like building or testing
|
||||||
|
|
||||||
## Integration with E2B Sandbox
|
## Integration with Vercel Sandbox
|
||||||
|
|
||||||
The package detection mechanism integrates seamlessly with the E2B sandbox:
|
The package detection mechanism integrates seamlessly with the Vercel Sandbox:
|
||||||
|
|
||||||
1. Packages are installed in `/home/user/app/node_modules`
|
1. Packages are installed in the sandbox's working directory
|
||||||
2. The Vite dev server is automatically restarted after package installation
|
2. The development server is automatically restarted after package installation
|
||||||
3. All npm operations run within the sandbox environment
|
3. All npm operations run within the sandbox environment
|
||||||
4. Package.json is automatically updated with new dependencies
|
4. Package.json is automatically updated with new dependencies
|
||||||
|
|
||||||
## E2B Command Execution Methods
|
## Vercel Sandbox Command Execution Methods
|
||||||
|
|
||||||
### Method 1: Using runCode() with Python subprocess
|
### Using runCommand() (Recommended)
|
||||||
```javascript
|
```javascript
|
||||||
// Current implementation pattern
|
// Direct command execution using Vercel Sandbox API
|
||||||
await global.activeSandbox.runCode(`
|
const result = await global.activeSandbox.runCommand({
|
||||||
import subprocess
|
cmd: 'npm',
|
||||||
import os
|
args: ['install', 'axios']
|
||||||
|
|
||||||
os.chdir('/home/user/app')
|
|
||||||
result = subprocess.run(['npm', 'install', 'axios'], capture_output=True, text=True)
|
|
||||||
print(result.stdout)
|
|
||||||
`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Using commands.run() directly (Recommended)
|
|
||||||
```javascript
|
|
||||||
// Direct command execution - cleaner approach
|
|
||||||
const result = await global.activeSandbox.commands.run('npm install axios', {
|
|
||||||
cwd: '/home/user/app',
|
|
||||||
timeout: 60000
|
|
||||||
});
|
});
|
||||||
console.log(result.stdout);
|
const stdout = await result.stdout();
|
||||||
|
const stderr = await result.stderr();
|
||||||
|
console.log(stdout);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Command Execution Options
|
### Command Execution Options
|
||||||
|
|
||||||
When using `sandbox.commands.run()`, you can specify:
|
When using `sandbox.runCommand()`, you can specify:
|
||||||
|
- `cmd`: The command to execute
|
||||||
|
- `args`: Array of arguments
|
||||||
|
- `detached`: Run in background (for long-running processes)
|
||||||
|
- `stdout`: Stream for capturing stdout
|
||||||
|
- `stderr`: Stream for capturing stderr
|
||||||
- `cmd`: Command string to execute
|
- `cmd`: Command string to execute
|
||||||
- `background`: Run in background (true) or wait for completion (false)
|
- `background`: Run in background (true) or wait for completion (false)
|
||||||
- `envs`: Environment variables as key-value pairs
|
- `envs`: Environment variables as key-value pairs
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Required
|
||||||
|
FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping)
|
||||||
|
|
||||||
|
# Vercel Sandbox Authentication (choose one method)
|
||||||
|
# See: https://vercel.com/docs/vercel-sandbox#authentication
|
||||||
|
|
||||||
|
# Method 1: OIDC Token (recommended for development)
|
||||||
|
# Run `vercel env pull` to get VERCEL_OIDC_TOKEN automatically
|
||||||
|
# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull
|
||||||
|
|
||||||
|
# Method 2: Personal Access Token (for production or when OIDC unavailable)
|
||||||
|
# Get these values from your Vercel dashboard
|
||||||
|
# VERCEL_TEAM_ID=team_xxxxxxxxx
|
||||||
|
# VERCEL_PROJECT_ID=prj_xxxxxxxxx
|
||||||
|
# VERCEL_TOKEN=vercel_xxxxxxxxxxxx
|
||||||
|
|
||||||
|
# Optional AI providers (need at least one)
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com
|
||||||
|
OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5)
|
||||||
|
GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey
|
||||||
|
GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference)
|
||||||
Generated
+159
-612
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -8,7 +8,6 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test:integration": "node tests/e2b-integration.test.js",
|
|
||||||
"test:api": "node tests/api-endpoints.test.js",
|
"test:api": "node tests/api-endpoints.test.js",
|
||||||
"test:code": "node tests/code-execution.test.js",
|
"test:code": "node tests/code-execution.test.js",
|
||||||
"test:all": "npm run test:integration && npm run test:api && npm run test:code"
|
"test:all": "npm run test:integration && npm run test:api && npm run test:code"
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
"@ai-sdk/groq": "^2.0.0",
|
"@ai-sdk/groq": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.4",
|
"@ai-sdk/openai": "^2.0.4",
|
||||||
"@anthropic-ai/sdk": "^0.57.0",
|
"@anthropic-ai/sdk": "^0.57.0",
|
||||||
"@e2b/code-interpreter": "^1.5.1",
|
"@vercel/sandbox": "^0.0.17",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"e2b": "^1.13.2",
|
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"groq-sdk": "^0.29.0",
|
"groq-sdk": "^0.29.0",
|
||||||
"lucide-react": "^0.532.0",
|
"lucide-react": "^0.532.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user