Files
open-lovable/app/api/install-packages/route.ts
T
2025-09-02 18:27:04 -04:00

369 lines
12 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { Sandbox } from '@e2b/code-interpreter';
declare global {
var activeSandbox: any;
var sandboxData: any;
}
export async function POST(request: NextRequest) {
try {
const { packages, sandboxId } = await request.json();
if (!packages || !Array.isArray(packages) || packages.length === 0) {
return NextResponse.json({
success: false,
error: 'Packages array is required'
}, { status: 400 });
}
// Validate and deduplicate package names
const validPackages = [...new Set(packages)]
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '')
.map(pkg => pkg.trim());
if (validPackages.length === 0) {
return NextResponse.json({
success: false,
error: 'No valid package names provided'
}, { status: 400 });
}
// Log if duplicates were found
if (packages.length !== validPackages.length) {
console.log(`[install-packages] Cleaned packages: removed ${packages.length - validPackages.length} invalid/duplicate entries`);
console.log(`[install-packages] Original:`, packages);
console.log(`[install-packages] Cleaned:`, validPackages);
}
// Try to get sandbox - either from global or reconnect
let 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) {
return NextResponse.json({
success: false,
error: 'No active sandbox available'
}, { status: 400 });
}
console.log('[install-packages] Installing packages:', packages);
// Create a response stream for real-time updates
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// Function to send progress updates
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(message));
};
// Start installation in background
(async (sandboxInstance) => {
try {
await sendProgress({
type: 'start',
message: `Installing ${validPackages.length} package${validPackages.length > 1 ? 's' : ''}...`,
packages: validPackages
});
// Kill any existing Vite process first
await sendProgress({ type: 'status', message: 'Stopping development server...' });
await sandboxInstance.runCode(`
import subprocess
import os
import signal
# Try to kill any existing Vite process
try:
with open('/tmp/vite-process.pid', 'r') as f:
pid = int(f.read().strip())
os.kill(pid, signal.SIGTERM)
print("Stopped existing Vite process")
except:
print("No existing Vite process found")
`);
// Check which packages are already installed
await sendProgress({
type: 'status',
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;
// 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 {
packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length));
} catch (e) {
console.error('Failed to parse packages to install:', e);
}
}
}
} else {
console.error('[install-packages] Invalid checkResult structure:', checkResult);
// If we can't check, just try to install all packages
packagesToInstall = validPackages;
}
if (packagesToInstall.length === 0) {
await sendProgress({
type: 'success',
message: 'All packages are already installed',
installedPackages: [],
alreadyInstalled: validPackages
});
return;
}
// 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({
type: 'info',
message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}`
});
const installResult = await sandboxInstance.runCode(`
import subprocess
import os
os.chdir('/home/user/app')
# 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')) {
await sendProgress({ type: 'warning', message: line });
} else if (line.trim() && !line.includes('undefined')) {
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) {
await sendProgress({
type: 'success',
message: `Successfully installed: ${installedPackages.join(', ')}`,
installedPackages
});
} else {
await sendProgress({
type: 'error',
message: 'Failed to verify package installation'
});
}
// Restart Vite dev server
await sendProgress({ type: 'status', message: 'Restarting development server...' });
await sandboxInstance.runCode(`
import subprocess
import os
import time
os.chdir('/home/user/app')
# 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({
type: 'complete',
message: 'Package installation complete and dev server restarted!',
installedPackages
});
} catch (error) {
const errorMessage = (error as Error).message;
if (errorMessage && errorMessage !== 'undefined') {
await sendProgress({
type: 'error',
message: errorMessage
});
}
} finally {
await writer.close();
}
})(sandbox);
// Return the stream
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('[install-packages] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}