/** * SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests. * * Channels: * - ssh:connect - Connect to SSH host, create new context * - ssh:disconnect - Disconnect and switch back to local context * - ssh:getState - Get current connection state * - ssh:test - Test connection without switching */ import { createLogger } from '@shared/utils/logger'; // Channel constants (mirrored from preload/constants/ipcChannels.ts to respect module boundaries) const SSH_CONNECT = 'ssh:connect'; const SSH_DISCONNECT = 'ssh:disconnect'; const SSH_GET_STATE = 'ssh:getState'; const SSH_TEST = 'ssh:test'; const SSH_GET_CONFIG_HOSTS = 'ssh:getConfigHosts'; const SSH_RESOLVE_HOST = 'ssh:resolveHost'; const SSH_SAVE_LAST_CONNECTION = 'ssh:saveLastConnection'; const SSH_GET_LAST_CONNECTION = 'ssh:getLastConnection'; import * as path from 'path'; import { configManager, ServiceContext } from '../services'; import type { ServiceContextRegistry, SshConnectionConfig, SshConnectionManager, SshConnectionStatus, } from '../services'; import type { SshLastConnection } from '@shared/types'; import type { IpcMain } from 'electron'; const logger = createLogger('IPC:ssh'); // ============================================================================= // Module State // ============================================================================= let connectionManager: SshConnectionManager; let registry: ServiceContextRegistry; let onContextRewire: (context: ServiceContext) => void; // ============================================================================= // Initialization // ============================================================================= /** * Initialize SSH handlers with required services. * @param manager - The SSH connection manager instance * @param contextRegistry - The service context registry * @param onRewire - Rewire-only callback (no renderer notification) for renderer-initiated switches */ export function initializeSshHandlers( manager: SshConnectionManager, contextRegistry: ServiceContextRegistry, onRewire: (context: ServiceContext) => void ): void { connectionManager = manager; registry = contextRegistry; onContextRewire = onRewire; } // ============================================================================= // Handler Registration // ============================================================================= export function registerSshHandlers(ipcMain: IpcMain): void { ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => { try { // Connect to SSH host await connectionManager.connect(config); // Get provider and remote path const provider = connectionManager.getProvider(); const remoteProjectsPath = connectionManager.getRemoteProjectsPath() ?? undefined; const remoteTodosPath = remoteProjectsPath ? path.join(path.dirname(remoteProjectsPath), 'todos') : undefined; // Generate context ID const contextId = `ssh-${config.host}`; // Destroy existing SSH context if any (reconnection case) if (registry.has(contextId)) { logger.info(`Destroying existing SSH context: ${contextId}`); registry.destroy(contextId); } // Create new SSH context const sshContext = new ServiceContext({ id: contextId, type: 'ssh', fsProvider: provider, projectsDir: remoteProjectsPath, todosDir: remoteTodosPath, }); // Register and start SSH context registry.registerContext(sshContext); sshContext.start(); // Switch to SSH context registry.switch(contextId); // Re-wire file watcher events only (renderer's connectSsh() handles state) onContextRewire(sshContext); return { success: true, data: connectionManager.getStatus() }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error('SSH connect failed:', message); return { success: false, error: message }; } }); ipcMain.handle(SSH_DISCONNECT, async () => { try { // Get current SSH context ID before disconnecting const currentContextId = registry.getActiveContextId(); const isSshContext = currentContextId.startsWith('ssh-'); // Disconnect from SSH connectionManager.disconnect(); // If we were on an SSH context, destroy it if (isSshContext) { // Switch back to local first (this also starts local file watcher) registry.switch('local'); // Destroy the SSH context registry.destroy(currentContextId); // Re-wire file watcher events only (renderer's disconnectSsh() handles state) const localContext = registry.getActive(); onContextRewire(localContext); } return { success: true, data: connectionManager.getStatus() }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error('SSH disconnect failed:', message); return { success: false, error: message }; } }); ipcMain.handle(SSH_GET_STATE, async (): Promise => { return connectionManager.getStatus(); }); ipcMain.handle(SSH_TEST, async (_event, config: SshConnectionConfig) => { try { const result = await connectionManager.testConnection(config); return { success: true, data: result }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, error: message }; } }); ipcMain.handle(SSH_GET_CONFIG_HOSTS, async () => { try { const hosts = await connectionManager.getConfigHosts(); return { success: true, data: hosts }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error('Failed to get SSH config hosts:', message); return { success: true, data: [] }; } }); ipcMain.handle(SSH_RESOLVE_HOST, async (_event, alias: string) => { try { const entry = await connectionManager.resolveHostConfig(alias); return { success: true, data: entry }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error(`Failed to resolve SSH host "${alias}":`, message); return { success: true, data: null }; } }); ipcMain.handle(SSH_SAVE_LAST_CONNECTION, async (_event, config: SshLastConnection) => { try { configManager.updateConfig('ssh', { lastConnection: { host: config.host, port: config.port, username: config.username, authMethod: config.authMethod, privateKeyPath: config.privateKeyPath, }, }); return { success: true }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error('Failed to save SSH connection:', message); return { success: false, error: message }; } }); ipcMain.handle(SSH_GET_LAST_CONNECTION, async () => { try { const config = configManager.getConfig(); return { success: true, data: config.ssh.lastConnection }; } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error('Failed to get last SSH connection:', message); return { success: true, data: null }; } }); logger.info('SSH handlers registered'); } export function removeSshHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler(SSH_CONNECT); ipcMain.removeHandler(SSH_DISCONNECT); ipcMain.removeHandler(SSH_GET_STATE); ipcMain.removeHandler(SSH_TEST); ipcMain.removeHandler(SSH_GET_CONFIG_HOSTS); ipcMain.removeHandler(SSH_RESOLVE_HOST); ipcMain.removeHandler(SSH_SAVE_LAST_CONNECTION); ipcMain.removeHandler(SSH_GET_LAST_CONNECTION); }