UNPKG

automagik-cli

Version:

Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems

280 lines (279 loc) 9.99 kB
import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import { config } from 'dotenv'; export class SettingsManager { settingsDir; settingsPath; SETTINGS_VERSION = '1.0.0'; constructor() { this.settingsDir = path.join(os.homedir(), '.automagik-cli'); this.settingsPath = path.join(this.settingsDir, 'settings.json'); } /** * Ensure the settings directory exists */ async ensureDirectoryExists() { try { await fs.mkdir(this.settingsDir, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { throw new Error(`Failed to create settings directory: ${error}`); } } } /** * Get default configuration with sensible defaults */ getDefaultConfig() { const now = new Date().toISOString(); return { // API Configuration - Default to localhost:8886 as specified apiBaseUrl: 'http://localhost:8886', apiKey: undefined, // Optional apiTimeout: 30000, apiRetryAttempts: 3, // CLI Configuration cliDebug: false, // Session Configuration sessionDir: path.join(this.settingsDir, 'sessions'), sessionMaxHistory: 100, sessionAutoSave: true, // Display Configuration enableColors: true, enableSpinner: true, maxDisplayWidth: 200, // Development Configuration nodeEnv: 'production', logLevel: 'error', // Meta fields version: this.SETTINGS_VERSION, createdAt: now, updatedAt: now, }; } /** * Validate settings configuration */ validate(config) { const errors = []; // Required fields if (!config.apiBaseUrl) { errors.push('apiBaseUrl is required'); } else if (!this.isValidUrl(config.apiBaseUrl)) { errors.push('apiBaseUrl must be a valid URL'); } // Type validations if (config.apiTimeout !== undefined && (typeof config.apiTimeout !== 'number' || config.apiTimeout <= 0)) { errors.push('apiTimeout must be a positive number'); } if (config.apiRetryAttempts !== undefined && (typeof config.apiRetryAttempts !== 'number' || config.apiRetryAttempts < 0)) { errors.push('apiRetryAttempts must be a non-negative number'); } if (config.sessionMaxHistory !== undefined && (typeof config.sessionMaxHistory !== 'number' || config.sessionMaxHistory <= 0)) { errors.push('sessionMaxHistory must be a positive number'); } if (config.maxDisplayWidth !== undefined && (typeof config.maxDisplayWidth !== 'number' || config.maxDisplayWidth <= 0)) { errors.push('maxDisplayWidth must be a positive number'); } return { valid: errors.length === 0, errors }; } /** * Load settings from JSON file */ async load() { try { await this.ensureDirectoryExists(); // Check if settings file exists const exists = await this.fileExists(this.settingsPath); if (!exists) { // No settings file - return defaults return this.getDefaultConfig(); } // Read and parse settings file const data = await fs.readFile(this.settingsPath, 'utf8'); const parsed = JSON.parse(data); // Validate loaded settings const validation = this.validate(parsed); if (!validation.valid) { console.warn('Settings validation failed:', validation.errors); // Return defaults with warnings but don't throw return this.getDefaultConfig(); } // Merge with defaults to ensure all fields are present const defaults = this.getDefaultConfig(); const merged = { ...defaults, ...parsed }; merged.updatedAt = new Date().toISOString(); return merged; } catch (error) { if (error instanceof SyntaxError) { // JSON parse error - backup corrupted file and return defaults await this.backupCorruptedFile(); console.warn('Settings file corrupted, using defaults'); return this.getDefaultConfig(); } throw new Error(`Failed to load settings: ${error}`); } } /** * Save settings to JSON file with atomic operations */ async save(config) { try { await this.ensureDirectoryExists(); // Validate before saving const validation = this.validate(config); if (!validation.valid) { throw new Error(`Invalid settings: ${validation.errors.join(', ')}`); } // Update metadata config.version = this.SETTINGS_VERSION; config.updatedAt = new Date().toISOString(); // Atomic write: write to temp file then rename const tempPath = `${this.settingsPath}.tmp`; const jsonContent = JSON.stringify(config, null, 2); await fs.writeFile(tempPath, jsonContent, 'utf8'); await fs.rename(tempPath, this.settingsPath); } catch (error) { throw new Error(`Failed to save settings: ${error}`); } } /** * Check if settings file exists */ async settingsExist() { return this.fileExists(this.settingsPath); } /** * Detect and migrate from .env file */ async migrate() { try { // Check if settings already exist if (await this.settingsExist()) { return { migrated: false, source: 'defaults' }; } // Look for .env file in current directory const envPath = path.join(process.cwd(), '.env'); const envExists = await this.fileExists(envPath); if (!envExists) { // No .env file found, create with defaults const defaults = this.getDefaultConfig(); await this.save(defaults); return { migrated: true, source: 'defaults' }; } // Load .env file config({ path: envPath }); // Create settings from environment variables const envConfig = this.createFromEnv(); // Validate migrated config const validation = this.validate(envConfig); if (!validation.valid) { return { migrated: false, source: 'env', errors: validation.errors }; } // Save migrated settings await this.save(envConfig); return { migrated: true, source: 'env' }; } catch (error) { return { migrated: false, source: 'env', errors: [`Migration failed: ${error}`] }; } } /** * Create settings config from environment variables (for migration) */ createFromEnv() { const now = new Date().toISOString(); return { // API Configuration apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8886', apiKey: process.env.API_KEY || undefined, apiTimeout: parseInt(process.env.API_TIMEOUT || '30000', 10), apiRetryAttempts: parseInt(process.env.API_RETRY_ATTEMPTS || '3', 10), // CLI Configuration cliDebug: process.env.CLI_DEBUG?.toLowerCase() === 'true', // Session Configuration sessionDir: process.env.SESSION_DIR || path.join(this.settingsDir, 'sessions'), sessionMaxHistory: parseInt(process.env.SESSION_MAX_HISTORY || '100', 10), sessionAutoSave: process.env.SESSION_AUTO_SAVE?.toLowerCase() !== 'false', // Display Configuration enableColors: process.env.ENABLE_COLORS?.toLowerCase() !== 'false', enableSpinner: process.env.ENABLE_SPINNER?.toLowerCase() !== 'false', maxDisplayWidth: parseInt(process.env.MAX_DISPLAY_WIDTH || '200', 10), // Development Configuration nodeEnv: process.env.NODE_ENV || 'production', logLevel: process.env.LOG_LEVEL || 'error', // Meta fields version: this.SETTINGS_VERSION, createdAt: now, updatedAt: now, }; } /** * Backup corrupted settings file */ async backupCorruptedFile() { try { const backupPath = `${this.settingsPath}.corrupted.${Date.now()}`; await fs.copyFile(this.settingsPath, backupPath); await fs.unlink(this.settingsPath); } catch (error) { // Ignore backup failures - we'll create new settings anyway } } /** * Check if file exists */ async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Validate URL format */ isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } /** * Get settings file path (for external access) */ getSettingsPath() { return this.settingsPath; } /** * Reset settings to defaults */ async reset() { const defaults = this.getDefaultConfig(); await this.save(defaults); } } // Export singleton instance export const settingsManager = new SettingsManager();