Ctrl K
ComunidadGuías…

scripts/preinstall-deps.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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 { execFileSync, execSync } from 'child_process';
import { createHash } from 'crypto';
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 MINGIT_DIR = path.join(PREBUILT_DIR, 'mingit');
const BACKEND_DIR = path.join(projectRoot, 'backend');

// MinGit is the official embeddable Git for Windows distribution. Keep the
// archive and checksum pinned so packaged builds are reproducible.
const MINGIT_VERSION = '2.55.0.3';
const MINGIT_X64_URL =
  'https://github.com/git-for-windows/git/releases/download/' +
  'v2.55.0.windows.3/MinGit-2.55.0.3-64-bit.zip';
const MINGIT_X64_SHA256 =
  'f48e2d2dc74a24454adc6d8fd0ac25bf9c2386f19cfb06202b9465aaad4f9f05';

// 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;
  }
}

function sha256File(filePath) {
  return createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}

/**
 * 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 the official embeddable Git runtime used by the packaged Brain.
 */
async function installMinGit() {
  if (process.platform !== 'win32') return null;
  if (process.arch !== 'x64') {
    throw new Error(`Unsupported MinGit architecture: ${process.arch}`);
  }

  console.log('\n📥 Installing MinGit...');
  const gitPath = path.join(MINGIT_DIR, 'cmd', 'git.exe');
  const versionMarker = path.join(MINGIT_DIR, '.eigent-mingit-version');
  if (
    fs.existsSync(gitPath) &&
    fs.existsSync(versionMarker) &&
    fs.readFileSync(versionMarker, 'utf-8').trim() === MINGIT_VERSION
  ) {
    try {
      execFileSync(gitPath, ['--version'], { stdio: 'pipe' });
      console.log(`✅ MinGit ${MINGIT_VERSION} already installed`);
      return gitPath;
    } catch (error) {
      console.error(`Error verifying MinGit: ${error}`);
    }
  }

  if (fs.existsSync(MINGIT_DIR)) {
    fs.rmSync(MINGIT_DIR, { recursive: true, force: true });
  }

  const archivePath = path.join(
    PREBUILT_DIR,
    `mingit-download-${Date.now()}.zip`
  );
  try {
    await downloadFileWithValidation(
      [{ url: MINGIT_X64_URL, name: 'Git for Windows' }],
      archivePath,
      isValidZip,
      'MinGit ZIP'
    );
    const actualSha256 = sha256File(archivePath);
    if (actualSha256 !== MINGIT_X64_SHA256) {
      throw new Error(
        `MinGit checksum mismatch: expected ${MINGIT_X64_SHA256}, ` +
          `received ${actualSha256}`
      );
    }

    const AdmZip = (await import('adm-zip')).default;
    new AdmZip(archivePath).extractAllTo(MINGIT_DIR, true);
  } finally {
    if (fs.existsSync(archivePath)) fs.unlinkSync(archivePath);
  }

  if (!fs.existsSync(gitPath)) {
    throw new Error(`MinGit executable not found after extraction: ${gitPath}`);
  }
  execFileSync(gitPath, ['--version'], { stdio: 'pipe' });
  fs.writeFileSync(versionMarker, `${MINGIT_VERSION}\n`, 'utf-8');
  console.log(`✅ MinGit ${MINGIT_VERSION} installed successfully`);
  return gitPath;
}

/**
 * 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 installMinGit();
    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}`);
    if (process.platform === 'win32') {
      console.log(`🔀 MinGit: ${MINGIT_DIR}`);
    }
  } catch (error) {
    console.error('\n❌ Failed:', error);
    process.exit(1);
  }
}

main();