/** * IPC Handlers for CLI Installer Operations. * * Handlers: * - cliInstaller:getStatus: Get current CLI installation status * - cliInstaller:install: Start CLI install/update flow * - cliInstaller:progress: Progress events (main → renderer, not a handler) */ import { CLI_INSTALLER_GET_PROVIDER_STATUS, CLI_INSTALLER_GET_STATUS, CLI_INSTALLER_INSTALL, CLI_INSTALLER_INVALIDATE_STATUS, CLI_INSTALLER_VERIFY_PROVIDER_MODELS, // eslint-disable-next-line boundaries/element-types -- IPC channel constants shared between main and preload } from '@preload/constants/ipcChannels'; import { getErrorMessage } from '@shared/utils/errorHandling'; import { createLogger } from '@shared/utils/logger'; import { CodexBinaryResolver } from '../services/infrastructure/codexAppServer'; import { ClaudeBinaryResolver } from '../services/team/ClaudeBinaryResolver'; import type { CliInstallerService } from '../services'; import type { CliInstallationStatus, CliProviderId, CliProviderStatus, IpcResult, } from '@shared/types'; import type { IpcMain, IpcMainInvokeEvent } from 'electron'; const logger = createLogger('IPC:cliInstaller'); let service: CliInstallerService; let statusInFlight: Promise | null = null; const providerStatusInFlight = new Map>(); let cachedStatus: { value: CliInstallationStatus; at: number } | null = null; let statusCacheGeneration = 0; const STATUS_CACHE_TTL_MS = 5_000; const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set(['anthropic', 'codex', 'opencode']); function isFrontendMultimodelProviderId(providerId: CliProviderId): boolean { return FRONTEND_MULTIMODEL_PROVIDER_IDS.has(providerId); } function getCachedStatusAuthenticatedProvider( providers: CliProviderStatus[] ): CliProviderStatus | null { return ( providers.find( (provider) => isFrontendMultimodelProviderId(provider.providerId) && provider.authenticated ) ?? null ); } /** * Initializes CLI installer handlers with the service instance. */ export function initializeCliInstallerHandlers(installerService: CliInstallerService): void { service = installerService; } /** * Registers all CLI installer IPC handlers. */ export function registerCliInstallerHandlers(ipcMain: IpcMain): void { ipcMain.handle(CLI_INSTALLER_GET_STATUS, handleGetStatus); ipcMain.handle(CLI_INSTALLER_GET_PROVIDER_STATUS, handleGetProviderStatus); ipcMain.handle(CLI_INSTALLER_VERIFY_PROVIDER_MODELS, handleVerifyProviderModels); ipcMain.handle(CLI_INSTALLER_INSTALL, handleInstall); ipcMain.handle(CLI_INSTALLER_INVALIDATE_STATUS, handleInvalidateStatus); logger.info('CLI installer handlers registered'); } /** * Removes all CLI installer IPC handlers. */ export function removeCliInstallerHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler(CLI_INSTALLER_GET_STATUS); ipcMain.removeHandler(CLI_INSTALLER_GET_PROVIDER_STATUS); ipcMain.removeHandler(CLI_INSTALLER_VERIFY_PROVIDER_MODELS); ipcMain.removeHandler(CLI_INSTALLER_INSTALL); ipcMain.removeHandler(CLI_INSTALLER_INVALIDATE_STATUS); logger.info('CLI installer handlers removed'); } // ============================================================================= // Handler Implementations // ============================================================================= async function handleGetStatus( _event: IpcMainInvokeEvent ): Promise> { try { const latestSnapshot = service.getLatestStatusSnapshot(); if (cachedStatus && Date.now() - cachedStatus.at < STATUS_CACHE_TTL_MS) { if (latestSnapshot) { cachedStatus = { value: latestSnapshot, at: Date.now() }; return { success: true, data: latestSnapshot }; } return { success: true, data: cachedStatus.value }; } if (!statusInFlight) { const startedAt = Date.now(); const generation = statusCacheGeneration; const request = service .getStatus() .then((status) => { if (generation === statusCacheGeneration) { cachedStatus = { value: status, at: Date.now() }; } return status; }) .catch((err) => { if (generation === statusCacheGeneration) { cachedStatus = null; } throw err; }) .finally(() => { const ms = Date.now() - startedAt; if (ms >= 2000) { logger.warn(`cliInstaller:getStatus slow ms=${ms}`); } if (statusInFlight === request) { statusInFlight = null; } }); statusInFlight = request; } const status = await statusInFlight; return { success: true, data: status }; } catch (error) { const msg = getErrorMessage(error); logger.error('Error in cliInstaller:getStatus:', msg); return { success: false, error: msg }; } } function patchCachedProviderStatus(providerStatus: CliProviderStatus | null): void { if (!cachedStatus || !providerStatus) { return; } if ( cachedStatus.value.flavor === 'agent_teams_orchestrator' && !isFrontendMultimodelProviderId(providerStatus.providerId) ) { return; } const hasProvider = cachedStatus.value.providers.some( (provider) => provider.providerId === providerStatus.providerId ); const nextProviders = hasProvider ? cachedStatus.value.providers.map((provider) => provider.providerId === providerStatus.providerId ? providerStatus : provider ) : [...cachedStatus.value.providers, providerStatus]; const authenticatedProvider = cachedStatus.value.flavor === 'agent_teams_orchestrator' ? getCachedStatusAuthenticatedProvider(nextProviders) : (nextProviders.find((provider) => provider.authenticated) ?? null); cachedStatus = { value: { ...cachedStatus.value, providers: nextProviders, authLoggedIn: cachedStatus.value.flavor === 'agent_teams_orchestrator' ? authenticatedProvider !== null : nextProviders.some((provider) => provider.authenticated), authMethod: authenticatedProvider?.authMethod ?? null, }, at: Date.now(), }; } async function handleGetProviderStatus( _event: IpcMainInvokeEvent, providerId: CliProviderId ): Promise> { try { const inFlight = providerStatusInFlight.get(providerId); if (inFlight) { const status = await inFlight; return { success: true, data: status }; } const generation = statusCacheGeneration; const request = service .getProviderStatus(providerId) .then((status) => { if (generation === statusCacheGeneration) { patchCachedProviderStatus(status); } return status; }) .finally(() => { if (providerStatusInFlight.get(providerId) === request) { providerStatusInFlight.delete(providerId); } }); providerStatusInFlight.set(providerId, request); const status = await request; return { success: true, data: status }; } catch (error) { const msg = getErrorMessage(error); logger.error(`Error in cliInstaller:getProviderStatus(${providerId}):`, msg); return { success: false, error: msg }; } } async function handleInstall(_event: IpcMainInvokeEvent): Promise> { try { await service.install(); return { success: true, data: undefined }; } catch (error) { const msg = getErrorMessage(error); logger.error('Error in cliInstaller:install:', msg); return { success: false, error: msg }; } } async function handleVerifyProviderModels( _event: IpcMainInvokeEvent, providerId: CliProviderId ): Promise> { try { const generation = statusCacheGeneration; const status = await service.verifyProviderModels(providerId); if (generation === statusCacheGeneration) { patchCachedProviderStatus(status); } return { success: true, data: status }; } catch (error) { const msg = getErrorMessage(error); logger.error(`Error in cliInstaller:verifyProviderModels(${providerId}):`, msg); return { success: false, error: msg }; } } function handleInvalidateStatus(_event: IpcMainInvokeEvent): IpcResult { statusCacheGeneration += 1; cachedStatus = null; statusInFlight = null; providerStatusInFlight.clear(); ClaudeBinaryResolver.clearCache(); CodexBinaryResolver.clearCache(); service.invalidateStatusCache(); return { success: true, data: undefined }; }