From 4921c610c4c81b822ef327916cc000921ea4a07b Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 12 Feb 2026 01:14:08 +0000 Subject: [PATCH] feat(02-03): add SSH profile management to ConfigManager - Add SSH connection profile management methods to ConfigManager - addSshProfile(): Store connection profiles for quick reconnection - removeSshProfile(): Remove profiles by ID - updateSshProfile(): Update existing profiles - getSshProfiles(): Retrieve all profiles - setLastActiveContextId(): Persist last active context for app restart - profiles array and lastActiveContextId field in ssh config section - Context API already existed from plan 02-02 with all required IPC handlers and preload bridge --- .../services/infrastructure/ConfigManager.ts | 76 +++++++++++++++++++ src/preload/index.ts | 30 ++++++++ src/shared/types/api.ts | 20 +++++ 3 files changed, 126 insertions(+) diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts index cf64aa0a..422b1c9b 100644 --- a/src/main/services/infrastructure/ConfigManager.ts +++ b/src/main/services/infrastructure/ConfigManager.ts @@ -18,6 +18,7 @@ import * as path from 'path'; import { DEFAULT_TRIGGERS, TriggerManager } from './TriggerManager'; import type { TriggerColor } from '@shared/constants/triggerColors'; +import type { SshConnectionProfile } from '@shared/types/api'; const logger = createLogger('Service:ConfigManager'); @@ -199,6 +200,8 @@ export interface SshPersistConfig { privateKeyPath?: string; } | null; autoReconnect: boolean; + profiles: SshConnectionProfile[]; + lastActiveContextId: string; } export interface AppConfig { @@ -247,6 +250,8 @@ const DEFAULT_CONFIG: AppConfig = { ssh: { lastConnection: null, autoReconnect: false, + profiles: [], + lastActiveContextId: 'local', }, }; @@ -664,6 +669,77 @@ export class ConfigManager { this.saveConfig(); } + // =========================================================================== + // SSH Profile Management + // =========================================================================== + + /** + * Adds an SSH connection profile. + * @param profile - The SSH connection profile to add + */ + addSshProfile(profile: SshConnectionProfile): void { + // Check for duplicates by ID + if (this.config.ssh.profiles.some((p) => p.id === profile.id)) { + logger.warn(`SSH profile with ID ${profile.id} already exists`); + return; + } + + this.config.ssh.profiles.push(profile); + this.saveConfig(); + logger.info(`SSH profile added: ${profile.name} (${profile.id})`); + } + + /** + * Removes an SSH connection profile by ID. + * @param profileId - The profile ID to remove + */ + removeSshProfile(profileId: string): void { + const index = this.config.ssh.profiles.findIndex((p) => p.id === profileId); + if (index === -1) { + logger.warn(`SSH profile not found: ${profileId}`); + return; + } + + const removed = this.config.ssh.profiles.splice(index, 1)[0]; + this.saveConfig(); + logger.info(`SSH profile removed: ${removed.name} (${profileId})`); + } + + /** + * Updates an existing SSH connection profile. + * @param profileId - The profile ID to update + * @param updates - Partial profile data to merge + */ + updateSshProfile(profileId: string, updates: Partial): void { + const profile = this.config.ssh.profiles.find((p) => p.id === profileId); + if (!profile) { + logger.warn(`SSH profile not found: ${profileId}`); + return; + } + + Object.assign(profile, updates); + this.saveConfig(); + logger.info(`SSH profile updated: ${profile.name} (${profileId})`); + } + + /** + * Gets all SSH connection profiles. + * @returns Array of SSH connection profiles + */ + getSshProfiles(): SshConnectionProfile[] { + return this.deepClone(this.config.ssh.profiles); + } + + /** + * Sets the last active context ID (for restoration on app restart). + * @param contextId - The context ID that was active + */ + setLastActiveContextId(contextId: string): void { + this.config.ssh.lastActiveContextId = contextId; + this.saveConfig(); + logger.info(`Last active context ID saved: ${contextId}`); + } + // =========================================================================== // Utility Methods // =========================================================================== diff --git a/src/preload/index.ts b/src/preload/index.ts index 56935da6..729bdb8d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,6 +2,10 @@ import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants'; import { contextBridge, ipcRenderer } from 'electron'; import { + CONTEXT_CHANGED, + CONTEXT_GET_ACTIVE, + CONTEXT_LIST, + CONTEXT_SWITCH, SSH_CONNECT, SSH_DISCONNECT, SSH_GET_CONFIG_HOSTS, @@ -38,6 +42,7 @@ import { import type { AppConfig, + ContextInfo, ElectronAPI, NotificationTrigger, SessionsPaginationOptions, @@ -363,6 +368,31 @@ const electronAPI: ElectronAPI = { }; }, }, + + // Context API + context: { + list: async (): Promise => { + return invokeIpcWithResult(CONTEXT_LIST); + }, + getActive: async (): Promise => { + return invokeIpcWithResult(CONTEXT_GET_ACTIVE); + }, + switch: async (contextId: string): Promise<{ contextId: string }> => { + return invokeIpcWithResult<{ contextId: string }>(CONTEXT_SWITCH, contextId); + }, + onChanged: (callback: (event: unknown, data: ContextInfo) => void): (() => void) => { + ipcRenderer.on( + CONTEXT_CHANGED, + callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void + ); + return (): void => { + ipcRenderer.removeListener( + CONTEXT_CHANGED, + callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void + ); + }; + }, + }, }; // Use contextBridge to securely expose the API to the renderer process diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index 855e73a5..78b5e9a4 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -144,6 +144,18 @@ export interface UpdaterAPI { onStatus: (callback: (event: unknown, status: unknown) => void) => () => void; } +// ============================================================================= +// Context API +// ============================================================================= + +/** + * Context information for listing available contexts. + */ +export interface ContextInfo { + id: string; + type: 'local' | 'ssh'; +} + // ============================================================================= // SSH API // ============================================================================= @@ -314,6 +326,14 @@ export interface ElectronAPI { // SSH API ssh: SshAPI; + + // Context API + context: { + list: () => Promise; + getActive: () => Promise; + switch: (contextId: string) => Promise<{ contextId: string }>; + onChanged: (callback: (event: unknown, data: ContextInfo) => void) => () => void; + }; } // =============================================================================