/** * CLI Configuration Loader * * Loads and manages configuration from ~/.postgres.do/config.json * * @module cli/config */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' /** * CLI Configuration structure */ export interface CLIConfig { /** Default API URL for postgres.do service */ apiUrl?: string /** Default database name */ defaultDatabase?: string /** Output format preference */ outputFormat?: 'json' | 'table' | 'plain' /** Enable verbose output by default */ verbose?: boolean /** Authentication tokens (keyed by API URL) */ auth?: { [apiUrl: string]: { token: string expiresAt?: string refreshToken?: string } } /** Recent databases (for autocomplete) */ recentDatabases?: string[] /** Migration settings */ migrations?: { defaultDir?: string format?: 'ts' | 'sql' } } /** * Default configuration values */ const DEFAULT_CONFIG: CLIConfig = { apiUrl: 'https://api.postgres.do', outputFormat: 'table', verbose: false, auth: {}, recentDatabases: [], migrations: { defaultDir: './migrations', format: 'ts', }, } /** * Get the configuration directory path */ export function getConfigDir(): string { return join(homedir(), '.postgres.do') } /** * Get the configuration file path */ export function getConfigPath(): string { return join(getConfigDir(), 'config.json') } /** * Ensure the configuration directory exists */ export function ensureConfigDir(): void { const configDir = getConfigDir() if (!existsSync(configDir)) { mkdirSync(configDir, { recursive: true, mode: 0o700 }) } } /** * Load configuration from disk * * @returns The loaded configuration merged with defaults */ export function loadConfig(): CLIConfig { const configPath = getConfigPath() if (!existsSync(configPath)) { return { ...DEFAULT_CONFIG } } try { const content = readFileSync(configPath, 'utf-8') const userConfig = JSON.parse(content) as Partial // Deep merge with defaults return { ...DEFAULT_CONFIG, ...userConfig, auth: { ...DEFAULT_CONFIG.auth, ...userConfig.auth, }, migrations: { ...DEFAULT_CONFIG.migrations, ...userConfig.migrations, }, } } catch (error) { // If config is corrupted, return defaults console.warn(`Warning: Could not parse config file: ${configPath}`) return { ...DEFAULT_CONFIG } } } /** * Save configuration to disk * * @param config - The configuration to save */ export function saveConfig(config: CLIConfig): void { ensureConfigDir() const configPath = getConfigPath() try { writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600, // User read/write only for security }) } catch (error) { throw new Error(`Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}`) } } /** * Update specific configuration values * * @param updates - Partial configuration updates * @returns The updated configuration */ export function updateConfig(updates: Partial): CLIConfig { const config = loadConfig() // Merge updates explicitly to handle optional properties const updated: CLIConfig = { ...config } // Apply updates if (updates.apiUrl !== undefined) updated.apiUrl = updates.apiUrl if (updates.defaultDatabase !== undefined) updated.defaultDatabase = updates.defaultDatabase if (updates.outputFormat !== undefined) updated.outputFormat = updates.outputFormat if (updates.verbose !== undefined) updated.verbose = updates.verbose if (updates.recentDatabases !== undefined) updated.recentDatabases = updates.recentDatabases // Merge auth if (updates.auth) { updated.auth = { ...config.auth, ...updates.auth } } // Merge migrations if (updates.migrations) { updated.migrations = { ...config.migrations, ...updates.migrations } } saveConfig(updated) return updated } /** * Get a specific configuration value * * @param key - Configuration key * @returns The configuration value or undefined */ export function getConfigValue(key: K): CLIConfig[K] { const config = loadConfig() return config[key] } /** * Set a specific configuration value * * @param key - Configuration key * @param value - Configuration value */ export function setConfigValue(key: K, value: CLIConfig[K]): void { updateConfig({ [key]: value } as Partial) } /** * Add a database to recent databases list * * @param database - Database name to add */ export function addRecentDatabase(database: string): void { const config = loadConfig() const recent = config.recentDatabases || [] // Remove if already exists, then add to front const filtered = recent.filter((d) => d !== database) filtered.unshift(database) // Keep only last 10 const updated = filtered.slice(0, 10) updateConfig({ recentDatabases: updated }) } /** * Get the effective API URL * * @param override - Optional URL override from command line * @returns The API URL to use */ export function getApiUrl(override?: string): string { if (override) return override if (process.env['POSTGRES_DO_API_URL']) return process.env['POSTGRES_DO_API_URL'] return loadConfig().apiUrl || DEFAULT_CONFIG.apiUrl! } /** * Reset configuration to defaults */ export function resetConfig(): void { saveConfig({ ...DEFAULT_CONFIG }) }