UNPKG

@dharshansr/gitgenius

Version:

AI-powered commit message generator with enhanced features

560 lines 20.5 kB
import Conf from 'conf'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { writeFileSync, readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import crypto from 'crypto'; import { validateConfig, migrateConfig, needsMigration, CONFIG_VERSION } from './ConfigSchema.js'; import { getTemplate, listTemplates } from './ConfigTemplates.js'; import { SecurityUtils } from '../utils/SecurityUtils.js'; import { SecurityManager } from './SecurityConfig.js'; export class ConfigManager { constructor() { this.projectConfig = null; this.securityManager = new SecurityManager(); // Generate or retrieve encryption key for API keys this.encryptionKey = this.getOrCreateEncryptionKey(); // Global configuration (system-wide) this.globalConfig = new Conf({ projectName: 'gitgenius', projectVersion: CONFIG_VERSION, defaults: this.getDefaultConfig() }); // User configuration (default, main instance) this.config = new Conf({ projectName: 'gitgenius', projectVersion: CONFIG_VERSION, configName: 'config', defaults: this.getDefaultConfig() }); // Initialize project config if in a git repository this.initProjectConfig(); // Auto-migrate if needed this.autoMigrate(); } /** * Get or create encryption key for secure API key storage */ getOrCreateEncryptionKey() { const keyPath = join(homedir(), '.gitgenius', '.key'); try { if (existsSync(keyPath)) { return readFileSync(keyPath, 'utf8').trim(); } else { // Generate new key const key = crypto.randomBytes(32).toString('hex'); const keyDir = join(homedir(), '.gitgenius'); // Create directory if it doesn't exist if (!existsSync(keyDir)) { const { mkdirSync } = require('fs'); mkdirSync(keyDir, { recursive: true, mode: 0o700 }); } // Write key file with restricted permissions writeFileSync(keyPath, key, { mode: 0o600 }); return key; } } catch (error) { // Fallback to a machine-specific key return crypto.createHash('sha256') .update(homedir() + process.platform) .digest('hex'); } } getDefaultConfig() { return { provider: 'openai', model: 'gpt-3.5-turbo', apiKey: null, maxTokens: 150, temperature: 0.7, commitTypes: [ 'feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'ci', 'build' ], configVersion: CONFIG_VERSION }; } initProjectConfig() { try { // Check if we're in a git repository const gitDir = process.cwd(); if (existsSync(join(gitDir, '.git'))) { // We're in a git repo, initialize project config this.projectConfig = new Conf({ projectName: 'gitgenius', configName: 'project-config', cwd: join(gitDir, '.gitgenius') }); } } catch (error) { // Not in a git repo or can't access, that's ok this.projectConfig = null; } } autoMigrate() { const version = this.config.get('configVersion'); if (needsMigration(version)) { console.log(chalk.yellow('⚠ Configuration needs migration. Migrating...')); const currentConfig = this.config.store; const migratedConfig = migrateConfig(currentConfig, version); // Update configuration Object.entries(migratedConfig).forEach(([key, value]) => { this.config.set(key, value); }); console.log(chalk.green(`✓ Configuration migrated to version ${CONFIG_VERSION}`)); } } async handleConfig(key, value, options) { if (options?.reset) { await this.resetConfig(); return; } if (options?.backup) { await this.backupConfig(); return; } if (options?.restore) { await this.restoreConfig(options.restore); return; } if (options?.validate) { await this.validateCurrentConfig(); return; } if (options?.template) { await this.applyTemplate(options.template); return; } if (options?.export) { await this.exportConfig(options.export); return; } if (options?.import) { await this.importConfig(options.import); return; } if (options?.migrate) { await this.migrateConfigManual(); return; } if (options?.list || (!key && !value)) { this.listConfig(); return; } if (key && !value) { await this.setConfigInteractive(key); return; } if (key && value) { this.setConfig(key, value); return; } } // Configuration inheritance: project > user > global getConfigWithInheritance(key) { // Check project config first if (this.projectConfig && this.projectConfig.has(key)) { return this.projectConfig.get(key); } // Then user config if (this.config.has(key)) { return this.config.get(key); } // Finally global config return this.globalConfig.get(key); } // Backup configuration async backupConfig() { try { const backup = { version: CONFIG_VERSION, timestamp: new Date().toISOString(), config: this.config.store }; const backupPath = join(this.config.path, '..', `config-backup-${Date.now()}.json`); writeFileSync(backupPath, JSON.stringify(backup, null, 2)); console.log(chalk.green('✓ Configuration backed up successfully')); console.log(chalk.blue(` Location: ${backupPath}`)); } catch (error) { console.error(chalk.red('✗ Failed to backup configuration:'), error); } } // Restore configuration from backup async restoreConfig(backupPath) { try { if (!existsSync(backupPath)) { console.error(chalk.red('✗ Backup file not found')); return; } const backupData = JSON.parse(readFileSync(backupPath, 'utf-8')); // Validate backup const validation = validateConfig(backupData.config); if (!validation.valid) { console.error(chalk.red('✗ Invalid backup file:')); validation.errors?.forEach(err => console.error(chalk.red(` - ${err}`))); return; } // Confirm restore const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: `Restore configuration from ${backupData.timestamp}?`, default: false } ]); if (!confirmed) { console.log(chalk.yellow('Restore cancelled')); return; } // Restore configuration this.config.clear(); Object.entries(backupData.config).forEach(([key, value]) => { this.config.set(key, value); }); console.log(chalk.green('✓ Configuration restored successfully')); } catch (error) { console.error(chalk.red('✗ Failed to restore configuration:'), error); } } // Validate current configuration async validateCurrentConfig() { const config = this.config.store; const validation = validateConfig(config); if (validation.valid) { console.log(chalk.green('✓ Configuration is valid')); console.log(chalk.blue(` Version: ${config.configVersion || 'unknown'}`)); } else { console.log(chalk.red('✗ Configuration validation failed:')); validation.errors?.forEach(err => { console.log(chalk.red(` - ${err}`)); }); // Offer to fix const { fix } = await inquirer.prompt([ { type: 'confirm', name: 'fix', message: 'Would you like to migrate and fix the configuration?', default: true } ]); if (fix) { await this.migrateConfigManual(); } } } // Apply a configuration template async applyTemplate(templateName) { const template = getTemplate(templateName); if (!template) { console.log(chalk.yellow('✗ Template not found')); console.log(chalk.blue('\nAvailable templates:')); listTemplates().forEach(t => { console.log(` ${chalk.yellow(t.name)}: ${chalk.white(t.description)}`); }); return; } // Show template details console.log(chalk.blue(`\n📋 Template: ${template.name}`)); console.log(chalk.white(` ${template.description}`)); console.log(chalk.blue('\nConfiguration:')); Object.entries(template.config).forEach(([key, value]) => { console.log(` ${chalk.yellow(key)}: ${chalk.white(JSON.stringify(value))}`); }); const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: 'Apply this template?', default: true } ]); if (!confirmed) { console.log(chalk.yellow('Template application cancelled')); return; } // Apply template Object.entries(template.config).forEach(([key, value]) => { this.config.set(key, value); }); console.log(chalk.green('✓ Template applied successfully')); } // Export configuration async exportConfig(exportPath) { try { const config = this.config.store; writeFileSync(exportPath, JSON.stringify(config, null, 2)); console.log(chalk.green('✓ Configuration exported successfully')); console.log(chalk.blue(` Location: ${exportPath}`)); } catch (error) { console.error(chalk.red('✗ Failed to export configuration:'), error); } } // Import configuration async importConfig(importPath) { try { if (!existsSync(importPath)) { console.error(chalk.red('✗ Import file not found')); return; } const importedConfig = JSON.parse(readFileSync(importPath, 'utf-8')); // Validate imported config const validation = validateConfig(importedConfig); if (!validation.valid) { console.error(chalk.red('✗ Invalid configuration file:')); validation.errors?.forEach(err => console.error(chalk.red(` - ${err}`))); return; } const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: 'Import this configuration?', default: false } ]); if (!confirmed) { console.log(chalk.yellow('Import cancelled')); return; } // Import configuration Object.entries(importedConfig).forEach(([key, value]) => { this.config.set(key, value); }); console.log(chalk.green('✓ Configuration imported successfully')); } catch (error) { console.error(chalk.red('✗ Failed to import configuration:'), error); } } // Manual migration async migrateConfigManual() { console.log(chalk.blue('🔄 Migrating configuration...')); const currentConfig = this.config.store; const version = this.config.get('configVersion'); const migratedConfig = migrateConfig(currentConfig, version); // Update configuration this.config.clear(); Object.entries(migratedConfig).forEach(([key, value]) => { this.config.set(key, value); }); console.log(chalk.green(`✓ Configuration migrated to version ${CONFIG_VERSION}`)); // Validate after migration const validation = validateConfig(this.config.store); if (validation.valid) { console.log(chalk.green('✓ Configuration is now valid')); } else { console.log(chalk.yellow('⚠ Some validation issues remain:')); validation.errors?.forEach(err => console.log(chalk.yellow(` - ${err}`))); } } async resetConfig() { const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: 'Are you sure you want to reset all configuration?', default: false } ]); if (confirmed) { this.config.clear(); console.log(chalk.green('✓ Configuration reset successfully')); } } listConfig() { const config = this.config.store; console.log(chalk.blue('📋 Current Configuration:')); // Always mask API keys, whether from config or environment const apiKeys = [ 'apiKey', 'GITGENIUS_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', ]; // Print config keys, masking any API key Object.entries(config).forEach(([key, value]) => { if (apiKeys.includes(key) && value) { console.log(` ${chalk.yellow(key)}: ${chalk.gray('***hidden***')}`); } else { console.log(` ${chalk.yellow(key)}: ${chalk.white(JSON.stringify(value))}`); } }); // Also show if API keys are set in environment (but always masked) apiKeys.forEach((envKey) => { if (process.env[envKey]) { console.log(` ${chalk.yellow(envKey)} (env): ${chalk.gray('***hidden***')}`); } }); } async setConfigInteractive(key) { if (key === 'apiKey') { await this.setApiKey(); } else if (key === 'provider') { await this.setProvider(); } else if (key === 'model') { await this.setModel(); } else { const { value } = await inquirer.prompt([ { type: 'input', name: 'value', message: `Enter value for ${key}:` } ]); this.setConfig(key, value); } } async setApiKey() { const { apiKey } = await inquirer.prompt([ { type: 'password', name: 'apiKey', message: 'Enter your API key:', mask: '*', validate: (input) => { if (!SecurityUtils.validateApiKey(input)) { return 'Invalid API key format. API key must be at least 20 characters.'; } return true; } } ]); // Encrypt the API key before storing const encryptedKey = SecurityUtils.encrypt(apiKey, this.encryptionKey); this.setConfig('apiKey', encryptedKey); this.setConfig('apiKeyEncrypted', true); // Audit log this.securityManager.auditLog('api_key_updated', { timestamp: new Date().toISOString() }); } async setProvider() { const { provider } = await inquirer.prompt([ { type: 'list', name: 'provider', message: 'Select AI provider:', choices: [ { name: 'OpenAI', value: 'openai' }, { name: 'Google Gemini', value: 'gemini' }, { name: 'Anthropic Claude', value: 'anthropic' } ] } ]); this.setConfig('provider', provider); } async setModel() { const provider = this.getConfig('provider'); let choices = []; switch (provider) { case 'openai': choices = [ { name: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' }, { name: 'GPT-4', value: 'gpt-4' }, { name: 'GPT-4 Turbo', value: 'gpt-4-turbo-preview' } ]; break; case 'gemini': choices = [ { name: 'Gemini Pro', value: 'gemini-pro' }, { name: 'Gemini Pro Vision', value: 'gemini-pro-vision' } ]; break; case 'anthropic': choices = [ { name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' }, { name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' }, { name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' } ]; break; } const { model } = await inquirer.prompt([ { type: 'list', name: 'model', message: 'Select model:', choices } ]); this.setConfig('model', model); } setConfig(key, value) { this.config.set(key, value); console.log(chalk.green(`✓ ${key} set successfully`)); } setConfigValue(key, value) { this.config.set(key, value); } getConfig(key, level) { if (level) { // Get from specific level switch (level) { case 'project': return this.projectConfig?.get(key); case 'user': return this.config.get(key); case 'global': return this.globalConfig.get(key); } } // Use inheritance: project > user > global return this.getConfigWithInheritance(key); } hasApiKey() { return !!this.getApiKey(); } getApiKey() { // Check environment variables with different possible names const envKey = process.env.GITGENIUS_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY; if (envKey) { // Environment variables are not encrypted return envKey; } // Get from config const storedKey = this.getConfig('apiKey'); const isEncrypted = this.getConfig('apiKeyEncrypted'); if (!storedKey) { return ''; } // Decrypt if encrypted if (isEncrypted) { try { return SecurityUtils.decrypt(storedKey, this.encryptionKey); } catch (error) { console.error(chalk.yellow('⚠ Failed to decrypt API key. Please reconfigure.')); return ''; } } // Legacy unencrypted key - migrate it if (storedKey && !isEncrypted) { try { const encryptedKey = SecurityUtils.encrypt(storedKey, this.encryptionKey); this.setConfig('apiKey', encryptedKey); this.setConfig('apiKeyEncrypted', true); } catch (error) { // If encryption fails, continue with unencrypted key console.warn(chalk.yellow('⚠ Could not encrypt API key')); } } return storedKey; } } //# sourceMappingURL=ConfigManager.js.map