Ctrl K
ComunidadGuías…

electron/main/install-deps.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 { spawn } from 'child_process';
import { app, BrowserWindow } from 'electron';
import log from 'electron-log';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
  cleanupOldVenvs,
  getBackendPath,
  getBinaryPath,
  getCachePath,
  getPrebuiltPythonDir,
  getPrebuiltTerminalVenvPath,
  getTerminalVenvPath,
  getUvEnv,
  getVenvPath,
  getVenvPythonPath,
  isBinaryExists,
  runInstallScript,
  TERMINAL_BASE_PACKAGES,
} from './utils/process';
import { safeMainWindowSend } from './utils/safeWebContentsSend';

const userData = app.getPath('userData');
const versionFile = path.join(userData, 'version.txt');

export type PromiseReturnType = {
  message: string;
  success: boolean;
};

interface checkInstallProps {
  win: BrowserWindow | null;
  forceInstall?: boolean;
}
// Read last run version and install dependencies on update
export const checkAndInstallDepsOnUpdate = async ({
  win,
  forceInstall = false,
}: checkInstallProps): Promise<PromiseReturnType> => {
  const currentVersion = app.getVersion();
  let savedVersion = '';

  // Check if prebuilt dependencies are available
  const hasPrebuiltDeps = (): boolean => {
    if (!app.isPackaged) {
      return false;
    }
    const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
    const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
    const prebuiltVenvDir = path.join(prebuiltDir, 'venv');
    const uvPath = path.join(
      prebuiltBinDir,
      process.platform === 'win32' ? 'uv.exe' : 'uv'
    );
    const bunPath = path.join(
      prebuiltBinDir,
      process.platform === 'win32' ? 'bun.exe' : 'bun'
    );
    const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg');

    const hasBinaries = fs.existsSync(uvPath) && fs.existsSync(bunPath);
    const hasVenv = fs.existsSync(pyvenvCfg);

    if (hasBinaries && hasVenv) {
      log.info(
        '[DEPS INSTALL] Prebuilt dependencies found, skipping installation'
      );
      return true;
    }
    return false;
  };

  const checkInstallOperations = {
    getSavedVersion: (): boolean => {
      // Check if version file exists
      const versionExists = fs.existsSync(versionFile);
      if (versionExists) {
        log.info('[DEPS INSTALL] start check version', { currentVersion });
        savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
        log.info('[DEPS INSTALL] read saved version', { savedVersion });
      } else {
        log.info('[DEPS INSTALL] version file not exist, will create new file');
      }
      return versionExists;
    },
    handleUpdateNotification: (versionExists: boolean) => {
      if (win && !win.isDestroyed()) {
        win.webContents.send('update-notification', {
          type: 'version-update',
          currentVersion,
          previousVersion: versionExists ? savedVersion : 'none',
          reason: !versionExists
            ? 'version file not exist'
            : 'version not match',
        });
      } else {
        log.warn(
          '[DEPS INSTALL] Cannot send update notification - window not available'
        );
      }
    },
    createVersionFile: () => {
      fs.writeFileSync(versionFile, currentVersion);
      log.info('[DEPS INSTALL] version file updated', { currentVersion });
    },
  };

  return new Promise(async (resolve, _reject) => {
    try {
      // If prebuilt dependencies are available, use them and skip main installation
      if (hasPrebuiltDeps()) {
        log.info(
          '[DEPS INSTALL] Using prebuilt dependencies, creating version file'
        );
        checkInstallOperations.createVersionFile();

        // Check if prebuilt terminal venv exists
        const prebuiltTerminalVenv = getPrebuiltTerminalVenvPath();
        if (prebuiltTerminalVenv) {
          log.info(
            '[DEPS INSTALL] Using prebuilt terminal venv:',
            prebuiltTerminalVenv
          );
        } else {
          // Create terminal base venv if not prebuilt
          log.info(
            '[DEPS INSTALL] Creating terminal base venv (not prebuilt)...'
          );
          try {
            uv_path = await getBinaryPath('uv');
            const terminalResult =
              await installTerminalBaseVenv(currentVersion);
            if (!terminalResult.success) {
              log.warn(
                '[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
                terminalResult.message
              );
            } else {
              log.info(
                '[DEPS INSTALL] Terminal base venv created successfully'
              );
            }
          } catch (error) {
            log.warn(
              '[DEPS INSTALL] Failed to create terminal base venv:',
              error
            );
          }
        }

        resolve({ message: 'Using prebuilt dependencies', success: true });
        return;
      }

      // Clean up cache in production environment BEFORE any checks
      // This ensures users always get fresh dependencies in production
      if (app.isPackaged) {
        log.info(
          '[CACHE CLEANUP] Production environment detected, cleaning cache before dependency check...'
        );
        cleanupCacheInProduction();
      }

      const versionExists: boolean = checkInstallOperations.getSavedVersion();

      // Check if command tools are installed
      const uvExists = await isBinaryExists('uv');
      const bunExists = await isBinaryExists('bun');
      const toolsMissing = !uvExists || !bunExists;

      // If version file does not exist or version does not match, reinstall dependencies
      // Or if command tools are missing, need to install them
      if (
        forceInstall ||
        !versionExists ||
        savedVersion !== currentVersion ||
        toolsMissing
      ) {
        if (toolsMissing) {
          log.info(
            '[DEPS INSTALL] Command tools missing, starting installation...',
            {
              uvExists,
              bunExists,
            }
          );
        } else {
          log.info(
            '[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...',
            {
              currentVersion,
              savedVersion: versionExists ? savedVersion : 'none',
              reason: !versionExists
                ? 'version file not exist'
                : 'version not match',
            }
          );
        }

        // Notify frontend to update
        checkInstallOperations.handleUpdateNotification(versionExists);

        // Install dependencies (version.txt will be updated AFTER successful install)
        const result = await installDependencies(currentVersion);
        if (!result.success) {
          log.error(' install dependencies failed');
          resolve({
            message: `Install dependencies failed, msg ${result.message}`,
            success: false,
          });
          return;
        }

        // Update version file ONLY after successful installation
        checkInstallOperations.createVersionFile();

        resolve({
          message: 'Dependencies installed successfully after update',
          success: true,
        });
        log.info('[DEPS INSTALL] install dependencies complete');
        return;
      } else {
        log.info(
          '[DEPS INSTALL] version not changed and tools installed, skip install dependencies',
          { currentVersion }
        );
        resolve({
          message:
            'Version not changed and tools installed, skipped installation',
          success: true,
        });
        return;
      }
    } catch (error) {
      log.error(' check version and install dependencies error:', error);
      resolve({ message: `Error checking version: ${error}`, success: false });
      return;
    }
  });
};

/**
 * Check if command line tools are installed, install if not
 */
export async function installCommandTool(): Promise<PromiseReturnType> {
  try {
    const ensureInstalled = async (
      toolName: 'uv' | 'bun',
      scriptName: string
    ): Promise<PromiseReturnType> => {
      if (await isBinaryExists(toolName)) {
        return { message: `${toolName} already installed`, success: true };
      }

      console.log(`start install ${toolName}`);
      try {
        await runInstallScript(scriptName);
        const installed = await isBinaryExists(toolName);

        if (installed) {
          safeMainWindowSend('install-dependencies-log', {
            type: 'stdout',
            data: `${toolName} installed successfully`,
          });
          return {
            message: `${toolName} installed successfully`,
            success: true,
          };
        } else {
          const errorMsg = `${toolName} installation failed: binary not found after installation`;
          safeMainWindowSend('install-dependencies-complete', {
            success: false,
            code: 2,
            error: errorMsg,
          });
          return {
            message: errorMsg,
            success: false,
          };
        }
      } catch (scriptError) {
        const errorMsg = `${toolName} installation failed: ${
          scriptError instanceof Error
            ? scriptError.message
            : String(scriptError)
        }`;
        safeMainWindowSend('install-dependencies-complete', {
          success: false,
          code: 2,
          error: errorMsg,
        });
        return {
          message: errorMsg,
          success: false,
        };
      }
    };

    const uvResult = await ensureInstalled('uv', 'install-uv.js');
    if (!uvResult.success) {
      return { message: uvResult.message, success: false };
    }

    const bunResult = await ensureInstalled('bun', 'install-bun.js');
    if (!bunResult.success) {
      return { message: bunResult.message, success: false };
    }

    return { message: 'Command tools installed successfully', success: true };
  } catch (error) {
    const errorMessage = `Command tool installation failed: ${error}`;
    log.error(
      '[DEPS INSTALL] Exception during command tool installation:',
      error
    );
    safeMainWindowSend('install-dependencies-complete', {
      success: false,
      code: 2,
      error: errorMessage,
    });
    return { message: errorMessage, success: false };
  }
}

let uv_path: string;
const backendPath = getBackendPath();

// Ensure backend directory exists
if (!fs.existsSync(backendPath)) {
  log.info(`Creating backend directory: ${backendPath}`);
  fs.mkdirSync(backendPath, { recursive: true });
}

const installingLockPath = path.join(backendPath, 'uv_installing.lock');
const installedLockPath = path.join(backendPath, 'uv_installed.lock');
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
const proxyArgs = [
  '--default-index',
  'https://mirrors.aliyun.com/pypi/simple/',
];

/**
 * Get current installation status by checking lock files
 * @returns Object with installation status information
 */
export async function getInstallationStatus(): Promise<{
  isInstalling: boolean;
  hasLockFile: boolean;
  installedExists: boolean;
}> {
  try {
    const installingExists = fs.existsSync(installingLockPath);
    const installedExists = fs.existsSync(installedLockPath);

    // If installing lock exists, installation is in progress
    // If installed lock exists, installation completed previously
    return {
      isInstalling: installingExists,
      hasLockFile: installingExists || installedExists,
      installedExists: installedExists,
    };
  } catch (error) {
    console.error(
      '[getInstallationStatus] Error checking installation status:',
      error
    );
    return {
      isInstalling: false,
      hasLockFile: false,
      installedExists: false,
    };
  }
}

class InstallLogs {
  private node_process;
  private version: string;

  constructor(extraArgs: string[], version: string) {
    console.log('start install dependencies', extraArgs, 'version:', version);
    this.version = version;

    this.node_process = spawn(
      uv_path,
      [
        'sync',
        '--no-dev',
        '--cache-dir',
        getCachePath('uv_cache'),
        ...extraArgs,
      ],
      {
        cwd: backendPath,
        env: {
          ...process.env,
          ...getUvEnv(version),
        },
      }
    );
  }

  /**Display filtered logs based on severity */
  displayFilteredLogs(data: String) {
    if (!data) return;
    const msg = data.toString().trimEnd();
    if (
      msg.toLowerCase().includes('error') ||
      msg.toLowerCase().includes('traceback')
    ) {
      log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
      safeMainWindowSend('install-dependencies-log', {
        type: 'stderr',
        data: data.toString(),
      });
    } else {
      log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
      safeMainWindowSend('install-dependencies-log', {
        type: 'stdout',
        data: data.toString(),
      });
    }
  }

  /**Handle stdout data */
  onStdout() {
    this.node_process.stdout.on('data', (data: any) => {
      this.displayFilteredLogs(data);
    });
  }

  /**Handle stderr data */
  onStderr() {
    this.node_process.stderr.on('data', (data: any) => {
      this.displayFilteredLogs(data);
    });
  }

  /**Handle process close event */
  onClose(resolveInner: (code: number | null) => void) {
    this.node_process.on('close', resolveInner);
  }

  /**
   * Set installing Lock Path
   * Creates uv_installing.lock file to indicate installation in progress
   * Creates backend directory if not exists
   */
  static setLockPath() {
    if (!fs.existsSync(backendPath)) {
      fs.mkdirSync(backendPath, { recursive: true });
    }
    fs.writeFileSync(installingLockPath, '');
  }

  /**Clean installing Lock Path */
  static cleanLockPath() {
    if (fs.existsSync(installingLockPath)) {
      fs.unlinkSync(installingLockPath);
    }
  }
}

/**
 * Clean up cache directory
 * This ensures users get fresh dependencies
 * Note: Only call this in production environment (caller should check app.isPackaged)
 */
function cleanupCacheInProduction(): void {
  try {
    const cacheBaseDir = path.join(os.homedir(), '.eigent', 'cache');

    if (!fs.existsSync(cacheBaseDir)) {
      log.info(
        '[CACHE CLEANUP] Cache directory does not exist, nothing to clean'
      );
      return;
    }

    log.info('[CACHE CLEANUP] Cleaning cache directory:', cacheBaseDir);

    fs.rmSync(cacheBaseDir, { recursive: true, force: true });

    log.info('[CACHE CLEANUP] Cache directory cleaned successfully');

    fs.mkdirSync(cacheBaseDir, { recursive: true });
    log.info('[CACHE CLEANUP] Empty cache directory recreated');
  } catch (error) {
    log.error('[CACHE CLEANUP] Failed to clean cache directory:', error);
  }
}

const runInstall = (extraArgs: string[], version: string) => {
  const installLogs = new InstallLogs(extraArgs, version);
  return new Promise<PromiseReturnType>((resolveInner, rejectInner) => {
    try {
      installLogs.onStdout();
      installLogs.onStderr();
      installLogs.onClose((code) => {
        console.log('install dependencies end', code === 0);
        InstallLogs.cleanLockPath();
        resolveInner({
          message:
            code === 0
              ? 'Installation completed successfully'
              : `Installation failed with code ${code}`,
          success: code === 0,
        });
      });
    } catch (err) {
      log.error('run install failed', err);
      // Clean up uv_installing.lock file if installation fails
      InstallLogs.cleanLockPath();
      rejectInner({ message: `Installation failed: ${err}`, success: false });
    }
  });
};

/**
 * Find Python executable in prebuilt Python directory
 * UV stores Python installations in directories like: cpython-3.11.x+.../install/bin/python
 */
function findPrebuiltPythonExecutable(): 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('[DEPS INSTALL] Error searching for prebuilt Python:', error);
  }

  for (const pythonPath of possiblePaths) {
    if (fs.existsSync(pythonPath)) {
      log.info(
        `[DEPS INSTALL] Found prebuilt Python executable: ${pythonPath}`
      );
      return pythonPath;
    }
  }

  log.info(
    '[DEPS INSTALL] Prebuilt Python directory found but executable not found, will use UV_PYTHON_INSTALL_DIR'
  );
  return null;
}

/**
 * Install terminal base venv with common packages for terminal tasks.
 * This is a lightweight venv separate from the backend venv.
 */
async function installTerminalBaseVenv(
  version: string
): Promise<PromiseReturnType> {
  const terminalVenvPath = getTerminalVenvPath(version);
  const pythonPath =
    process.platform === 'win32'
      ? path.join(terminalVenvPath, 'Scripts', 'python.exe')
      : path.join(terminalVenvPath, 'bin', 'python');
  // Marker file to indicate packages were installed successfully
  const installedMarker = path.join(terminalVenvPath, '.packages_installed');

  // Check if terminal base venv already exists and packages are installed
  if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) {
    log.info(
      '[DEPS INSTALL] Terminal base venv already exists with packages, skipping creation'
    );
    return { message: 'Terminal base venv already exists', success: true };
  }

  // If python exists but marker doesn't, packages may not be installed - need to reinstall
  const needsPackageInstall =
    fs.existsSync(pythonPath) && !fs.existsSync(installedMarker);

  if (needsPackageInstall) {
    log.info(
      '[DEPS INSTALL] Terminal venv exists but packages not installed, installing packages...'
    );
  } else {
    log.info('[DEPS INSTALL] Creating terminal base venv...');
  }
  safeMainWindowSend('install-dependencies-log', {
    type: 'stdout',
    data: needsPackageInstall
      ? 'Installing missing packages in terminal environment...\n'
      : 'Creating terminal base environment...\n',
  });

  try {
    // Get UV environment variables (includes prebuilt Python if available)
    const uvEnv = getUvEnv(version);

    // Create the venv using uv (skip if only need package install)
    if (!needsPackageInstall) {
      // Try to use prebuilt Python directly if available
      const prebuiltPython = findPrebuiltPythonExecutable();
      const venvArgs = prebuiltPython
        ? ['venv', '--python', prebuiltPython, terminalVenvPath]
        : ['venv', '--python', '3.11', terminalVenvPath];

      await new Promise<void>((resolve, reject) => {
        const createVenv = spawn(uv_path, venvArgs, {
          env: {
            ...process.env,
            ...uvEnv,
          },
        });

        createVenv.stdout.on('data', (data) => {
          log.info(`[DEPS INSTALL] terminal venv: ${data}`);
        });

        createVenv.stderr.on('data', (data) => {
          log.info(`[DEPS INSTALL] terminal venv: ${data}`);
        });

        createVenv.on('close', (code) => {
          if (code === 0) {
            resolve();
          } else {
            reject(
              new Error(`Failed to create terminal venv, exit code: ${code}`)
            );
          }
        });

        createVenv.on('error', reject);
      });
    }

    // Install base packages
    log.info('[DEPS INSTALL] Installing terminal base packages...');
    safeMainWindowSend('install-dependencies-log', {
      type: 'stdout',
      data: `Installing packages: ${TERMINAL_BASE_PACKAGES.join(', ')}...\n`,
    });

    await new Promise<void>((resolve, reject) => {
      const installPkgs = spawn(
        uv_path,
        ['pip', 'install', '--python', pythonPath, ...TERMINAL_BASE_PACKAGES],
        {
          env: {
            ...process.env,
            ...uvEnv,
          },
        }
      );

      installPkgs.stdout.on('data', (data) => {
        log.info(`[DEPS INSTALL] terminal packages: ${data}`);
        safeMainWindowSend('install-dependencies-log', {
          type: 'stdout',
          data: data.toString(),
        });
      });

      installPkgs.stderr.on('data', (data) => {
        log.info(`[DEPS INSTALL] terminal packages: ${data}`);
        safeMainWindowSend('install-dependencies-log', {
          type: 'stdout',
          data: data.toString(),
        });
      });

      installPkgs.on('close', (code) => {
        if (code === 0) {
          resolve();
        } else {
          reject(
            new Error(`Failed to install terminal packages, exit code: ${code}`)
          );
        }
      });

      installPkgs.on('error', reject);
    });

    // Create marker file to indicate successful installation
    fs.writeFileSync(installedMarker, new Date().toISOString());
    log.info('[DEPS INSTALL] Terminal base venv created successfully');
    return {
      message: 'Terminal base venv created successfully',
      success: true,
    };
  } catch (error) {
    log.error('[DEPS INSTALL] Failed to create terminal base venv:', error);
    return {
      message: `Failed to create terminal base venv: ${error}`,
      success: false,
    };
  }
}

export async function installDependencies(
  version: string
): Promise<PromiseReturnType> {
  uv_path = await getBinaryPath('uv');
  const venvPath = getVenvPath(version);

  const handleInstallOperations = {
    spawnBabel: (message: 'mirror' | 'main' = 'main') => {
      fs.writeFileSync(installedLockPath, '');
      log.info('[DEPS INSTALL] Script completed successfully');
      console.log(
        `Install Dependencies completed ${message} for version ${version}`
      );
      console.log(`Virtual environment path: ${venvPath}`);
      const pythonPath = getVenvPythonPath(venvPath);
      spawn(
        pythonPath,
        ['-m', 'babel.messages.frontend', 'compile', '-d', 'lang'],
        {
          cwd: backendPath,
          env: { ...process.env },
        }
      );
    },
    notifyInstallDependenciesPage: (): boolean => {
      const success = safeMainWindowSend('install-dependencies-start');
      if (!success) {
        log.warn(
          '[DEPS INSTALL] Main window not available, continuing installation without UI updates'
        );
      }
      return success;
    },
    installHybridBrowserDependencies: async (): Promise<boolean> => {
      try {
        // Find the hybrid_browser_toolkit ts directory in the virtual environment
        // Need to determine the Python version to construct the correct path
        let sitePackagesPath: string | null = null;
        const libPath = path.join(venvPath, 'lib');

        // Try to find the site-packages directory (it varies by Python version)
        if (fs.existsSync(libPath)) {
          const libContents = fs.readdirSync(libPath);
          const pythonDir = libContents.find((name) =>
            name.startsWith('python')
          );
          if (pythonDir) {
            sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
          }
        }

        if (!sitePackagesPath || !fs.existsSync(sitePackagesPath)) {
          log.warn(
            '[DEPS INSTALL] site-packages directory not found in venv, skipping npm install'
          );
          return true; // Not an error if the venv structure is different
        }

        const toolkitPath = path.join(
          sitePackagesPath,
          'camel',
          'toolkits',
          'hybrid_browser_toolkit',
          'ts'
        );

        if (!fs.existsSync(toolkitPath)) {
          log.warn(
            '[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' +
              toolkitPath +
              ', skipping npm install'
          );
          return true; // Not an error if the toolkit isn't installed
        }

        // Check if npm dependencies are already installed
        const npmMarkerPath = path.join(
          toolkitPath,
          '.npm_dependencies_installed'
        );
        const nodeModulesPath = path.join(toolkitPath, 'node_modules');
        const distPath = path.join(toolkitPath, 'dist');

        // Check if marker exists and verify version
        if (
          fs.existsSync(npmMarkerPath) &&
          fs.existsSync(nodeModulesPath) &&
          fs.existsSync(distPath)
        ) {
          try {
            const markerContent = JSON.parse(
              fs.readFileSync(npmMarkerPath, 'utf-8')
            );
            if (markerContent.version === version) {
              log.info(
                '[DEPS INSTALL] hybrid_browser_toolkit npm dependencies already installed for current version, skipping...'
              );
              return true;
            } else {
              log.info(
                '[DEPS INSTALL] npm dependencies installed for different version, will reinstall...'
              );
              // Clean up old installation
              fs.unlinkSync(npmMarkerPath);
            }
          } catch (error) {
            log.warn(
              '[DEPS INSTALL] Could not read npm marker file, will reinstall...',
              error
            );
            // If we can't read the marker, assume we need to reinstall
          }
        }

        log.info(
          '[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...'
        );
        safeMainWindowSend('install-dependencies-log', {
          type: 'stdout',
          data: 'Installing browser toolkit dependencies...\n',
        });

        // Try to find npm - first try system npm, then try uv run npm
        let npmCommand: string[];
        const testNpm = spawn('npm', ['--version'], { shell: true });
        const npmExists = await new Promise<boolean>((resolve) => {
          testNpm.on('close', (code) => resolve(code === 0));
          testNpm.on('error', () => resolve(false));
        });

        if (npmExists) {
          // Use system npm directly
          npmCommand = ['npm'];
          log.info('[DEPS INSTALL] Using system npm for installation');
        } else {
          // Try uv run npm (might not work if nodejs-wheel isn't properly set up)
          // Quote the path to handle spaces in username on Windows
          npmCommand = [`"${uv_path}"`, 'run', 'npm'];
          log.info('[DEPS INSTALL] Attempting to use uv run npm');
        }

        // Run npm install
        const npmCacheDir = path.join(venvPath, '.npm-cache');
        if (!fs.existsSync(npmCacheDir)) {
          fs.mkdirSync(npmCacheDir, { recursive: true });
        }

        const npmInstall = spawn(
          npmCommand[0],
          [...npmCommand.slice(1), 'install'],
          {
            cwd: toolkitPath,
            env: {
              ...process.env,
              UV_PROJECT_ENVIRONMENT: venvPath,
              npm_config_cache: npmCacheDir,
            },
            shell: true, // Important for Windows
          }
        );

        await new Promise<void>((resolve, reject) => {
          if (npmInstall.stdout) {
            npmInstall.stdout.on('data', (data) => {
              log.info(`[DEPS INSTALL] npm install: ${data}`);
              safeMainWindowSend('install-dependencies-log', {
                type: 'stdout',
                data: data.toString(),
              });
            });
          }

          if (npmInstall.stderr) {
            npmInstall.stderr.on('data', (data) => {
              log.warn(`[DEPS INSTALL] npm install stderr: ${data}`);
              safeMainWindowSend('install-dependencies-log', {
                type: 'stderr',
                data: data.toString(),
              });
            });
          }

          npmInstall.on('close', (code) => {
            if (code === 0) {
              log.info('[DEPS INSTALL] npm install completed successfully');
              resolve();
            } else {
              log.error(`[DEPS INSTALL] npm install failed with code ${code}`);
              reject(new Error(`npm install failed with code ${code}`));
            }
          });

          npmInstall.on('error', (err) => {
            log.error(`[DEPS INSTALL] npm install process error: ${err}`);
            reject(err);
          });
        });

        // Run npm build (use the same npm command as install)
        log.info(
          '[DEPS INSTALL] Building hybrid_browser_toolkit TypeScript...'
        );
        safeMainWindowSend('install-dependencies-log', {
          type: 'stdout',
          data: 'Building browser toolkit TypeScript...\n',
        });

        const buildArgs =
          npmCommand[0] === 'npm'
            ? ['run', 'build']
            : [...npmCommand.slice(1), 'run', 'build'];
        const npmBuild = spawn(npmCommand[0], buildArgs, {
          cwd: toolkitPath,
          env: {
            ...process.env,
            UV_PROJECT_ENVIRONMENT: venvPath,
            npm_config_cache: npmCacheDir,
          },
          shell: true, // Important for Windows
        });

        await new Promise<void>((resolve, reject) => {
          if (npmBuild.stdout) {
            npmBuild.stdout.on('data', (data) => {
              log.info(`[DEPS INSTALL] npm build: ${data}`);
              safeMainWindowSend('install-dependencies-log', {
                type: 'stdout',
                data: data.toString(),
              });
            });
          }

          if (npmBuild.stderr) {
            npmBuild.stderr.on('data', (data) => {
              // TypeScript build warnings are common, don't treat as errors
              log.info(`[DEPS INSTALL] npm build output: ${data}`);
              safeMainWindowSend('install-dependencies-log', {
                type: 'stdout',
                data: data.toString(),
              });
            });
          }

          npmBuild.on('close', (code) => {
            if (code === 0) {
              log.info(
                '[DEPS INSTALL] TypeScript build completed successfully'
              );
              resolve();
            } else {
              log.error(
                `[DEPS INSTALL] TypeScript build failed with code ${code}`
              );
              reject(new Error(`TypeScript build failed with code ${code}`));
            }
          });

          npmBuild.on('error', (err) => {
            log.error(`[DEPS INSTALL] npm build process error: ${err}`);
            reject(err);
          });
        });

        // Optionally install Playwright browsers
        try {
          log.info('[DEPS INSTALL] Installing Playwright browsers...');
          const npxCommand =
            npmCommand[0] === 'npm' ? ['npx'] : [`"${uv_path}"`, 'run', 'npx'];
          const playwrightInstall = spawn(
            npxCommand[0],
            [...npxCommand.slice(1), 'playwright', 'install'],
            {
              cwd: toolkitPath,
              env: {
                ...process.env,
                UV_PROJECT_ENVIRONMENT: venvPath,
              },
              shell: true,
            }
          );

          await new Promise<void>((resolve) => {
            playwrightInstall.on('close', (code) => {
              if (code === 0) {
                log.info(
                  '[DEPS INSTALL] Playwright browsers installed successfully'
                );
                // Create marker file
                const markerPath = path.join(
                  toolkitPath,
                  '.playwright_installed'
                );
                fs.writeFileSync(markerPath, 'installed');
              } else {
                log.warn(
                  '[DEPS INSTALL] Playwright installation failed, but continuing anyway'
                );
              }
              resolve();
            });

            playwrightInstall.on('error', (err) => {
              log.warn(
                '[DEPS INSTALL] Playwright installation process error:',
                err
              );
              resolve(); // Non-critical, continue
            });
          });
        } catch (error) {
          log.warn(
            '[DEPS INSTALL] Failed to install Playwright browsers:',
            error
          );
          // Non-critical, continue
        }

        // Create marker file to indicate npm dependencies are installed
        fs.writeFileSync(
          npmMarkerPath,
          JSON.stringify({
            installedAt: new Date().toISOString(),
            version: version,
          })
        );
        log.info('[DEPS INSTALL] Created npm dependencies marker file');

        log.info(
          '[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully'
        );
        return true;
      } catch (error) {
        log.error(
          '[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:',
          error
        );
        // Don't fail the entire installation if this fails
        return false;
      }
    },
  };

  return new Promise<PromiseReturnType>(async (resolve, _reject) => {
    console.log('start install dependencies');
    const mainWindowAvailable =
      handleInstallOperations.notifyInstallDependenciesPage();

    if (!mainWindowAvailable) {
      log.info(
        '[DEPS INSTALL] Proceeding with installation without UI notifications'
      );
    }

    const isInstalCommandTool = await installCommandTool();
    if (!isInstalCommandTool.success) {
      log.error(
        '[DEPS INSTALL] Command tool installation failed:',
        isInstalCommandTool.message
      );
      safeMainWindowSend('install-dependencies-complete', {
        success: false,
        code: 2,
        error:
          isInstalCommandTool.message || 'Command tool installation failed',
      });
      resolve({ message: 'Command tool installation failed', success: false });
      return;
    }

    // Set Installing Lock Files
    InstallLogs.setLockPath();

    // Clean up npm dependencies marker when reinstalling Python deps
    // This ensures npm deps are reinstalled when Python environment changes
    try {
      let sitePackagesPath: string | null = null;
      const libPath = path.join(venvPath, 'lib');

      if (fs.existsSync(libPath)) {
        const libContents = fs.readdirSync(libPath);
        const pythonDir = libContents.find((name) => name.startsWith('python'));
        if (pythonDir) {
          sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
        }
      }

      if (sitePackagesPath) {
        const npmMarkerPath = path.join(
          sitePackagesPath,
          'camel',
          'toolkits',
          'hybrid_browser_toolkit',
          'ts',
          '.npm_dependencies_installed'
        );
        if (fs.existsSync(npmMarkerPath)) {
          fs.unlinkSync(npmMarkerPath);
          log.info(
            '[DEPS INSTALL] Removed npm dependencies marker for fresh installation'
          );
        }
      }
    } catch (error) {
      log.warn('[DEPS INSTALL] Could not clean npm marker file:', error);
      // Non-critical, continue
    }

    // try default install
    const installSuccess = await runInstall([], version);
    if (installSuccess.success) {
      // Install terminal base venv (lightweight venv for terminal tasks)
      log.info('[DEPS INSTALL] Installing terminal base venv...');
      const terminalResult = await installTerminalBaseVenv(version);
      if (!terminalResult.success) {
        log.warn(
          '[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
          terminalResult.message
        );
      }

      // Install hybrid_browser_toolkit npm dependencies after Python packages are installed
      log.info(
        '[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
      );
      await handleInstallOperations.installHybridBrowserDependencies();

      handleInstallOperations.spawnBabel();

      // Clean up old venvs after successful installation
      log.info('[DEPS INSTALL] Cleaning up old virtual environments...');
      await cleanupOldVenvs(version);
      log.info('[DEPS INSTALL] Old venvs cleanup completed');

      resolve({
        message: 'Dependencies installed successfully',
        success: true,
      });
      return;
    }

    // try mirror install
    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
    let mirrorInstallSuccess: PromiseReturnType = {
      message: '',
      success: false,
    };
    mirrorInstallSuccess =
      timezone === 'Asia/Shanghai'
        ? await runInstall(proxyArgs, version)
        : await runInstall([], version);

    if (mirrorInstallSuccess.success) {
      // Install terminal base venv (lightweight venv for terminal tasks)
      log.info('[DEPS INSTALL] Installing terminal base venv...');
      const terminalResult = await installTerminalBaseVenv(version);
      if (!terminalResult.success) {
        log.warn(
          '[DEPS INSTALL] Terminal base venv installation failed, but continuing...',
          terminalResult.message
        );
      }

      // Install hybrid_browser_toolkit npm dependencies after Python packages are installed
      log.info(
        '[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
      );
      await handleInstallOperations.installHybridBrowserDependencies();

      handleInstallOperations.spawnBabel('mirror');

      // Clean up old venvs after successful installation
      log.info('[DEPS INSTALL] Cleaning up old virtual environments...');
      await cleanupOldVenvs(version);
      log.info('[DEPS INSTALL] Old venvs cleanup completed');

      resolve({
        message: 'Dependencies installed successfully with mirror',
        success: true,
      });
    } else {
      log.error('Both default and mirror install failed');
      safeMainWindowSend('install-dependencies-complete', {
        success: false,
        error: 'Both default and mirror install failed',
      });
      resolve({
        message: 'Both default and mirror install failed',
        success: false,
      });
    }
  });
}