import { NextRequest, NextResponse } from 'next/server'; declare global { var activeSandbox: any; } export async function POST(request: NextRequest) { try { const { files } = await request.json(); if (!files || typeof files !== 'object') { return NextResponse.json({ success: false, error: 'Files object is required' }, { status: 400 }); } if (!global.activeSandbox) { return NextResponse.json({ success: false, error: 'No active sandbox' }, { status: 404 }); } console.log('[detect-and-install-packages] Processing files:', Object.keys(files)); // Extract all import statements from the files const imports = new Set(); const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*(?:from\s+)?['"]([^'"]+)['"]/g; const requireRegex = /require\s*\(['"]([^'"]+)['"]\)/g; for (const [filePath, content] of Object.entries(files)) { if (typeof content !== 'string') continue; // Skip non-JS/JSX/TS/TSX files if (!filePath.match(/\.(jsx?|tsx?)$/)) continue; // Find ES6 imports let match; while ((match = importRegex.exec(content)) !== null) { imports.add(match[1]); } // Find CommonJS requires while ((match = requireRegex.exec(content)) !== null) { imports.add(match[1]); } } console.log('[detect-and-install-packages] Found imports:', Array.from(imports)); // Log specific heroicons imports const heroiconImports = Array.from(imports).filter(imp => imp.includes('heroicons')); if (heroiconImports.length > 0) { console.log('[detect-and-install-packages] Heroicon imports:', heroiconImports); } // Filter out relative imports and built-in modules const packages = Array.from(imports).filter(imp => { // Skip relative imports if (imp.startsWith('.') || imp.startsWith('/')) return false; // Skip built-in Node modules const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process']; 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; } else { // Regular package, return just the first part return true; } }); // Extract just the package names (without subpaths) const packageNames = packages.map(pkg => { if (pkg.startsWith('@')) { // Scoped package: @scope/package or @scope/package/subpath const parts = pkg.split('/'); return parts.slice(0, 2).join('/'); } else { // Regular package: package or package/subpath return pkg.split('/')[0]; } }); // Remove duplicates const uniquePackages = [...new Set(packageNames)]; console.log('[detect-and-install-packages] Packages to install:', uniquePackages); if (uniquePackages.length === 0) { return NextResponse.json({ success: true, packagesInstalled: [], message: 'No new packages to install' }); } // Check which packages are already installed const checkResult = await global.activeSandbox.runCode(` import os import json installed = [] missing = [] packages = ${JSON.stringify(uniquePackages)} for package in packages: # 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): 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({ success: true, packagesInstalled: [], packagesAlreadyInstalled: status.installed, message: 'All packages already installed' }); } // Install missing packages console.log('[detect-and-install-packages] Installing packages:', status.missing); const installResult = await global.activeSandbox.runCode(` import subprocess import os import json os.chdir('/home/user/app') packages_to_install = ${JSON.stringify(status.missing)} # Join packages into a single install command packages_str = ' '.join(packages_to_install) cmd = f'npm install {packages_str} --save' print(f"Running: {cmd}") # Run npm install with explicit save flag 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 { const stdout = installResult.logs.stdout.join(''); const resultMatch = stdout.match(/Result:\s*({.*})/); if (resultMatch) { installStatus = JSON.parse(resultMatch[1]); } else { // Fallback parsing const lines = stdout.split('\n'); 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 (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) { console.error('[detect-and-install-packages] Failed to install:', installStatus.failed); } return NextResponse.json({ success: true, packagesInstalled: installStatus.installed, packagesFailed: installStatus.failed, packagesAlreadyInstalled: status.installed, message: `Installed ${installStatus.installed.length} packages`, logs: installResult.logs.stdout.join('\n') }); } catch (error) { console.error('[detect-and-install-packages] Error:', error); return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 }); } }