#!/usr/bin/env node // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= /* global console, process, URL */ /** * Pre-install dependencies script * This script installs all necessary dependencies before packaging the app * so users don't have to wait for installation on first run */ import { execSync } from 'child_process'; import fs from 'fs'; import http from 'http'; import https from 'https'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); const PREBUILT_DIR = path.join(projectRoot, 'resources', 'prebuilt'); const BIN_DIR = path.join(PREBUILT_DIR, 'bin'); const VENV_DIR = path.join(PREBUILT_DIR, 'venv'); const TERMINAL_VENV_DIR = path.join(PREBUILT_DIR, 'terminal_venv'); const BACKEND_DIR = path.join(projectRoot, 'backend'); // Terminal base packages - keep in sync with electron/main/utils/process.ts const TERMINAL_BASE_PACKAGES = [ 'pandas', 'numpy', 'matplotlib', 'requests', 'openpyxl', 'beautifulsoup4', 'pillow', 'plotly', ]; console.log('šŸš€ Starting pre-installation of dependencies...'); console.log(`šŸ“¦ Binaries will be installed to: ${BIN_DIR}`); console.log(`šŸ Python venv will be installed to: ${VENV_DIR}`); // Ensure directories exist fs.mkdirSync(BIN_DIR, { recursive: true }); fs.mkdirSync(VENV_DIR, { recursive: true }); /** * Validate if the downloaded file is a valid ZIP file */ function isValidZip(filePath) { try { const buffer = fs.readFileSync(filePath); return ( buffer.length > 4 && buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x03 && buffer[3] === 0x04 ); } catch { return false; } } /** * Validate if the downloaded file is a valid tar.gz file */ function isValidTarGz(filePath) { try { const buffer = fs.readFileSync(filePath); return buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b; } catch { return false; } } /** * Download file and validate integrity */ async function downloadFileWithValidation( urlsToTry, dest, validateFn, fileType = 'file' ) { const maxRetries = 2; for (const { url, name } of urlsToTry) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { console.log(` Trying ${name} (attempt ${attempt + 1}/${maxRetries})`); console.log(` URL: ${url}`); await new Promise((resolve, reject) => { const timeout = 180000; // 3 minutes let redirectCount = 0; const maxRedirects = 10; const makeRequest = (requestUrl) => { const requestProtocol = requestUrl.startsWith('https') ? https : http; const request = requestProtocol.get( requestUrl, { timeout: timeout, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', }, }, (response) => { // Handle redirects (301, 302, 307, 308) if ( response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308 ) { redirectCount++; if (redirectCount > maxRedirects) { reject(new Error(`Too many redirects (${redirectCount})`)); return; } const redirectUrl = response.headers.location; if (!redirectUrl) { reject(new Error(`Redirect without location header`)); return; } // Handle relative redirects const absoluteRedirectUrl = redirectUrl.startsWith('http') ? redirectUrl : new URL(redirectUrl, requestUrl).href; console.log( ` Following redirect ${redirectCount} to: ${absoluteRedirectUrl}` ); // Close current response response.destroy(); // Recursively handle redirects makeRequest(absoluteRedirectUrl); return; } if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)); return; } const file = fs.createWriteStream(dest); let downloadedSize = 0; const totalSize = parseInt( response.headers['content-length'] || '0' ); response.on('data', (chunk) => { downloadedSize += chunk.length; if (totalSize > 0) { const progress = ( (downloadedSize / totalSize) * 100 ).toFixed(1); process.stdout.write( `\r Progress: ${progress}% (${(downloadedSize / 1024 / 1024).toFixed(2)}MB / ${(totalSize / 1024 / 1024).toFixed(2)}MB)` ); } }); response.pipe(file); file.on('finish', () => { file.close(); console.log(''); // New line resolve(); }); file.on('error', (err) => { file.close(); if (fs.existsSync(dest)) fs.unlinkSync(dest); reject(err); }); } ); request.on('error', (err) => { if (fs.existsSync(dest)) fs.unlinkSync(dest); reject(err); }); request.on('timeout', () => { request.destroy(); if (fs.existsSync(dest)) fs.unlinkSync(dest); reject(new Error('Request timeout')); }); }; makeRequest(url); }); // Validate downloaded file if (!fs.existsSync(dest)) { throw new Error('Downloaded file does not exist'); } const fileSize = fs.statSync(dest).size; console.log( ` Downloaded file size: ${(fileSize / 1024 / 1024).toFixed(2)}MB` ); if (fileSize < 1024) { const content = fs.readFileSync(dest, 'utf-8'); console.log( ` āš ļø File too small, content: ${content.substring(0, 200)}` ); throw new Error( 'Downloaded file is too small (likely an error page)' ); } if (!validateFn(dest)) { throw new Error(`Downloaded file is not a valid ${fileType}`); } console.log(` āœ… Successfully downloaded and validated from ${name}`); return true; } catch (error) { console.log(`\n āš ļø Failed: ${error.message}`); if (fs.existsSync(dest)) { try { fs.unlinkSync(dest); } catch (e) { console.error(`Error deleting file: ${e}`); // Ignore } } } } } throw new Error(`Failed to download ${fileType} from all sources`); } /** * Recursively copy directory, handling symlinks properly */ function copyDirRecursiveSync(src, dest) { if (!fs.existsSync(src)) { return; } // Create destination directory fs.mkdirSync(dest, { recursive: true }); // Get all files and directories const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyDirRecursiveSync(srcPath, destPath); } else if (entry.isSymbolicLink()) { try { const realPath = fs.realpathSync(srcPath); const realStat = fs.statSync(realPath); if (realStat.isDirectory()) { copyDirRecursiveSync(realPath, destPath); } else { fs.copyFileSync(realPath, destPath); } } catch (err) { console.error(`Error copying file: ${err}`); // If symlink target doesn't exist, skip it console.log(` Skipping broken symlink: ${srcPath}`); } } else { fs.copyFileSync(srcPath, destPath); } } } /** * Get Bun download URL list */ function getBunUrls(platform, arch) { const filename = `bun-${platform}-${arch}.zip`; return [ { url: `https://github.com/oven-sh/bun/releases/latest/download/${filename}`, name: 'GitHub', }, ]; } /** * Get UV download URL list */ function getUvUrls(archStr, platformStr, isWindows = false) { const extension = isWindows ? '.zip' : '.tar.gz'; const filename = `uv-${archStr}-${platformStr}${extension}`; return [ { url: `https://github.com/astral-sh/uv/releases/latest/download/${filename}`, name: 'GitHub', }, ]; } /** * Install uv binary */ async function installUv() { console.log('\nšŸ“„ Installing uv...'); const uvPath = path.join( BIN_DIR, process.platform === 'win32' ? 'uv.exe' : 'uv' ); if (fs.existsSync(uvPath)) { // Verify the binary actually works (correct architecture) try { execSync(`"${uvPath}" --version`, { stdio: 'pipe' }); console.log('āœ… uv already installed'); return uvPath; } catch (e) { console.error(`Error verifying uv binary: ${e}`); console.log( 'āš ļø Existing uv binary is invalid or wrong architecture, removing...' ); fs.unlinkSync(uvPath); } } // Check manual path const manualUvPath = process.env.MANUAL_UV_PATH; if (manualUvPath && fs.existsSync(manualUvPath)) { console.log(`šŸ“‹ Using manually provided uv binary: ${manualUvPath}`); fs.copyFileSync(manualUvPath, uvPath); if (process.platform !== 'win32') { fs.chmodSync(uvPath, '755'); } return uvPath; } // Try to find uv in system PATH try { const whichCommand = process.platform === 'win32' ? 'where uv' : 'which uv'; const systemUvPath = execSync(whichCommand, { encoding: 'utf-8', stdio: 'pipe', }) .trim() .split('\n')[0]; if (systemUvPath && fs.existsSync(systemUvPath)) { console.log(`šŸ“‹ Using system uv: ${systemUvPath}`); fs.copyFileSync(systemUvPath, uvPath); if (process.platform !== 'win32') { fs.chmodSync(uvPath, '755'); } return uvPath; } } catch (error) { console.error(`Error finding uv in system PATH: ${error}`); // uv not found in PATH, continue to try pip or download console.log(' uv not found in system PATH, will try pip or download...'); } // Try pip first const usePipEnv = process.env.USE_PIP_INSTALL_UV; const shouldTryPip = usePipEnv !== 'false'; if (shouldTryPip) { console.log('\nšŸ Trying to install uv via pip...'); try { let pipCommand = null; try { execSync('pip3 --version', { stdio: 'ignore' }); pipCommand = 'pip3'; } catch { try { execSync('pip --version', { stdio: 'ignore' }); pipCommand = 'pip'; } catch { throw new Error('pip not found'); } } const isMacOS = process.platform === 'darwin'; const pipArgs = isMacOS ? `install --user --break-system-packages uv` : `install --user uv`; console.log(` Installing via ${pipCommand}...`); execSync(`${pipCommand} ${pipArgs}`, { stdio: 'inherit' }); // Find installed uv const possiblePaths = process.platform === 'win32' ? [ path.join( os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python311', 'Scripts', 'uv.exe' ), path.join( os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python312', 'Scripts', 'uv.exe' ), path.join( os.homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python313', 'Scripts', 'uv.exe' ), path.join(os.homedir(), '.local', 'bin', 'uv.exe'), 'C:\\Python311\\Scripts\\uv.exe', 'C:\\Python312\\Scripts\\uv.exe', 'C:\\Python313\\Scripts\\uv.exe', ] : [ path.join(os.homedir(), '.local', 'bin', 'uv'), path.join(os.homedir(), 'Library', 'Python', '3.11', 'bin', 'uv'), path.join(os.homedir(), 'Library', 'Python', '3.12', 'bin', 'uv'), path.join(os.homedir(), 'Library', 'Python', '3.13', 'bin', 'uv'), '/usr/local/bin/uv', ]; let foundUvPath = null; try { const whichCommand = process.platform === 'win32' ? 'where uv' : 'which uv'; foundUvPath = execSync(whichCommand, { encoding: 'utf-8' }) .trim() .split('\n')[0]; } catch { for (const p of possiblePaths) { if (fs.existsSync(p)) { foundUvPath = p; break; } } } if (foundUvPath && fs.existsSync(foundUvPath)) { fs.copyFileSync(foundUvPath, uvPath); if (process.platform !== 'win32') { fs.chmodSync(uvPath, '755'); } console.log('āœ… uv installed via pip'); return uvPath; } } catch (error) { console.log(` āš ļø pip install failed: ${error.message}`); } } // Download from GitHub console.log('\nšŸ“„ Downloading uv...'); const platform = process.platform; const arch = process.arch; let platformStr, archStr; archStr = arch === 'x64' ? 'x86_64' : arch === 'arm64' ? 'aarch64' : arch; if (platform === 'darwin') { platformStr = 'apple-darwin'; } else if (platform === 'linux') { platformStr = 'unknown-linux-gnu'; } else if (platform === 'win32') { platformStr = 'pc-windows-msvc'; archStr = 'x86_64'; } else { throw new Error(`Unsupported platform: ${platform}`); } const isWindows = platform === 'win32'; const fileExtension = isWindows ? '.zip' : '.tar.gz'; const tempFilename = path.join( BIN_DIR, `uv-download-${Date.now()}${fileExtension}` ); console.log(` Platform: ${platform}-${arch}`); const urlsToTry = getUvUrls(archStr, platformStr, isWindows); const validateFn = isWindows ? isValidZip : isValidTarGz; await downloadFileWithValidation( urlsToTry, tempFilename, validateFn, fileExtension ); // Extract console.log(' Extracting...'); if (isWindows) { try { const AdmZip = (await import('adm-zip')).default; const zip = new AdmZip(tempFilename); zip.extractAllTo(BIN_DIR, true); } catch (admZipError) { console.error(`Error extracting zip: ${admZipError}`); console.log(' Using system unzip...'); execSync( `powershell -command "Expand-Archive -Path '${tempFilename}' -DestinationPath '${BIN_DIR}' -Force"`, { stdio: 'inherit' } ); } } else { const tar = await import('tar'); await tar.extract({ file: tempFilename, cwd: BIN_DIR }); } // Handle nested directory from tarball if needed if (!isWindows) { const nestedDir = fs .readdirSync(BIN_DIR) .find( (f) => fs.statSync(path.join(BIN_DIR, f)).isDirectory() && f.startsWith('uv-') ); if (nestedDir) { const nestedUvPath = path.join(BIN_DIR, nestedDir, 'uv'); const targetPath = path.join(BIN_DIR, 'uv'); if (fs.existsSync(nestedUvPath)) { console.log(` Found uv in ${nestedDir}, moving...`); try { if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath); fs.renameSync(nestedUvPath, targetPath); // Clean up directory fs.rmSync(path.join(BIN_DIR, nestedDir), { recursive: true, force: true, }); } catch (e) { console.log( ` Warning: Failed to move uv from nested dir: ${e.message}` ); } } } } const extractedUvPath = path.join(BIN_DIR, isWindows ? 'uv.exe' : 'uv'); if (fs.existsSync(extractedUvPath)) { if (!isWindows && extractedUvPath !== uvPath) { fs.renameSync(extractedUvPath, uvPath); } if (process.platform !== 'win32') { fs.chmodSync(uvPath, '755'); } } fs.unlinkSync(tempFilename); console.log('āœ… uv installed successfully'); return uvPath; } /** * Install bun binary */ async function installBun() { console.log('\nšŸ“„ Installing bun...'); const platform = process.platform; const arch = process.arch; const bunPath = path.join(BIN_DIR, platform === 'win32' ? 'bun.exe' : 'bun'); if (fs.existsSync(bunPath)) { // Verify the binary actually works (correct architecture) try { execSync(`"${bunPath}" --version`, { stdio: 'pipe' }); console.log('āœ… bun already installed'); return bunPath; } catch (e) { console.error(`Error verifying bun binary: ${e}`); console.log( 'āš ļø Existing bun binary is invalid or wrong architecture, removing...' ); fs.unlinkSync(bunPath); } } // Check manual path const manualBunPath = process.env.MANUAL_BUN_PATH; if (manualBunPath && fs.existsSync(manualBunPath)) { console.log(`šŸ“‹ Using manually provided bun binary: ${manualBunPath}`); fs.copyFileSync(manualBunPath, bunPath); if (platform !== 'win32') { fs.chmodSync(bunPath, '755'); } return bunPath; } // Try to find bun in system PATH try { const whichCommand = platform === 'win32' ? 'where bun' : 'which bun'; const output = execSync(whichCommand, { encoding: 'utf-8', stdio: 'pipe', }).trim(); const paths = output .split(/[\r\n]+/) .map((p) => p.trim()) .filter((p) => p && !p.includes('INFO:')); for (const systemBunPath of paths) { if (systemBunPath && fs.existsSync(systemBunPath)) { console.log(`šŸ“‹ Using system bun: ${systemBunPath}`); fs.copyFileSync(systemBunPath, bunPath); if (platform !== 'win32') { fs.chmodSync(bunPath, '755'); } return bunPath; } } // Also try common Windows paths (npm global install locations) if (platform === 'win32') { const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8', stdio: 'pipe', }).trim(); const commonPaths = [ path.join(npmPrefix, 'bun.exe'), path.join(npmPrefix, 'bun.cmd'), path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'bun.exe'), path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'bun.cmd'), path.join(os.homedir(), 'AppData', 'Local', 'npm', 'bun.exe'), path.join(os.homedir(), '.bun', 'bin', 'bun.exe'), 'C:\\Program Files\\nodejs\\bun.exe', 'C:\\Program Files\\bun\\bun.exe', 'C:\\bun\\bun.exe', ]; for (const commonPath of commonPaths) { if (fs.existsSync(commonPath)) { console.log(`šŸ“‹ Using bun from common path: ${commonPath}`); fs.copyFileSync(commonPath, bunPath); return bunPath; } } } } catch (error) { // bun not found in PATH, continue to download console.log( ` bun not found in system PATH (${error.message}), will download...` ); } // Determine platform and architecture let bunPlatform, bunArch; if (platform === 'darwin') { bunPlatform = 'darwin'; bunArch = arch === 'arm64' ? 'aarch64' : 'x64'; } else if (platform === 'linux') { bunPlatform = 'linux'; bunArch = arch === 'arm64' ? 'aarch64' : 'x64'; } else if (platform === 'win32') { bunPlatform = 'windows'; bunArch = 'x64'; } else { throw new Error(`Unsupported platform: ${platform}`); } const tempFilename = path.join(BIN_DIR, `bun-download-${Date.now()}.zip`); console.log(` Platform: ${bunPlatform}-${bunArch}`); // Try using curl first (more reliable for redirects) try { const urlsToTry = getBunUrls(bunPlatform, bunArch); const url = urlsToTry[0].url; console.log(` Trying to download with curl: ${url}`); execSync(`curl -L -o "${tempFilename}" "${url}"`, { stdio: 'inherit' }); if (fs.existsSync(tempFilename) && isValidZip(tempFilename)) { console.log(' āœ… Downloaded successfully with curl'); } else { throw new Error('Downloaded file is invalid'); } } catch (curlError) { console.log(` āš ļø curl download failed: ${curlError.message}`); console.log(' Falling back to manual download...'); // Fallback to manual download const urlsToTry = getBunUrls(bunPlatform, bunArch); await downloadFileWithValidation( urlsToTry, tempFilename, isValidZip, 'ZIP' ); } // Extract console.log(' Extracting...'); try { const AdmZip = (await import('adm-zip')).default; const zip = new AdmZip(tempFilename); const entries = zip.getEntries(); for (const entry of entries) { const name = entry.entryName; if ( name === 'bun' || name === 'bun.exe' || name.endsWith('/bun') || name.endsWith('/bun.exe') ) { zip.extractEntryTo(entry, BIN_DIR, false, true); break; } } } catch (admZipError) { console.error(`Error extracting zip: ${admZipError}`); console.log(' Using system unzip...'); if (platform === 'win32') { execSync( `powershell -command "Expand-Archive -Path '${tempFilename}' -DestinationPath '${BIN_DIR}' -Force"`, { stdio: 'inherit' } ); } else { execSync(`unzip -o "${tempFilename}" -d "${BIN_DIR}"`, { stdio: 'inherit', }); } } if (platform !== 'win32' && fs.existsSync(bunPath)) { fs.chmodSync(bunPath, '755'); } fs.unlinkSync(tempFilename); if (fs.existsSync(bunPath)) { console.log('āœ… bun installed successfully'); return bunPath; } else { throw new Error('bun binary not found after extraction'); } } /** * Install Python dependencies */ async function installPythonDeps(uvPath) { console.log('\nšŸ Installing Python dependencies...'); const venvPath = VENV_DIR; const cacheDir = path.join( projectRoot, 'resources', 'prebuilt', 'cache', 'uv_cache' ); const pythonCacheDir = path.join( projectRoot, 'resources', 'prebuilt', 'cache', 'uv_python' ); const toolCacheDir = path.join( projectRoot, 'resources', 'prebuilt', 'cache', 'uv_tool' ); fs.mkdirSync(cacheDir, { recursive: true }); fs.mkdirSync(pythonCacheDir, { recursive: true }); fs.mkdirSync(toolCacheDir, { recursive: true }); const env = { ...process.env, UV_PYTHON_INSTALL_DIR: pythonCacheDir, UV_TOOL_DIR: toolCacheDir, UV_PROJECT_ENVIRONMENT: venvPath, UV_HTTP_TIMEOUT: '300', }; const pyvenvCfg = path.join(venvPath, 'pyvenv.cfg'); // Check if venv contains placeholder // If so, remove it to force recreation if (fs.existsSync(pyvenvCfg)) { try { const content = fs.readFileSync(pyvenvCfg, 'utf-8'); if (content.includes('{{PREBUILT_PYTHON_DIR}}')) { console.log( 'āš ļø Venv contains placeholder from previous build, removing...' ); fs.rmSync(venvPath, { recursive: true, force: true }); console.log('šŸ“¦ Creating fresh Python venv...'); } else { console.log('āœ… Python venv exists, syncing...'); } } catch (error) { console.error(`Error checking venv: ${error}`); console.log('āš ļø Failed to check venv, will try to sync anyway...'); } } else { console.log('šŸ“¦ Creating Python venv...'); } // Ensure Python is installed before syncing // This is critical for Windows where Python might not be in the venv console.log('šŸ Ensuring Python is installed...'); try { execSync(`"${uvPath}" python install 3.10`, { cwd: BACKEND_DIR, env: env, stdio: 'inherit', }); } catch (error) { console.error(`Error installing Python: ${error}`); console.log( 'āš ļø Python install command failed, continuing with sync (Python may already be installed)...' ); } // Use --python-preference only-managed to ensure uv uses its own managed Python // This makes the venv more portable execSync( `"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" --python-preference only-managed`, { cwd: BACKEND_DIR, env: env, stdio: 'inherit' } ); // Verify Python executable exists in the virtual environment const isWindows = process.platform === 'win32'; const pythonExePath = isWindows ? path.join(venvPath, 'Scripts', 'python.exe') : path.join(venvPath, 'bin', 'python'); if (!fs.existsSync(pythonExePath)) { throw new Error( `Python executable not found in virtual environment at: ${pythonExePath}\n` + `Virtual environment may be corrupted. Please ensure uv sync completed successfully.` ); } console.log(`āœ… Python executable verified: ${pythonExePath}`); // Bundle the actual Python installation from UV cache into prebuilt console.log('šŸ“¦ Bundling Python installation...'); try { const uvPythonDir = pythonCacheDir; const prebuiltPythonDir = path.join(PREBUILT_DIR, 'uv_python'); if (fs.existsSync(uvPythonDir)) { console.log(` Copying from: ${uvPythonDir}`); console.log(` Copying to: ${prebuiltPythonDir}`); // Remove existing python dir if it exists if (fs.existsSync(prebuiltPythonDir)) { fs.rmSync(prebuiltPythonDir, { recursive: true, force: true }); } // Copy the Python installation copyDirRecursiveSync(uvPythonDir, prebuiltPythonDir); console.log('āœ… Python installation bundled'); } else { console.log('āš ļø UV Python cache not found, venv may not be portable'); } } catch (error) { console.log(`āš ļø Failed to bundle Python: ${error.message}`); console.log(' The app may fail to start without internet connection'); } console.log('āœ… Python dependencies installed'); console.log('šŸ“ Compiling babel...'); execSync(`"${pythonExePath}" -m babel.messages.frontend compile -d lang`, { cwd: BACKEND_DIR, env: { ...env }, stdio: 'inherit', }); console.log('āœ… Babel compiled'); } /** * Install terminal base venv with common packages for terminal tasks */ async function installTerminalBaseVenv(uvPath) { console.log('\nšŸ–„ļø Installing terminal base venv...'); const pythonCacheDir = path.join( projectRoot, 'resources', 'prebuilt', 'cache', 'uv_python' ); const isWindows = process.platform === 'win32'; const pythonPath = isWindows ? path.join(TERMINAL_VENV_DIR, 'Scripts', 'python.exe') : path.join(TERMINAL_VENV_DIR, 'bin', 'python'); const installedMarker = path.join(TERMINAL_VENV_DIR, '.packages_installed'); const pyvenvCfg = path.join(TERMINAL_VENV_DIR, 'pyvenv.cfg'); // Check if venv contains placeholder (from previous build) // If so, remove it to force recreation if (fs.existsSync(pyvenvCfg)) { try { const content = fs.readFileSync(pyvenvCfg, 'utf-8'); if (content.includes('{{PREBUILT_PYTHON_DIR}}')) { console.log( 'āš ļø Terminal venv contains placeholder from previous build, removing...' ); fs.rmSync(TERMINAL_VENV_DIR, { recursive: true, force: true }); } } catch (error) { console.error(`Error checking terminal venv: ${error}`); console.log('āš ļø Failed to check terminal venv, will continue...'); } } // Check if already fully installed if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) { console.log('āœ… Terminal base venv already exists with packages'); return; } // Check if venv exists but packages not installed (partial install) const needsPackageInstall = fs.existsSync(pythonPath) && !fs.existsSync(installedMarker); const env = { ...process.env, UV_PYTHON_INSTALL_DIR: pythonCacheDir, }; // Create the venv if needed if (!needsPackageInstall) { fs.mkdirSync(TERMINAL_VENV_DIR, { recursive: true }); console.log('šŸ“¦ Creating terminal venv...'); execSync(`"${uvPath}" venv --python 3.10 "${TERMINAL_VENV_DIR}"`, { env: env, stdio: 'inherit', }); } else { console.log('šŸ“¦ Terminal venv exists, installing missing packages...'); } // Install base packages console.log( `šŸ“¦ Installing packages: ${TERMINAL_BASE_PACKAGES.join(', ')}...` ); execSync( `"${uvPath}" pip install --python "${pythonPath}" ${TERMINAL_BASE_PACKAGES.join(' ')}`, { env: env, stdio: 'inherit' } ); // Create marker file fs.writeFileSync(installedMarker, new Date().toISOString()); console.log('āœ… Terminal base venv installed'); } /** * Install browser toolkit deps */ async function installBrowserToolkitDeps(uvPath, venvPath) { console.log('\n🌐 Installing browser toolkit...'); try { const libPath = path.join(venvPath, 'lib'); if (!fs.existsSync(libPath)) { console.log('āš ļø Skipping browser toolkit'); return; } const pythonDir = fs .readdirSync(libPath) .find((n) => n.startsWith('python')); if (!pythonDir) { console.log('āš ļø Skipping browser toolkit'); return; } const toolkitPath = path.join( libPath, pythonDir, 'site-packages', 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts' ); if (!fs.existsSync(toolkitPath)) { console.log('āš ļø Toolkit not found'); return; } const nodeModulesPath = path.join(toolkitPath, 'node_modules'); const distPath = path.join(toolkitPath, 'dist'); if (fs.existsSync(nodeModulesPath) && fs.existsSync(distPath)) { console.log('āœ… Browser toolkit already installed'); return; } const npmCacheDir = path.join(venvPath, '.npm-cache'); fs.mkdirSync(npmCacheDir, { recursive: true }); const env = { ...process.env, UV_PROJECT_ENVIRONMENT: venvPath, npm_config_cache: npmCacheDir, }; let npmCommand = 'npm'; try { execSync('npm --version', { stdio: 'ignore' }); } catch { npmCommand = `"${uvPath}" run npm`; } console.log('šŸ“¦ Installing npm deps...'); execSync(`${npmCommand} install`, { cwd: toolkitPath, env: env, stdio: 'inherit', }); console.log('šŸ”Ø Building TS...'); execSync(`${npmCommand} run build`, { cwd: toolkitPath, env: env, stdio: 'inherit', }); console.log('šŸŽ­ Installing Playwright...'); try { const npxCommand = npmCommand === 'npm' ? 'npx' : `"${uvPath}" run npx`; execSync(`${npxCommand} playwright install`, { cwd: toolkitPath, env: env, stdio: 'inherit', timeout: 600000, }); } catch (e) { console.error(`Error installing Playwright: ${e}`); console.log('āš ļø Playwright install failed (non-critical)'); } console.log('āœ… Browser toolkit installed'); } catch (error) { console.error('āŒ Browser toolkit failed:', error.message); } } /** * Main */ async function main() { try { const uvPath = await installUv(); await installBun(); await installPythonDeps(uvPath); await installTerminalBaseVenv(uvPath); await installBrowserToolkitDeps(uvPath, VENV_DIR); console.log('\nāœ… All dependencies installed!'); console.log(`šŸ“¦ Binaries: ${BIN_DIR}`); console.log(`šŸ Python venv: ${VENV_DIR}`); console.log(`šŸ–„ļø Terminal venv: ${TERMINAL_VENV_DIR}`); } catch (error) { console.error('\nāŒ Failed:', error); process.exit(1); } } main();