258 lines
8.4 KiB
TypeScript
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 };
|
|
}
|