eigent/scripts/preinstall-deps.js
Tong Chen af93bb3065
feat: Add Lint & Format (#878)
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com>
Co-authored-by: eigent-ai <camel@eigent.ai>
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
2026-02-01 23:16:18 +08:00

1080 lines
31 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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',
];
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');
if (fs.existsSync(pyvenvCfg)) {
console.log('✅ Python venv exists, syncing...');
} 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(`"${uvPath}" run pybabel 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');
// 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();