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
This commit is contained in:
matt 2026-02-12 01:14:08 +00:00
parent 4aafc9c4fc
commit 4921c610c4
3 changed files with 126 additions and 0 deletions

View file

@ -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<SshConnectionProfile>): 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
// ===========================================================================

View file

@ -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<ContextInfo[]> => {
return invokeIpcWithResult<ContextInfo[]>(CONTEXT_LIST);
},
getActive: async (): Promise<string> => {
return invokeIpcWithResult<string>(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

View file

@ -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<ContextInfo[]>;
getActive: () => Promise<string>;
switch: (contextId: string) => Promise<{ contextId: string }>;
onChanged: (callback: (event: unknown, data: ContextInfo) => void) => () => void;
};
}
// =============================================================================