Ctrl K
ComunidadGuías…

electron/main/utils/process.ts

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

Archivo original ↗Preguntar a KiBot
// ========= 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. =========

import { execFileSync, execSync, spawn } from 'child_process';
import { app } from 'electron';
import log from 'electron-log';
import fs from 'fs';
import os from 'os';
import path from 'path';

export function getResourcePath() {
  return path.join(app.getAppPath(), 'resources');
}

export function getBackendPath() {
  if (app.isPackaged) {
    //  after packaging, backend is in extraResources
    return path.join(process.resourcesPath, 'backend');
  } else {
    // development environment
    return path.join(app.getAppPath(), 'backend');
  }
}

/** Return the Git runtime embedded in packaged Windows builds. */
export function getBundledGitPath(): string | null {
  if (!app.isPackaged || process.platform !== 'win32') {
    return null;
  }

  const gitPath = path.join(
    process.resourcesPath,
    'prebuilt',
    'mingit',
    'cmd',
    'git.exe'
  );
  return fs.existsSync(gitPath) ? gitPath : null;
}

export function runInstallScript(scriptPath: string): Promise<boolean> {
  return new Promise<boolean>((resolve, reject) => {
    const installScriptPath = path.join(
      getResourcePath(),
      'scripts',
      scriptPath
    );
    log.info(`Running script at: ${installScriptPath}`);

    const nodeProcess = spawn(process.execPath, [installScriptPath], {
      env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
    });

    let stderrOutput = '';

    nodeProcess.stdout.on('data', (data) => {
      log.info(`Script output: ${data}`);
    });

    nodeProcess.stderr.on('data', (data) => {
      const errorMsg = data.toString();
      stderrOutput += errorMsg;
      log.error(`Script error: ${errorMsg}`);
    });

    nodeProcess.on('close', (code) => {
      if (code === 0) {
        log.info('Script completed successfully');
        resolve(true);
      } else {
        log.error(`Script exited with code ${code}`);
        const errorMessage =
          stderrOutput.trim() || `Script exited with code ${code}`;
        reject(new Error(errorMessage));
      }
    });
  });
}

export async function getBinaryName(name: string): Promise<string> {
  if (process.platform === 'win32') {
    return `${name}.exe`;
  }
  return name;
}

/**
 * Get path to prebuilt binary (if available in packaged app)
 */
export function getPrebuiltBinaryPath(name?: string): string | null {
  if (!app.isPackaged) {
    return null;
  }

  const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
  if (!fs.existsSync(prebuiltBinDir)) {
    return null;
  }

  if (!name) {
    return prebuiltBinDir;
  }

  const binaryName = process.platform === 'win32' ? `${name}.exe` : name;
  const binaryPath = path.join(prebuiltBinDir, binaryName);
  return fs.existsSync(binaryPath) ? binaryPath : null;
}

export async function getBinaryPath(name?: string): Promise<string> {
  // First check for prebuilt binary in packaged app
  if (app.isPackaged) {
    const prebuiltPath = getPrebuiltBinaryPath(name);
    if (prebuiltPath) {
      log.info(`Using prebuilt binary: ${prebuiltPath}`);
      return prebuiltPath;
    }
  }

  // In dev: prefer system PATH uv (e.g. Homebrew) for speed - reuses existing cache
  if (!app.isPackaged && name === 'uv') {
    try {
      const whichCmd = process.platform === 'win32' ? 'where.exe' : 'which';
      const found = execFileSync(whichCmd, [name], {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
      }).trim();
      const systemPath = found.split(/\r?\n/)[0]?.trim();
      if (systemPath && fs.existsSync(systemPath)) {
        log.info(`[DEV] Using system uv from PATH: ${systemPath}`);
        return systemPath;
      }
    } catch {
      // Not found on PATH, fall through to .eigent/bin
    }
  }

  const binariesDir = path.join(os.homedir(), '.eigent', 'bin');

  // Ensure .eigent/bin directory exists
  if (!fs.existsSync(binariesDir)) {
    fs.mkdirSync(binariesDir, { recursive: true });
  }

  if (!name) {
    return binariesDir;
  }

  const binaryName = await getBinaryName(name);
  return path.join(binariesDir, binaryName);
}

export function getCachePath(folder: string): string {
  // For packaged app, try to use prebuilt cache first
  if (app.isPackaged) {
    const prebuiltCachePath = path.join(
      process.resourcesPath,
      'prebuilt',
      'cache',
      folder
    );
    if (fs.existsSync(prebuiltCachePath)) {
      log.info(`Using prebuilt cache: ${prebuiltCachePath}`);
      return prebuiltCachePath;
    }
  }

  const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder);

  // Ensure cache directory exists
  if (!fs.existsSync(cacheDir)) {
    fs.mkdirSync(cacheDir, { recursive: true });
  }

  return cacheDir;
}

/**
 * Fix pyvenv.cfg by replacing placeholder with actual Python path
 * This makes prebuilt venvs portable across different machines
 */
function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean {
  try {
    let content = fs.readFileSync(pyvenvCfgPath, 'utf-8');

    if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
      const prebuiltPythonDir = getPrebuiltPythonDir();
      if (!prebuiltPythonDir) {
        log.warn(
          '[VENV] Cannot fix pyvenv.cfg: prebuilt Python directory not found'
        );
        return false;
      }

      content = content.replace(
        /\{\{PREBUILT_PYTHON_DIR\}\}/g,
        prebuiltPythonDir
      );

      const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
      if (homeMatch) {
        const finalHomePath = homeMatch[1].trim();
        log.info(`[VENV] pyvenv.cfg home path set to: ${finalHomePath}`);

        // Verify the path exists
        if (!fs.existsSync(finalHomePath)) {
          log.warn(
            `[VENV] WARNING: home path does not exist: ${finalHomePath}`
          );
        } else {
          log.info(`[VENV] home path verified successfully`);
        }
      }

      fs.writeFileSync(pyvenvCfgPath, content);
      log.info(
        `[VENV] Fixed pyvenv.cfg placeholder with: ${prebuiltPythonDir}`
      );
      return true;
    }

    const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
    if (homeMatch) {
      const homePath = homeMatch[1].trim();
      if (!fs.existsSync(homePath)) {
        log.warn(`[VENV] pyvenv.cfg home path does not exist: ${homePath}`);
        return false;
      }
    }

    return true;
  } catch (error) {
    log.warn(`[VENV] Failed to fix pyvenv.cfg: ${error}`);
    return false;
  }
}

/**
 * Get the actual Python interpreter path from venv's pyvenv.cfg (home points to Python's bin dir).
 * Used to fix shebangs when venv is in userData but Python is in app bundle.
 */
function getActualPythonPathFromPyvenvCfg(venvPath: string): string | null {
  const pyvenvCfgPath = path.join(venvPath, 'pyvenv.cfg');
  if (!fs.existsSync(pyvenvCfgPath)) return null;

  const content = fs.readFileSync(pyvenvCfgPath, 'utf-8');
  const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
  if (!homeMatch) return null;

  const home = homeMatch[1].trim();
  if (!path.isAbsolute(home) || !fs.existsSync(home)) return null;

  // home is Python's bin dir; find python3.X or python3
  try {
    const entries = fs.readdirSync(home);
    const py = entries.find(
      (e) => e === 'python3' || (e.startsWith('python3.') && !e.endsWith('.py'))
    );
    if (py) {
      const fullPath = path.join(home, py);
      if (fs.existsSync(fullPath)) return fullPath;
    }
  } catch {
    // ignore
  }
  return null;
}

/**
 * Fix shebang lines in venv scripts by replacing placeholder or broken relative path with actual Python path.
 * The venv/bin/python script was previously skipped but must be fixed when venv is extracted to userData
 * (relative paths like ../../uv_python/... break because Python lives in the app bundle).
 */
function fixVenvScriptShebangs(venvPath: string): boolean {
  const isWindows = process.platform === 'win32';

  if (isWindows) {
    log.info(`[VENV] Skipping shebang fixes on Windows (not needed)`);
    return true;
  }

  const binDir = path.join(venvPath, 'bin');
  if (!fs.existsSync(binDir)) return false;

  const pythonExe = path.join(binDir, 'python');
  if (!fs.existsSync(pythonExe)) {
    log.warn(`[VENV] Python executable not found: ${pythonExe}`);
    return false;
  }

  const actualPythonPath =
    getActualPythonPathFromPyvenvCfg(venvPath) ?? findPythonForTerminalVenv();

  try {
    const entries = fs.readdirSync(binDir);
    let fixedCount = 0;

    for (const entry of entries) {
      const filePath = path.join(binDir, entry);

      try {
        const stat = fs.lstatSync(filePath);
        if (stat.isDirectory() || stat.isSymbolicLink()) continue;
        if (
          entry.endsWith('.exe') ||
          entry.endsWith('.dll') ||
          entry.endsWith('.pyd')
        ) {
          continue;
        }
        // Include python/activate scripts - they were previously skipped but need shebang fix
        // when venv is in userData with relative paths
      } catch {
        continue;
      }

      try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const firstLine = content.split('\n')[0];
        if (!firstLine?.startsWith('#!')) continue;

        const shebangPath = firstLine.slice(2).trim();
        let newContent = content;

        // Replace placeholders
        if (content.includes('{{PREBUILT_VENV_PYTHON}}')) {
          newContent = newContent.replace(
            /\{\{PREBUILT_VENV_PYTHON\}\}/g,
            actualPythonPath ?? pythonExe
          );
        }
        if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
          const prebuiltPythonDir = getPrebuiltPythonDir();
          if (prebuiltPythonDir) {
            newContent = newContent.replace(
              /\{\{PREBUILT_PYTHON_DIR\}\}/g,
              prebuiltPythonDir
            );
          }
        }

        if (actualPythonPath && shebangPath && !shebangPath.startsWith('{{')) {
          const resolved = path.resolve(path.dirname(filePath), shebangPath);
          if (!fs.existsSync(resolved)) {
            newContent = newContent.replace(/^#!.*$/m, `#!${actualPythonPath}`);
          }
        }

        if (newContent !== content) {
          fs.writeFileSync(filePath, newContent, 'utf-8');
          if (process.platform !== 'win32') {
            fs.chmodSync(filePath, 0o755);
          }
          fixedCount++;
        }
      } catch {
        // Silently skip files that can't be processed
      }
    }

    if (fixedCount > 0) {
      log.info(`[VENV] Fixed shebangs in ${fixedCount} script(s)`);
    }
    return true;
  } catch (error) {
    log.warn(`[VENV] Failed to fix script shebangs: ${error}`);
    return false;
  }
}

const PREBUILT_FIXED_MARKER = '.prebuilt_fixed';

/**
 * Ensure venv/bin/python exists - create symlink if missing or broken.
 */
function ensureVenvPythonSymlink(venvPath: string): boolean {
  if (process.platform === 'win32') return true;

  const binDir = path.join(venvPath, 'bin');
  const pythonPath = path.join(binDir, 'python');
  if (!fs.existsSync(binDir)) return false;

  try {
    fs.accessSync(pythonPath, fs.constants.X_OK);
    return true;
  } catch {
    // python missing or broken symlink - create/fix below
    log.info(
      `[VENV] python not found or broken at ${pythonPath}, creating symlink...`
    );
  }

  const actualPython = getActualPythonPathFromPyvenvCfg(venvPath);

  // Find python3.X in venv/bin as fallback (e.g. python3.10)
  const entries = fs.readdirSync(binDir, { withFileTypes: true });
  const py3 = entries.find(
    (e) =>
      !e.isDirectory() &&
      (e.name === 'python3' ||
        (e.name.startsWith('python3.') && !e.name.endsWith('.py')))
  );
  const targetInBin = py3 ? path.join(binDir, py3.name) : null;

  try {
    // Remove existing file/symlink (existsSync is false for broken symlinks, so use lstat)
    try {
      fs.lstatSync(pythonPath);
      fs.unlinkSync(pythonPath);
    } catch {
      // ENOENT = path doesn't exist, that's fine
    }

    // Prefer actual Python from pyvenv.cfg (absolute path to app bundle);
    // fallback to python3.X in same dir (relative symlink)
    let target: string | null = null;
    if (actualPython && fs.existsSync(actualPython)) {
      target = actualPython;
    } else if (targetInBin && fs.existsSync(targetInBin)) {
      // Use relative name for symlink within same directory
      target = py3!.name;
    }

    if (!target) {
      log.warn(`[VENV] No valid Python target found for symlink`);
      return false;
    }

    fs.symlinkSync(target, pythonPath);
    try {
      fs.chmodSync(pythonPath, 0o755);
    } catch {}
    log.info(`[VENV] Created python symlink -> ${target}`);
    return true;
  } catch (error) {
    log.warn(`[VENV] Failed to create python symlink: ${error}`);
    return false;
  }
}

/**
 * Get path to prebuilt venv (if available in packaged app)
 * All platforms use prebuilt/venv directory.
 */
export function getPrebuiltVenvPath(): string | null {
  if (!app.isPackaged) {
    return null;
  }

  const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
  const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
  const pyvenvCfgPath = path.join(prebuiltVenvPath, 'pyvenv.cfg');
  const fixedMarkerPath = path.join(prebuiltDir, PREBUILT_FIXED_MARKER);
  const currentVersion = app.getVersion();

  if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(pyvenvCfgPath)) {
    const needsFix =
      !fs.existsSync(fixedMarkerPath) ||
      fs.readFileSync(fixedMarkerPath, 'utf-8').trim() !== currentVersion;

    if (needsFix) {
      fixPyvenvCfgPlaceholder(pyvenvCfgPath);
      ensureVenvPythonSymlink(prebuiltVenvPath);
      fixVenvScriptShebangs(prebuiltVenvPath);
      fs.writeFileSync(fixedMarkerPath, currentVersion, 'utf-8');
    }

    const pythonExePath = getVenvPythonPath(prebuiltVenvPath);
    if (fs.existsSync(pythonExePath)) {
      return prebuiltVenvPath;
    }
    log.warn(`[VENV] Prebuilt venv Python missing at: ${pythonExePath}`);
  }

  return null;
}

/**
 * Find Python executable in prebuilt Python directory for terminal venv
 */
function findPythonForTerminalVenv(): string | null {
  const prebuiltPythonDir = getPrebuiltPythonDir();
  if (!prebuiltPythonDir) {
    return null;
  }

  // Look for Python executable in the prebuilt directory
  // UV stores Python in subdirectories like: cpython-3.11.x+.../install/bin/python
  const possiblePaths: string[] = [];

  // First, try common direct paths
  possiblePaths.push(
    path.join(prebuiltPythonDir, 'install', 'bin', 'python'),
    path.join(prebuiltPythonDir, 'install', 'python.exe'),
    path.join(prebuiltPythonDir, 'bin', 'python'),
    path.join(prebuiltPythonDir, 'python.exe')
  );

  // Then, search in subdirectories (UV stores Python in versioned directories)
  try {
    if (fs.existsSync(prebuiltPythonDir)) {
      const entries = fs.readdirSync(prebuiltPythonDir, {
        withFileTypes: true,
      });
      for (const entry of entries) {
        if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
          const subDir = path.join(prebuiltPythonDir, entry.name);
          possiblePaths.push(
            path.join(subDir, 'install', 'bin', 'python'),
            path.join(subDir, 'install', 'python.exe'),
            path.join(subDir, 'bin', 'python'),
            path.join(subDir, 'python.exe')
          );
        }
      }
    }
  } catch (error) {
    log.warn('[PROCESS] Error searching for prebuilt Python:', error);
  }

  for (const pythonPath of possiblePaths) {
    if (fs.existsSync(pythonPath)) {
      return pythonPath;
    }
  }

  return null;
}

const TERMINAL_VENV_VERSION_FILE = '.terminal_venv_version';
const BACKEND_VENV_VERSION_FILE = '.backend_venv_version';

let _optionalDepsSyncStarted = false;

/** Background uv sync to install deps excluded from bundle (yt_dlp, etc). Does not block startup. */
function runBackgroundUvSyncForOptionalDeps(userBackendVenv: string): void {
  if (!app.isPackaged) return;
  if (_optionalDepsSyncStarted) return;
  _optionalDepsSyncStarted = true;

  const uvPath = getPrebuiltBinaryPath('uv');
  const backendPath = getBackendPath();
  const uvLockPath = path.join(backendPath, 'uv.lock');
  if (
    !uvPath ||
    !fs.existsSync(uvLockPath) ||
    !fs.existsSync(path.join(backendPath, 'pyproject.toml'))
  ) {
    return;
  }

  const prebuiltPython = getPrebuiltPythonDir();
  const uvEnv = {
    ...process.env,
    UV_PROJECT_ENVIRONMENT: userBackendVenv,
    UV_PYTHON_INSTALL_DIR: prebuiltPython || getCachePath('uv_python'),
    UV_TOOL_DIR: getCachePath('uv_tool'),
    UV_HTTP_TIMEOUT: '300',
  } as NodeJS.ProcessEnv;
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  const syncArgs =
    timezone === 'Asia/Shanghai'
      ? [
          'sync',
          '--no-dev',
          '--default-index',
          'https://mirrors.aliyun.com/pypi/simple/',
          '--index',
          'https://pypi.org/simple/',
        ]
      : ['sync', '--no-dev'];
  log.info(
    '[VENV] Starting background uv sync to install optional deps (e.g. yt_dlp); app will not wait.'
  );
  const child = spawn(uvPath, syncArgs, {
    cwd: backendPath,
    env: uvEnv,
    stdio: 'ignore',
    detached: true,
  });
  child.unref();
  child.on('error', (err) => {
    log.warn(`[VENV] Background uv sync error: ${err.message}`);
  });
  child.on('exit', (code) => {
    if (code === 0) {
      log.info('[VENV] Background uv sync completed');
    } else {
      log.warn(
        `[VENV] Background uv sync exited with code ${code} (optional deps may be missing)`
      );
    }
  });
}

/**
 * Copy prebuilt backend venv to ~/.eigent/venvs/backend-{version} for unified management.
 * The copied venv is the one actually used by the backend (via getVenvPath()).
 * The source venv (prebuilt/extracted) is kept as-is for re-copying on version changes.
 *
 * @param version App version (used for version-specific venv directory)
 */
export function ensureBackendVenvAtUserPath(version: string): void {
  if (!app.isPackaged) return;

  const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
  const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
  const prebuiltUvPython = path.join(prebuiltDir, 'uv_python');

  if (
    !fs.existsSync(prebuiltVenvPath) ||
    !fs.existsSync(path.join(prebuiltVenvPath, 'pyvenv.cfg'))
  ) {
    return;
  }

  const sourceVenvPath = prebuiltVenvPath;

  const userVenvsDir = path.join(os.homedir(), '.eigent', 'venvs');
  const userBackendVenv = path.join(userVenvsDir, `backend-${version}`);
  const pyvenvCfgPath = path.join(userBackendVenv, 'pyvenv.cfg');
  const versionFile = path.join(userVenvsDir, BACKEND_VENV_VERSION_FILE);

  // Ensure uv_python symlink exists (needed even if venv already copied)
  const userUvPython = path.join(os.homedir(), '.eigent', 'uv_python');
  if (!fs.existsSync(userUvPython) && fs.existsSync(prebuiltUvPython)) {
    try {
      fs.mkdirSync(path.dirname(userUvPython), { recursive: true });
      fs.symlinkSync(prebuiltUvPython, userUvPython);
      log.info(`[VENV] Created uv_python symlink: ${userUvPython}`);
    } catch (e) {
      log.warn(`[VENV] Failed to create uv_python symlink: ${e}`);
    }
  }

  if (fs.existsSync(pyvenvCfgPath)) {
    const storedVersion = fs.existsSync(versionFile)
      ? fs.readFileSync(versionFile, 'utf-8').trim()
      : null;
    if (storedVersion === version) {
      log.info(
        `[VENV] Backend venv already at ${userBackendVenv} (v${version})`
      );
      // Ensure optional deps get installed even if last sync failed or was interrupted.
      runBackgroundUvSyncForOptionalDeps(userBackendVenv);
      return;
    }
  }

  log.info(`[VENV] Copying prebuilt backend venv to ${userBackendVenv}...`);

  try {
    fs.mkdirSync(userVenvsDir, { recursive: true });

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

    fs.cpSync(sourceVenvPath, userBackendVenv, {
      recursive: true,
      verbatimSymlinks: true,
    });

    // Fix paths after copying (source venv paths don't match user venv location)
    // - pyvenv.cfg: update home path to point to correct Python location
    // - shebangs: update #! paths in bin/* scripts to point to correct Python
    // - python symlink: ensure bin/python exists and points to correct Python
    fixPyvenvCfgPlaceholder(pyvenvCfgPath);
    fixVenvScriptShebangs(userBackendVenv);
    ensureVenvPythonSymlink(userBackendVenv);

    if (process.platform === 'darwin') {
      try {
        execSync(`xattr -cr "${userBackendVenv}"`, { stdio: 'ignore' });
      } catch {
        // ignore
      }
    }

    fs.writeFileSync(versionFile, version, 'utf-8');
    log.info(`[VENV] Backend venv copied successfully`);

    runBackgroundUvSyncForOptionalDeps(userBackendVenv);
  } catch (error) {
    log.error(`[VENV] Failed to copy backend venv: ${error}`);
  }
}

/**
 * Copy prebuilt terminal venv to ~/.eigent/venvs/terminal_base-{version}.
 * @param version App version (used for version-specific venv directory)
 */
export function ensureTerminalVenvAtUserPath(version: string): void {
  if (!app.isPackaged) return;

  const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
  const prebuiltTerminalVenv = path.join(prebuiltDir, 'terminal_venv');
  const prebuiltUvPython = path.join(prebuiltDir, 'uv_python');

  if (!fs.existsSync(prebuiltTerminalVenv)) return;
  const installedMarker = path.join(
    prebuiltTerminalVenv,
    '.packages_installed'
  );
  if (!fs.existsSync(installedMarker)) return;

  const userVenvsDir = path.join(os.homedir(), '.eigent', 'venvs');
  const userTerminalVenv = path.join(userVenvsDir, `terminal_base-${version}`);
  const userVenvMarker = path.join(userTerminalVenv, '.packages_installed');
  const versionFile = path.join(userVenvsDir, TERMINAL_VENV_VERSION_FILE);

  // Ensure uv_python symlink exists (needed even if venv already copied)
  const userUvPython = path.join(os.homedir(), '.eigent', 'uv_python');
  if (!fs.existsSync(userUvPython) && fs.existsSync(prebuiltUvPython)) {
    try {
      fs.mkdirSync(path.dirname(userUvPython), { recursive: true });
      fs.symlinkSync(prebuiltUvPython, userUvPython);
      log.info(`[VENV] Created uv_python symlink: ${userUvPython}`);
    } catch (e) {
      log.warn(`[VENV] Failed to create uv_python symlink: ${e}`);
    }
  }

  if (fs.existsSync(userVenvMarker)) {
    const storedVersion = fs.existsSync(versionFile)
      ? fs.readFileSync(versionFile, 'utf-8').trim()
      : null;
    if (storedVersion === version) {
      log.info(
        `[VENV] Terminal venv already at ${userTerminalVenv} (v${version})`
      );
      return;
    }
  }

  log.info(`[VENV] Copying prebuilt terminal venv to ${userTerminalVenv}...`);

  try {
    fs.mkdirSync(userVenvsDir, { recursive: true });

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

    fs.cpSync(prebuiltTerminalVenv, userTerminalVenv, {
      recursive: true,
      verbatimSymlinks: true,
    });

    // Fix paths after copying (source venv paths don't match user venv location)
    // - pyvenv.cfg: update home path to point to correct Python location
    // - shebangs: update #! paths in bin/* scripts to point to correct Python
    // - python symlink: ensure bin/python exists and points to correct Python
    fixPyvenvCfgPlaceholder(path.join(userTerminalVenv, 'pyvenv.cfg'));
    fixVenvScriptShebangs(userTerminalVenv);
    ensureVenvPythonSymlink(userTerminalVenv);

    if (process.platform === 'darwin') {
      try {
        execSync(`xattr -cr "${userTerminalVenv}"`, { stdio: 'ignore' });
      } catch {
        // ignore
      }
    }

    fs.writeFileSync(versionFile, version, 'utf-8');
    log.info(`[VENV] Terminal venv copied successfully`);
  } catch (error) {
    log.error(`[VENV] Failed to copy terminal venv: ${error}`);
  }
}

/**
 * Get path to prebuilt terminal venv (if available in packaged app)
 */
export function getPrebuiltTerminalVenvPath(): string | null {
  if (!app.isPackaged) {
    return null;
  }

  const prebuiltTerminalVenvPath = path.join(
    process.resourcesPath,
    'prebuilt',
    'terminal_venv'
  );
  if (!fs.existsSync(prebuiltTerminalVenvPath)) {
    return null;
  }

  const pyvenvCfgPath = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg');
  const installedMarker = path.join(
    prebuiltTerminalVenvPath,
    '.packages_installed'
  );
  if (!fs.existsSync(pyvenvCfgPath) || !fs.existsSync(installedMarker)) {
    return null;
  }

  // Check if already fixed for this version (avoid repeated fixes)
  const fixedMarkerPath = path.join(
    process.resourcesPath,
    'prebuilt',
    '.terminal_venv_fixed'
  );
  const currentVersion = app.getVersion();
  const needsFix =
    !fs.existsSync(fixedMarkerPath) ||
    fs.readFileSync(fixedMarkerPath, 'utf-8').trim() !== currentVersion;

  if (needsFix) {
    fixPyvenvCfgPlaceholder(pyvenvCfgPath);
    ensureVenvPythonSymlink(prebuiltTerminalVenvPath);
    fixVenvScriptShebangs(prebuiltTerminalVenvPath);
    fs.writeFileSync(fixedMarkerPath, currentVersion, 'utf-8');
  }

  const pythonExePath = getVenvPythonPath(prebuiltTerminalVenvPath);

  if (fs.existsSync(pythonExePath)) {
    return prebuiltTerminalVenvPath;
  }

  // Try to fix the missing Python executable by creating a symlink to prebuilt Python
  const prebuiltPython = findPythonForTerminalVenv();
  if (prebuiltPython && fs.existsSync(prebuiltPython)) {
    try {
      const binDir = path.join(
        prebuiltTerminalVenvPath,
        process.platform === 'win32' ? 'Scripts' : 'bin'
      );

      if (!fs.existsSync(binDir)) {
        fs.mkdirSync(binDir, { recursive: true });
      }

      if (fs.existsSync(pythonExePath)) {
        fs.unlinkSync(pythonExePath);
      }

      const relativePath = path.relative(binDir, prebuiltPython);
      fs.symlinkSync(relativePath, pythonExePath);
      log.info(
        `[VENV] Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}`
      );
      return prebuiltTerminalVenvPath;
    } catch (error) {
      log.warn(`[VENV] Failed to fix terminal venv Python symlink: ${error}`);
    }
  }

  log.warn(
    `[VENV] Prebuilt terminal venv Python missing, falling back to user venv`
  );
  return null;
}

/**
 * Get the Python executable path from a venv directory.
 * Use this to directly invoke venv's python, avoiding uv/launcher placeholder issues.
 */
export function getVenvPythonPath(venvPath: string): string {
  const isWindows = process.platform === 'win32';
  return isWindows
    ? path.join(venvPath, 'Scripts', 'python.exe')
    : path.join(venvPath, 'bin', 'python');
}

/**
 * Check venv existence for pre-check WITHOUT triggering extraction.
 * Used to avoid blocking app launch - extraction is deferred to startBackend when window is already visible.
 */
export function checkVenvExistsForPreCheck(version: string): {
  exists: boolean;
  path: string;
} {
  if (!app.isPackaged) {
    const venvDir = path.join(
      os.homedir(),
      '.eigent',
      'venvs',
      `backend-${version}`
    );
    const pyvenvCfg = path.join(venvDir, 'pyvenv.cfg');
    return {
      exists: fs.existsSync(pyvenvCfg),
      path: venvDir,
    };
  }

  const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
  const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
  const prebuiltPyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');

  if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(prebuiltPyvenvCfg)) {
    return { exists: true, path: prebuiltVenvPath };
  }

  const venvDir = path.join(
    os.homedir(),
    '.eigent',
    'venvs',
    `backend-${version}`
  );
  const pyvenvCfg = path.join(venvDir, 'pyvenv.cfg');
  return {
    exists: fs.existsSync(pyvenvCfg),
    path: venvDir,
  };
}

/**
 * Get path to backend venv for the given version.
 * @param version App version
 * @returns Path to backend venv
 */
export function getVenvPath(version: string): string {
  // For packaged apps, ensure venv is copied to ~/.eigent/venvs first
  if (app.isPackaged) {
    ensureBackendVenvAtUserPath(version);

    // Check if user venv exists (after ensuring copy)
    const userVenvDir = path.join(
      os.homedir(),
      '.eigent',
      'venvs',
      `backend-${version}`
    );
    const pyvenvCfgPath = path.join(userVenvDir, 'pyvenv.cfg');
    if (fs.existsSync(pyvenvCfgPath)) {
      return userVenvDir;
    }

    // Fallback to prebuilt venv if copy failed (shouldn't happen normally)
    const prebuiltVenv = getPrebuiltVenvPath();
    if (prebuiltVenv) {
      return prebuiltVenv;
    }
  }

  const venvDir = path.join(
    os.homedir(),
    '.eigent',
    'venvs',
    `backend-${version}`
  );

  // Ensure venvs directory exists (parent of the actual venv)
  const venvsBaseDir = path.dirname(venvDir);
  if (!fs.existsSync(venvsBaseDir)) {
    fs.mkdirSync(venvsBaseDir, { recursive: true });
  }

  return venvDir;
}

/**
 * Create npm/npx wrapper scripts that use nodejs_wheel Python API.
 * The bin/npm from nodejs_wheel can fail with "Cannot find module '../lib/cli.js'"
 * when invoked directly. Using the Python API avoids this.
 */
export function ensureNpmWrappersForBrowserToolkit(
  venvPath: string
): string | null {
  const pythonPath = getVenvPythonPath(venvPath);
  if (!fs.existsSync(pythonPath)) return null;

  const eigentBinDir = path.join(os.homedir(), '.eigent', 'bin');
  fs.mkdirSync(eigentBinDir, { recursive: true });

  // Store wrapper target so wrappers are recreated when venv path changes (e.g. app upgrade)
  const wrapperVersion = `wrapper:${pythonPath}`;
  const versionFile = path.join(eigentBinDir, '.npm_wrapper_version');
  const storedVersion = fs.existsSync(versionFile)
    ? fs.readFileSync(versionFile, 'utf-8').trim()
    : '';

  const npmWrapper = path.join(
    eigentBinDir,
    process.platform === 'win32' ? 'npm.cmd' : 'npm'
  );
  const npxWrapper = path.join(
    eigentBinDir,
    process.platform === 'win32' ? 'npx.cmd' : 'npx'
  );

  // Recreate wrappers when: version changed, wrappers missing, or existing shebang points to wrong Python
  const needsUpdate =
    storedVersion !== wrapperVersion ||
    !fs.existsSync(npmWrapper) ||
    !fs.existsSync(npxWrapper) ||
    (process.platform !== 'win32' &&
      fs.existsSync(npmWrapper) &&
      !fs.readFileSync(npmWrapper, 'utf-8').startsWith(`#!${pythonPath}`));

  if (needsUpdate) {
    try {
      if (process.platform === 'win32') {
        const npmContent = `@echo off
"${pythonPath.replace(/\//g, '\\')}" -c "import sys; from nodejs_wheel import npm; sys.exit(npm(sys.argv[1:]))" %*
`;
        const npxContent = `@echo off
"${pythonPath.replace(/\//g, '\\')}" -c "import sys; from nodejs_wheel import npx; sys.exit(npx(sys.argv[1:]))" %*
`;
        fs.writeFileSync(npmWrapper, npmContent, 'utf-8');
        fs.writeFileSync(npxWrapper, npxContent, 'utf-8');
      } else {
        const shebang = `#!${pythonPath}\n`;
        const npmContent =
          shebang +
          `import sys
from nodejs_wheel import npm
sys.exit(npm(sys.argv[1:]))
`;
        const npxContent =
          shebang +
          `import sys
from nodejs_wheel import npx
sys.exit(npx(sys.argv[1:]))
`;
        fs.writeFileSync(npmWrapper, npmContent, 'utf-8');
        fs.writeFileSync(npxWrapper, npxContent, 'utf-8');
        fs.chmodSync(npmWrapper, 0o755);
        fs.chmodSync(npxWrapper, 0o755);
      }
      fs.writeFileSync(versionFile, wrapperVersion, 'utf-8');
      log.info(`[VENV] Created npm/npx wrappers at ${eigentBinDir}`);
    } catch (error) {
      log.warn(`[VENV] Failed to create npm wrappers: ${error}`);
      return null;
    }
  }

  return eigentBinDir;
}

/**
 * Find nodejs-wheel npm path in venv for browser toolkit.
 * Prefer Python API wrappers over direct bin (which can fail with cli.js error).
 */
export function findNodejsWheelNpmPath(venvPath: string): string | null {
  // Prefer wrapper scripts that use Python API (avoids bin/npm "../lib/cli.js" error)
  const wrapperDir = ensureNpmWrappersForBrowserToolkit(venvPath);
  if (wrapperDir) {
    const npmWrapper = path.join(
      wrapperDir,
      process.platform === 'win32' ? 'npm.cmd' : 'npm'
    );
    const npxWrapper = path.join(
      wrapperDir,
      process.platform === 'win32' ? 'npx.cmd' : 'npx'
    );
    if (fs.existsSync(npmWrapper) && fs.existsSync(npxWrapper)) {
      return wrapperDir;
    }
  }

  // Fallback to nodejs_wheel/bin (may fail with cli.js error)
  return findNodejsWheelBinPath(venvPath);
}

/**
 * Find nodejs_wheel/bin directory for the node executable.
 * Browser toolkit needs node in PATH (npm/npx use our wrappers from ~/.eigent/bin).
 */
export function findNodejsWheelBinPath(venvPath: string): string | null {
  try {
    const libPath = path.join(venvPath, 'lib');
    if (!fs.existsSync(libPath)) return null;

    const pythonDirs = fs
      .readdirSync(libPath)
      .filter((n) => n.startsWith('python'));
    if (pythonDirs.length === 0) return null;

    for (const pythonDir of pythonDirs) {
      const sitePackages = path.join(libPath, pythonDir, 'site-packages');
      const nodejsWheelBin = path.join(sitePackages, 'nodejs_wheel', 'bin');
      const nodePath = path.join(
        nodejsWheelBin,
        process.platform === 'win32' ? 'node.exe' : 'node'
      );

      if (fs.existsSync(nodePath)) {
        return nodejsWheelBin;
      }
    }
  } catch {
    // ignore
  }
  return null;
}

export function getVenvsBaseDir(): string {
  return path.join(os.homedir(), '.eigent', 'venvs');
}

/**
 * Packages to install in the terminal base venv.
 * These are commonly used packages for terminal tasks (data processing, visualization, etc.)
 * Keep this list minimal - users can install additional packages as needed.
 */
export const TERMINAL_BASE_PACKAGES = [
  'pandas',
  'numpy',
  'matplotlib',
  'requests',
  'openpyxl',
  'beautifulsoup4',
  'pillow',
  'plotly',
];

/**
 * Get path to the terminal base venv.
 * This is a lightweight venv with common packages for terminal tasks,
 * separate from the backend venv.
 */
export function getTerminalVenvPath(version: string): string {
  // First check for prebuilt terminal venv in packaged app
  if (app.isPackaged) {
    const prebuiltTerminalVenv = getPrebuiltTerminalVenvPath();
    if (prebuiltTerminalVenv) {
      return prebuiltTerminalVenv;
    }
  }

  const venvDir = path.join(
    os.homedir(),
    '.eigent',
    'venvs',
    `terminal_base-${version}`
  );

  // Ensure venvs directory exists
  const venvsBaseDir = path.dirname(venvDir);
  if (!fs.existsSync(venvsBaseDir)) {
    fs.mkdirSync(venvsBaseDir, { recursive: true });
  }

  return venvDir;
}

export async function cleanupOldVenvs(currentVersion: string): Promise<void> {
  const venvsBaseDir = getVenvsBaseDir();

  // Check if venvs directory exists
  if (!fs.existsSync(venvsBaseDir)) {
    return;
  }

  // Patterns to match: backend-{version} and terminal_base-{version}
  const venvPatterns = [
    { prefix: 'backend-', regex: /^backend-(.+)$/ },
    { prefix: 'terminal_base-', regex: /^terminal_base-(.+)$/ },
  ];

  try {
    const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true });

    for (const entry of entries) {
      if (!entry.isDirectory()) continue;

      for (const pattern of venvPatterns) {
        if (entry.name.startsWith(pattern.prefix)) {
          const versionMatch = entry.name.match(pattern.regex);
          if (versionMatch && versionMatch[1] !== currentVersion) {
            const oldVenvPath = path.join(venvsBaseDir, entry.name);
            console.log(`Cleaning up old venv: ${oldVenvPath}`);

            try {
              // Remove old venv directory recursively
              fs.rmSync(oldVenvPath, { recursive: true, force: true });
              console.log(`Successfully removed old venv: ${entry.name}`);
            } catch (err) {
              console.error(`Failed to remove old venv ${entry.name}:`, err);
            }
          }
          break; // Found matching pattern, no need to check others
        }
      }
    }
  } catch (err) {
    console.error('Error during venv cleanup:', err);
  }
}

export async function isBinaryExists(name: string): Promise<boolean> {
  const cmd = await getBinaryPath(name);

  return fs.existsSync(cmd);
}

/**
 * Get path to prebuilt Python installation (if available in packaged app)
 */
export function getPrebuiltPythonDir(): string | null {
  if (!app.isPackaged) {
    return null;
  }

  const prebuiltPythonDir = path.join(
    process.resourcesPath,
    'prebuilt',
    'uv_python'
  );
  if (fs.existsSync(prebuiltPythonDir)) {
    log.info(`[VENV] Using prebuilt Python: ${prebuiltPythonDir}`);
    return prebuiltPythonDir;
  }

  return null;
}

/**
 * Get unified UV environment variables for consistent Python environment management.
 * This ensures both installation and runtime use the same paths.
 * @param version - The app version for venv path
 * @returns Environment variables for UV commands
 */
export function getUvEnv(version: string): Record<string, string> {
  // Use prebuilt Python if available (packaged app)
  const prebuiltPython = getPrebuiltPythonDir();
  const pythonInstallDir = prebuiltPython || getCachePath('uv_python');

  return {
    UV_PYTHON_INSTALL_DIR: pythonInstallDir,
    UV_TOOL_DIR: getCachePath('uv_tool'),
    UV_PROJECT_ENVIRONMENT: getVenvPath(version),
    UV_HTTP_TIMEOUT: '300',
  };
}

export async function killProcessByName(name: string): Promise<void> {
  const platform = process.platform;
  try {
    if (platform === 'win32') {
      await new Promise<void>((resolve, reject) => {
        // /F = force, /IM = image name
        const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]);
        cmd.on('close', (code) => {
          // code 0 = success, code 128 = process not found (which is fine)
          if (code === 0 || code === 128) resolve();
          else reject(new Error(`taskkill exited with code ${code}`));
        });
        cmd.on('error', reject);
      });
    } else {
      await new Promise<void>((resolve, reject) => {
        const cmd = spawn('pkill', ['-9', name]);
        cmd.on('close', (code) => {
          // code 0 = success, code 1 = no process found (which is fine)
          if (code === 0 || code === 1) resolve();
          else reject(new Error(`pkill exited with code ${code}`));
        });
        cmd.on('error', reject);
      });
    }
  } catch (err) {
    // Ignore errors, just best effort
    log.warn(`Failed to kill process ${name}:`, err);
  }
}