@civic/hub-bridge
Version:
Stdio <-> HTTP/SSE MCP bridge with Civic auth handling
169 lines • 5.06 kB
JavaScript
/**
* ConfigManager.ts
*
* Unified configuration management for hub-bridge.
* Handles OAuth tokens, profile selection, and other persistent configuration.
*/
import { promises as fs } from 'node:fs';
import { dirname } from 'node:path';
import { homedir } from 'node:os';
import { logger } from '../utils/logger.js';
import { CONFIG_FILE } from "../config/index.js";
/**
* Centralized configuration manager for hub-bridge
* Also implements TokenPersistence interface for OAuth token storage
*/
export class ConfigManager {
static instance = null;
configPath;
config = null;
constructor(configPath) {
this.configPath = this.expandTildePath(configPath);
this.loadConfig().then((config) => { this.config = config; });
}
/**
* Get singleton instance of ConfigManager
*/
static getInstance(configPath = CONFIG_FILE) {
if (!ConfigManager.instance) {
if (!configPath) {
throw new Error('ConfigManager not initialized. Provide configPath on first call.');
}
ConfigManager.instance = new ConfigManager(configPath);
}
return ConfigManager.instance;
}
/**
* Reset singleton instance (primarily for testing)
*/
static resetInstance() {
ConfigManager.instance = null;
}
/**
* Expands tilde (~) in file paths to the user's home directory
*/
expandTildePath(filePath) {
if (filePath.startsWith('~/')) {
return filePath.replace('~', homedir());
}
return filePath;
}
/**
* Load configuration from disk
*/
async loadConfig() {
if (this.config) {
return this.config;
}
try {
const data = await fs.readFile(this.configPath, 'utf8');
this.config = JSON.parse(data);
return this.config;
}
catch {
// Config doesn't exist or can't be read - return empty config
logger.debug('Config file not found or unreadable, using empty config');
this.config = {};
return this.config;
}
}
/**
* Save configuration to disk
*/
async saveConfig() {
if (!this.config) {
throw new Error('No configuration loaded');
}
try {
// Ensure directory exists
await fs.mkdir(dirname(this.configPath), { recursive: true });
// Write config atomically
const tempPath = `${this.configPath}.tmp`;
await fs.writeFile(tempPath, JSON.stringify(this.config, null, 2), 'utf8');
await fs.rename(tempPath, this.configPath);
logger.info('Configuration saved successfully');
}
catch (error) {
logger.error('Failed to save configuration:', error);
throw error;
}
}
/**
* Get OAuth tokens (alias for TokenPersistence interface)
*/
async getTokens() {
const config = await this.loadConfig();
return config.tokens;
}
/**
* Load OAuth tokens (TokenPersistence interface method)
*/
async loadTokens() {
return this.getTokens();
}
/**
* Save OAuth tokens
*/
async saveTokens(tokens) {
const config = await this.loadConfig();
config.tokens = tokens;
await this.saveConfig();
logger.info('OAuth tokens saved');
}
/**
* Clear OAuth tokens
*/
async clearTokens() {
const config = await this.loadConfig();
delete config.tokens;
await this.saveConfig();
logger.info('OAuth tokens cleared');
}
/**
* Get current profile alias synchronously from cached config
* Returns undefined if config not yet loaded
*/
getCurrentProfileAliasSync() {
return this.config?.currentProfileAlias;
}
/**
* Get current profile alias
*/
async getCurrentProfileAlias() {
const config = await this.loadConfig();
return config.currentProfileAlias;
}
/**
* Set current profile by alias
*/
async setCurrentProfile(alias) {
this.config = await this.loadConfig();
this.config.currentProfileAlias = alias;
await this.saveConfig();
logger.info(`Profile switched to: ${alias}`);
}
/**
* Clear current profile selection
*/
async clearCurrentProfile() {
this.config = await this.loadConfig();
delete this.config.currentProfileAlias;
await this.saveConfig();
logger.info('Profile selection cleared');
}
/**
* Clear all configuration
*/
async clearAll() {
try {
await fs.unlink(this.configPath);
this.config = null;
logger.info('All configuration cleared');
}
catch {
// File doesn't exist - ignore error
logger.debug('Config file does not exist, nothing to clear');
}
}
}
//# sourceMappingURL=ConfigManager.js.map