config-service.ts•8.39 kB
import Conf from 'conf';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import {
DEFAULT_INSTALLATION_CONFIG,
ENCRYPTION_KEY_ENV_VAR,
ENV_STORAGE_DIR,
EnvVar,
INSTALLATION_CONFIG_FILE,
InstallationConfig,
Profile,
PROFILES_FILE,
ProfilesConfig,
} from '../config.js';
import { ensureDir } from '../utils/fs-utils.js';
/**
* Service for managing configuration, profiles, and environment variables
*/
export class ConfigService {
private profilesConfig: ProfilesConfig;
private installationConfig: InstallationConfig;
private envStore: Conf<Record<string, Record<string, EnvVar>>>;
private encryptionKey: string;
/**
* Creates a new ConfigService instance
* @param encryptionKey - Encryption key for sensitive data
*/
constructor(encryptionKey: string) {
this.encryptionKey = encryptionKey;
this.profilesConfig = { profiles: [] };
this.installationConfig = { ...DEFAULT_INSTALLATION_CONFIG };
// Initialize environment variable store with encryption
this.envStore = new Conf({
projectName: 'mcp-env-manager',
cwd: ENV_STORAGE_DIR,
encryptionKey,
fileExtension: 'enc'
});
}
/**
* Loads configuration files from disk
*/
async loadConfig(): Promise<void> {
try {
// Ensure storage directory exists
await ensureDir(ENV_STORAGE_DIR);
// Load profiles file if it exists
try {
const profilesData = await fs.readFile(PROFILES_FILE, 'utf-8');
this.profilesConfig = JSON.parse(profilesData);
} catch (error) {
// Create new profiles file if it doesn't exist
this.profilesConfig = { profiles: [] };
await this.saveProfiles();
}
// Load installation config if it exists
try {
const installationConfigData = await fs.readFile(INSTALLATION_CONFIG_FILE, 'utf-8');
this.installationConfig = JSON.parse(installationConfigData);
} catch (error) {
// Create new installation config if it doesn't exist
this.installationConfig = { ...DEFAULT_INSTALLATION_CONFIG };
await this.saveInstallationConfig();
}
} catch (error) {
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Saves profiles to disk
*/
async saveProfiles(): Promise<void> {
await ensureDir(path.dirname(PROFILES_FILE));
await fs.writeFile(PROFILES_FILE, JSON.stringify(this.profilesConfig, null, 2));
}
/**
* Saves installation config to disk
*/
async saveInstallationConfig(): Promise<void> {
await ensureDir(path.dirname(INSTALLATION_CONFIG_FILE));
await fs.writeFile(INSTALLATION_CONFIG_FILE, JSON.stringify(this.installationConfig, null, 2));
}
/**
* Gets all profiles
*/
getProfiles(): Profile[] {
return this.profilesConfig.profiles;
}
/**
* Gets a profile by ID
*/
getProfile(id: string): Profile | undefined {
return this.profilesConfig.profiles.find(p => p.id === id);
}
/**
* Gets the active profile ID
*/
getActiveProfileId(): string | undefined {
return this.profilesConfig.activeProfile;
}
/**
* Gets the active profile
*/
getActiveProfile(): Profile | undefined {
const activeId = this.getActiveProfileId();
if (!activeId) return undefined;
return this.getProfile(activeId);
}
/**
* Creates a new profile
*/
async createProfile(name: string, description?: string): Promise<Profile> {
const id = `${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Date.now().toString(36)}`;
const now = new Date().toISOString();
const profile: Profile = {
id,
name,
description,
createdAt: now,
updatedAt: now
};
this.profilesConfig.profiles.push(profile);
await this.saveProfiles();
return profile;
}
/**
* Updates a profile
*/
async updateProfile(id: string, updates: Partial<Pick<Profile, 'name' | 'description'>>): Promise<Profile> {
const profileIndex = this.profilesConfig.profiles.findIndex(p => p.id === id);
if (profileIndex === -1) {
throw new Error(`Profile not found: ${id}`);
}
const profile = this.profilesConfig.profiles[profileIndex];
const updatedProfile = {
...profile,
...updates,
updatedAt: new Date().toISOString()
};
this.profilesConfig.profiles[profileIndex] = updatedProfile;
await this.saveProfiles();
return updatedProfile;
}
/**
* Deletes a profile
*/
async deleteProfile(id: string): Promise<void> {
const initialLength = this.profilesConfig.profiles.length;
this.profilesConfig.profiles = this.profilesConfig.profiles.filter(p => p.id !== id);
if (this.profilesConfig.profiles.length === initialLength) {
throw new Error(`Profile not found: ${id}`);
}
if (this.profilesConfig.activeProfile === id) {
this.profilesConfig.activeProfile = undefined;
}
await this.saveProfiles();
// Delete environment variables for this profile
this.envStore.delete(id);
}
/**
* Sets the active profile
*/
async setActiveProfile(id: string): Promise<void> {
const profile = this.getProfile(id);
if (!profile) {
throw new Error(`Profile not found: ${id}`);
}
this.profilesConfig.activeProfile = id;
await this.saveProfiles();
}
/**
* Gets all environment variables for a profile
*/
getEnvVars(profileId: string): Record<string, EnvVar> {
return this.envStore.get(profileId, {});
}
/**
* Gets an environment variable
*/
getEnvVar(profileId: string, key: string): EnvVar | undefined {
const vars = this.getEnvVars(profileId);
return vars[key];
}
/**
* Sets an environment variable
*/
setEnvVar(profileId: string, key: string, value: string, sensitive: boolean, description?: string): void {
const profile = this.getProfile(profileId);
if (!profile) {
throw new Error(`Profile not found: ${profileId}`);
}
const vars = this.getEnvVars(profileId);
vars[key] = { value, sensitive, description };
this.envStore.set(profileId, vars);
}
/**
* Deletes an environment variable
*/
deleteEnvVar(profileId: string, key: string): void {
const vars = this.getEnvVars(profileId);
if (!vars[key]) {
throw new Error(`Environment variable not found: ${key}`);
}
delete vars[key];
this.envStore.set(profileId, vars);
}
/**
* Gets the installation configuration
*/
getInstallationConfig(): InstallationConfig {
return this.installationConfig;
}
/**
* Updates the installation configuration
*/
async updateInstallationConfig(updates: Partial<InstallationConfig>): Promise<InstallationConfig> {
this.installationConfig = {
...this.installationConfig,
...updates,
watchers: {
...this.installationConfig.watchers,
...(updates.watchers || {})
},
packageManager: {
...this.installationConfig.packageManager,
...(updates.packageManager || {})
},
notifications: {
...this.installationConfig.notifications,
...(updates.notifications || {})
}
};
await this.saveInstallationConfig();
return this.installationConfig;
}
}
/**
* Generates a new encryption key
*/
export function generateEncryptionKey(): string {
return crypto.randomBytes(32).toString('hex');
}
/**
* Gets the encryption key from the environment or generates a new one
*/
export function getEncryptionKey(): string {
const envKey = process.env[ENCRYPTION_KEY_ENV_VAR];
if (envKey) {
return envKey;
}
// Generate a new key
const newKey = generateEncryptionKey();
process.env[ENCRYPTION_KEY_ENV_VAR] = newKey;
console.warn(`
⚠️ Generated a new encryption key. For data persistence across restarts, set the
${ENCRYPTION_KEY_ENV_VAR} environment variable to:
${newKey}
`);
return newKey;
}
/**
* Initializes the config service
*/
export async function initializeConfigService(): Promise<ConfigService> {
const encryptionKey = getEncryptionKey();
const configService = new ConfigService(encryptionKey);
await configService.loadConfig();
return configService;
}