agent-ecosystem/src/main/ipc/cliInstaller.ts
2026-05-22 00:18:59 +03:00

258 lines
8.4 KiB
TypeScript

/**
* 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<CliInstallationStatus> | null = null;
const providerStatusInFlight = new Map<CliProviderId, Promise<CliProviderStatus | null>>();
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<CliProviderId>(['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<IpcResult<CliInstallationStatus>> {
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<IpcResult<CliProviderStatus | null>> {
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<IpcResult<void>> {
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<IpcResult<CliProviderStatus | null>> {
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<void> {
statusCacheGeneration += 1;
cachedStatus = null;
statusInFlight = null;
providerStatusInFlight.clear();
ClaudeBinaryResolver.clearCache();
CodexBinaryResolver.clearCache();
service.invalidateStatusCache();
return { success: true, data: undefined };
}