UNPKG

blue-beatle

Version:

๐Ÿค– AI-Powered Development Assistant - Intelligent code analysis, problem solving, and terminal automation using Gemini API

651 lines (579 loc) โ€ข 23.2 kB
/** * โš™๏ธ Config Manager - Configuration and settings management * Handles API keys, user preferences, and application settings */ const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const chalk = require('chalk'); const inquirer = require('inquirer'); const crypto = require('crypto'); class ConfigManager { constructor() { this.configDir = path.join(os.homedir(), '.blue-beatle'); this.configFile = path.join(this.configDir, 'config.json'); this.secretsFile = path.join(this.configDir, 'secrets.enc'); this.defaultConfig = { version: '1.2.0', user: { name: '', email: '', preferences: { theme: 'default', verboseOutput: false, autoUpdate: true, telemetry: false } }, ai: { provider: 'gemini', model: 'gemini-2.0-flash', maxTokens: 4096, temperature: 0.7, timeout: 30000 }, terminal: { safeMode: true, logCommands: false, confirmDangerous: true, historySize: 1000 }, analysis: { autoFix: false, watchMode: false, notifications: true, reportFormat: 'table' }, project: { defaultTemplate: 'node', autoGitInit: true, createReadme: true, setupDocker: false } }; this.config = { ...this.defaultConfig }; this.secrets = { geminiApiKey: 'AIzaSyD4QvJ_DEBDxOzM4nGlfSy9jYMm6ac7plI' }; } async initialize() { try { await fs.ensureDir(this.configDir); await this.loadConfig(); await this.loadSecrets(); return true; } catch (error) { console.error(chalk.red('โŒ Failed to initialize config:'), error.message); return false; } } async loadConfig() { try { if (await fs.pathExists(this.configFile)) { const configData = await fs.readJson(this.configFile); this.config = this.mergeConfig(this.defaultConfig, configData); } else { await this.saveConfig(); } } catch (error) { console.error(chalk.yellow('โš ๏ธ Using default config due to error:'), error.message); this.config = { ...this.defaultConfig }; } } async loadSecrets() { try { if (await fs.pathExists(this.secretsFile)) { const encryptedData = await fs.readFile(this.secretsFile); this.secrets = this.decryptSecrets(encryptedData); } } catch (error) { console.error(chalk.yellow('โš ๏ธ Could not load secrets:'), error.message); this.secrets = {}; } } async saveConfig() { try { await fs.writeJson(this.configFile, this.config, { spaces: 2 }); } catch (error) { console.error(chalk.red('โŒ Failed to save config:'), error.message); } } async saveSecrets() { try { const encryptedData = this.encryptSecrets(this.secrets); await fs.writeFile(this.secretsFile, encryptedData); } catch (error) { console.error(chalk.red('โŒ Failed to save secrets:'), error.message); } } mergeConfig(defaultConfig, userConfig) { const merged = { ...defaultConfig }; for (const [key, value] of Object.entries(userConfig)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { merged[key] = this.mergeConfig(defaultConfig[key] || {}, value); } else { merged[key] = value; } } return merged; } encryptSecrets(secrets) { const algorithm = 'aes-256-cbc'; const key = this.getEncryptionKey(); const iv = crypto.randomBytes(16); const cipher = crypto.createCipher(algorithm, key); let encrypted = cipher.update(JSON.stringify(secrets), 'utf8', 'hex'); encrypted += cipher.final('hex'); return Buffer.concat([iv, Buffer.from(encrypted, 'hex')]); } decryptSecrets(encryptedData) { try { const algorithm = 'aes-256-cbc'; const key = this.getEncryptionKey(); const iv = encryptedData.slice(0, 16); const encrypted = encryptedData.slice(16); const decipher = crypto.createDecipher(algorithm, key); let decrypted = decipher.update(encrypted.toString('hex'), 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } catch (error) { console.error(chalk.yellow('โš ๏ธ Could not decrypt secrets, using empty secrets')); return {}; } } getEncryptionKey() { // Generate a key based on machine-specific information const machineId = os.hostname() + os.userInfo().username; return crypto.createHash('sha256').update(machineId).digest(); } async get(keyPath) { const keys = keyPath.split('.'); let value = this.config; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return undefined; } } // Check if it's a secret reference if (typeof value === 'string' && value.startsWith('secret:')) { const secretKey = value.replace('secret:', ''); return this.secrets[secretKey]; } return value; } async set(keyPath, value) { const keys = keyPath.split('.'); const lastKey = keys.pop(); let target = this.config; for (const key of keys) { if (!(key in target) || typeof target[key] !== 'object') { target[key] = {}; } target = target[key]; } target[lastKey] = value; await this.saveConfig(); } async setSecret(key, value) { this.secrets[key] = value; await this.saveSecrets(); // Update config to reference the secret if (key === 'geminiApiKey') { await this.set('gemini.apiKey', `secret:${key}`); } } async handleCommand(options) { if (options.setupApi) { await this.setupApiKey(); } else if (options.set) { await this.setConfigValue(options.set); } else if (options.get) { await this.getConfigValue(options.get); } else if (options.list) { await this.listConfig(); } else if (options.reset) { await this.resetConfig(); } else { await this.showConfigMenu(); } } async setupApiKey() { console.log(chalk.cyan('๐Ÿ”‘ API Key Setup\n')); console.log(chalk.blue('To use Blue Beatle AI features, you need a Gemini API key.')); console.log(chalk.gray('Get your free API key at: https://makersuite.google.com/app/apikey\n')); const { apiKey } = await inquirer.prompt([{ type: 'password', name: 'apiKey', message: 'Enter your Gemini API key:', mask: '*', validate: input => { if (!input.trim()) { return 'API key is required'; } if (!input.startsWith('AIza')) { return 'Invalid Gemini API key format'; } return true; } }]); await this.setSecret('geminiApiKey', apiKey.trim()); console.log(chalk.green('โœ… API key saved securely!')); // Test the API key console.log(chalk.blue('๐Ÿงช Testing API key...')); try { const { GoogleGenerativeAI } = require('@google/generative-ai'); const genAI = new GoogleGenerativeAI(apiKey); const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); await model.generateContent('Hello'); console.log(chalk.green('โœ… API key is working correctly!')); } catch (error) { console.log(chalk.red('โŒ API key test failed:'), error.message); console.log(chalk.yellow('๐Ÿ’ก Please check your API key and try again.')); } } async setConfigValue(keyValue) { const [key, value] = keyValue.split('='); if (!key || value === undefined) { console.error(chalk.red('โŒ Invalid format. Use: key=value')); return; } try { // Try to parse as JSON for complex values let parsedValue; try { parsedValue = JSON.parse(value); } catch { parsedValue = value; } await this.set(key, parsedValue); console.log(chalk.green(`โœ… Set ${key} = ${value}`)); } catch (error) { console.error(chalk.red('โŒ Failed to set config:'), error.message); } } async getConfigValue(key) { try { const value = await this.get(key); if (value !== undefined) { console.log(chalk.cyan(`${key}:`), JSON.stringify(value, null, 2)); } else { console.log(chalk.yellow(`โš ๏ธ Key '${key}' not found`)); } } catch (error) { console.error(chalk.red('โŒ Failed to get config:'), error.message); } } async listConfig() { console.log(chalk.cyan('โš™๏ธ Current Configuration:\n')); this.printConfigSection('User Settings', this.config.user); this.printConfigSection('AI Settings', this.config.ai); this.printConfigSection('Terminal Settings', this.config.terminal); this.printConfigSection('Analysis Settings', this.config.analysis); this.printConfigSection('Project Settings', this.config.project); console.log(chalk.gray('\n๐Ÿ“ Config file location:'), this.configFile); console.log(chalk.gray('๐Ÿ” Secrets file location:'), this.secretsFile); } printConfigSection(title, section) { console.log(chalk.blue(`${title}:`)); for (const [key, value] of Object.entries(section)) { if (typeof value === 'object') { console.log(chalk.gray(` ${key}:`)); for (const [subKey, subValue] of Object.entries(value)) { console.log(chalk.gray(` ${subKey}: ${JSON.stringify(subValue)}`)); } } else { const displayValue = typeof value === 'string' && value.startsWith('secret:') ? '***hidden***' : JSON.stringify(value); console.log(chalk.gray(` ${key}: ${displayValue}`)); } } console.log(''); } async resetConfig() { const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: chalk.red('โš ๏ธ This will reset all configuration to defaults. Continue?'), default: false }]); if (confirm) { this.config = { ...this.defaultConfig }; await this.saveConfig(); console.log(chalk.green('โœ… Configuration reset to defaults')); } else { console.log(chalk.yellow('โญ๏ธ Reset cancelled')); } } async showConfigMenu() { console.log(chalk.cyan('โš™๏ธ Configuration Menu\n')); const { action } = await inquirer.prompt([{ type: 'list', name: 'action', message: 'What would you like to configure?', choices: [ { name: '๐Ÿ”‘ Setup API Key', value: 'api' }, { name: '๐Ÿ‘ค User Settings', value: 'user' }, { name: '๐Ÿค– AI Settings', value: 'ai' }, { name: 'โšก Terminal Settings', value: 'terminal' }, { name: '๐Ÿ” Analysis Settings', value: 'analysis' }, { name: '๐Ÿ“ Project Settings', value: 'project' }, { name: '๐Ÿ“‹ View All Settings', value: 'list' }, { name: '๐Ÿ”„ Reset to Defaults', value: 'reset' }, { name: 'โŒ Exit', value: 'exit' } ] }]); switch (action) { case 'api': await this.setupApiKey(); break; case 'user': await this.configureUserSettings(); break; case 'ai': await this.configureAISettings(); break; case 'terminal': await this.configureTerminalSettings(); break; case 'analysis': await this.configureAnalysisSettings(); break; case 'project': await this.configureProjectSettings(); break; case 'list': await this.listConfig(); break; case 'reset': await this.resetConfig(); break; case 'exit': console.log(chalk.yellow('๐Ÿ‘‹ Configuration menu closed')); break; } } async configureUserSettings() { console.log(chalk.cyan('๐Ÿ‘ค User Settings\n')); const userSettings = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Your name:', default: this.config.user.name }, { type: 'input', name: 'email', message: 'Your email:', default: this.config.user.email }, { type: 'list', name: 'theme', message: 'Preferred theme:', choices: ['default', 'dark', 'light', 'colorful'], default: this.config.user.preferences.theme }, { type: 'confirm', name: 'verboseOutput', message: 'Enable verbose output?', default: this.config.user.preferences.verboseOutput }, { type: 'confirm', name: 'autoUpdate', message: 'Enable automatic updates?', default: this.config.user.preferences.autoUpdate }, { type: 'confirm', name: 'telemetry', message: 'Enable anonymous telemetry?', default: this.config.user.preferences.telemetry } ]); this.config.user.name = userSettings.name; this.config.user.email = userSettings.email; this.config.user.preferences.theme = userSettings.theme; this.config.user.preferences.verboseOutput = userSettings.verboseOutput; this.config.user.preferences.autoUpdate = userSettings.autoUpdate; this.config.user.preferences.telemetry = userSettings.telemetry; await this.saveConfig(); console.log(chalk.green('โœ… User settings updated!')); } async configureAISettings() { console.log(chalk.cyan('๐Ÿค– AI Settings\n')); const aiSettings = await inquirer.prompt([ { type: 'list', name: 'model', message: 'AI Model:', choices: ['gemini-2.5-pro', 'gemini-1.5-pro', 'gemini-1.0-flash', 'gemini-2.0-flash'], default: this.config.ai.model }, { type: 'number', name: 'maxTokens', message: 'Max tokens per request:', default: this.config.ai.maxTokens, validate: input => input > 0 && input <= 32768 }, { type: 'number', name: 'temperature', message: 'Temperature (0.0-1.0):', default: this.config.ai.temperature, validate: input => input >= 0 && input <= 1 }, { type: 'number', name: 'timeout', message: 'Request timeout (ms):', default: this.config.ai.timeout, validate: input => input > 0 } ]); Object.assign(this.config.ai, aiSettings); await this.saveConfig(); console.log(chalk.green('โœ… AI settings updated!')); } async configureTerminalSettings() { console.log(chalk.cyan('โšก Terminal Settings\n')); const terminalSettings = await inquirer.prompt([ { type: 'confirm', name: 'safeMode', message: 'Enable safe mode (confirm dangerous commands)?', default: this.config.terminal.safeMode }, { type: 'confirm', name: 'logCommands', message: 'Log all executed commands?', default: this.config.terminal.logCommands }, { type: 'confirm', name: 'confirmDangerous', message: 'Confirm before executing dangerous commands?', default: this.config.terminal.confirmDangerous }, { type: 'number', name: 'historySize', message: 'Command history size:', default: this.config.terminal.historySize, validate: input => input > 0 } ]); Object.assign(this.config.terminal, terminalSettings); await this.saveConfig(); console.log(chalk.green('โœ… Terminal settings updated!')); } async configureAnalysisSettings() { console.log(chalk.cyan('๐Ÿ” Analysis Settings\n')); const analysisSettings = await inquirer.prompt([ { type: 'confirm', name: 'autoFix', message: 'Enable automatic fixing of issues?', default: this.config.analysis.autoFix }, { type: 'confirm', name: 'watchMode', message: 'Enable file watching by default?', default: this.config.analysis.watchMode }, { type: 'confirm', name: 'notifications', message: 'Enable notifications for issues?', default: this.config.analysis.notifications }, { type: 'list', name: 'reportFormat', message: 'Default report format:', choices: ['table', 'json', 'markdown'], default: this.config.analysis.reportFormat } ]); Object.assign(this.config.analysis, analysisSettings); await this.saveConfig(); console.log(chalk.green('โœ… Analysis settings updated!')); } async configureProjectSettings() { console.log(chalk.cyan('๐Ÿ“ Project Settings\n')); const projectSettings = await inquirer.prompt([ { type: 'list', name: 'defaultTemplate', message: 'Default project template:', choices: ['react', 'node', 'vue', 'angular', 'python', 'rust', 'go'], default: this.config.project.defaultTemplate }, { type: 'confirm', name: 'autoGitInit', message: 'Automatically initialize Git repository?', default: this.config.project.autoGitInit }, { type: 'confirm', name: 'createReadme', message: 'Automatically create README.md?', default: this.config.project.createReadme }, { type: 'confirm', name: 'setupDocker', message: 'Setup Docker files by default?', default: this.config.project.setupDocker } ]); Object.assign(this.config.project, projectSettings); await this.saveConfig(); console.log(chalk.green('โœ… Project settings updated!')); } async runSetup() { console.log(chalk.cyan('๐Ÿš€ MacAdida Initial Setup\n')); console.log(chalk.blue('Welcome to MacAdida AI-Powered Development Assistant!')); console.log(chalk.gray('Let\'s configure your environment for the best experience.\n')); // User information await this.configureUserSettings(); console.log(''); // API key setup const { setupApi } = await inquirer.prompt([{ type: 'confirm', name: 'setupApi', message: 'Would you like to setup your Gemini API key now?', default: true }]); if (setupApi) { await this.setupApiKey(); } else { console.log(chalk.yellow('โš ๏ธ You can setup your API key later with: blue-beatle config --setup-api')); } console.log(''); // Quick preferences const { quickSetup } = await inquirer.prompt([{ type: 'confirm', name: 'quickSetup', message: 'Configure additional preferences now?', default: false }]); if (quickSetup) { await this.configureTerminalSettings(); await this.configureAnalysisSettings(); } console.log(chalk.green('\n๐ŸŽ‰ Setup complete! Blue Beatle is ready to use.')); console.log(chalk.cyan('๐Ÿ’ก Try: blue-beatle ai "help me analyze my code"')); console.log(chalk.cyan('๐Ÿ’ก Or: blue-beatle interactive')); } } module.exports = ConfigManager;