UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

735 lines (623 loc) 18.9 kB
/** * ConfigManager - 統一設定管理システム * * 機能: * - 統一設定ファイル (.claude.config.js) * - 既存設定互換性 (.claude-watch.config.js) * - 環境変数統合 * - 階層的設定統合 (デフォルト→ファイル→環境変数→引数) * - 設定バリデーション * - 動的設定変更 * - ホットリロード対応 */ import { EventEmitter } from 'events'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import { watch } from 'fs'; /** * 統一設定のデフォルト値 */ const UNIFIED_CONFIG_DEFAULTS = { // プロジェクト基本情報 projectName: null, version: '1.0.0', // 検索設定 search: { threshold: 0.3, maxResults: 20, aiOptimization: true, semanticChunking: true, phaseAdaptation: true, claudeOptimization: true }, // 監視設定 watch: { enabled: false, patterns: ['**/*.{js,jsx,ts,tsx,md}'], ignorePatterns: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '.next/**', 'coverage/**' ], // パフォーマンス制御 throttling: { batchSize: 5, maxConcurrent: 3, apiCooldown: 1000, adaptiveThrottling: true, maxDelay: 30000 }, // フィードバック制御 feedback: { level: 'normal', // 'silent', 'normal', 'verbose' logFile: '.claude-watch.log', colorOutput: true, enableEmojis: true, showProgress: true, showStatistics: false, notifications: false }, // プロセス制御 process: { allowMultiple: false, autoRestart: false, shutdownTimeout: 30000, healthCheckInterval: 5000 }, // ファイル制御 files: { maxFiles: 10000, memoryThreshold: 500, debounceMs: 500, backupEnabled: true } }, // OpenAI API設定 openai: { apiKey: null, // 環境変数から読み込み model: 'text-embedding-3-small', maxTokens: 8000, timeout: 30000 }, // 出力・ログ設定 output: { format: 'structured', // 'structured', 'simple', 'json' verbosity: 'normal', // 'silent', 'normal', 'verbose', 'debug' logLevel: 'info', // 'error', 'warn', 'info', 'debug' timestamp: false }, // パフォーマンス設定 performance: { cacheEnabled: true, cacheTtl: 3600000, // 1時間 maxMemoryUsage: 1024, // MB enableProfiling: false }, // セキュリティ設定 security: { ignoreSecretFiles: true, secretPatterns: [ '**/*.key', '**/*.pem', '**/.env*', '**/secrets/**' ] } }; /** * 設定ファイル候補(優先度順) */ const CONFIG_FILE_CANDIDATES = [ '.claude.config.js', '.claude.config.json', '.claude-watch.config.js', 'claude.config.js', 'package.json' // claudeVector フィールド ]; /** * 環境変数マッピング */ const ENV_MAPPINGS = { 'OPENAI_API_KEY': 'openai.apiKey', 'CLAUDE_LOG_LEVEL': 'output.logLevel', 'CLAUDE_VERBOSITY': 'output.verbosity', 'CLAUDE_WATCH_ENABLED': 'watch.enabled', 'CLAUDE_FEEDBACK_LEVEL': 'watch.feedback.level', 'CLAUDE_MAX_FILES': 'watch.files.maxFiles', 'CLAUDE_BATCH_SIZE': 'watch.throttling.batchSize' }; export class ConfigManager extends EventEmitter { constructor(options = {}) { super(); this.options = options; this.projectRoot = null; this.configFilePath = null; this.isInitialized = false; // 現在の設定 this.config = { ...UNIFIED_CONFIG_DEFAULTS }; // 設定源の追跡 this.configSources = { defaults: { ...UNIFIED_CONFIG_DEFAULTS }, file: {}, environment: {}, runtime: {}, commandLine: {} }; // 監視設定 this.watchConfig = false; this.configWatcher = null; // バリデーション結果 this.validationErrors = []; this.validationWarnings = []; } /** * 初期化 */ async initialize(projectRoot, options = {}) { if (this.isInitialized) { return this.config; } this.projectRoot = projectRoot; this.watchConfig = options.watchConfig ?? false; try { // 1. 設定ファイルの検出・読み込み await this.loadConfigFile(); // 2. 環境変数の読み込み this.loadEnvironmentVariables(); // 3. ランタイム設定の適用 if (options.runtimeConfig) { this.configSources.runtime = options.runtimeConfig; } // 4. コマンドライン引数の適用 if (options.commandLineArgs) { this.configSources.commandLine = this.parseCommandLineArgs(options.commandLineArgs); } // 5. 設定の統合 this.mergeAllConfigs(); // 6. 設定の検証 this.validateConfig(); // 7. 設定監視の開始 if (this.watchConfig && this.configFilePath) { await this.startConfigWatching(); } this.isInitialized = true; this.emit('initialized', { config: this.config, sources: this.configSources, filePath: this.configFilePath, validationErrors: this.validationErrors, validationWarnings: this.validationWarnings }); return this.config; } catch (error) { this.emit('error', { type: 'initialization', error }); throw error; } } /** * 設定ファイルの検出・読み込み */ async loadConfigFile() { for (const fileName of CONFIG_FILE_CANDIDATES) { const filePath = path.join(this.projectRoot, fileName); if (fsSync.existsSync(filePath)) { try { const config = await this.loadConfigFromFile(filePath); if (config && Object.keys(config).length > 0) { this.configFilePath = filePath; this.configSources.file = config; console.log(`📄 Loaded configuration from: ${path.relative(this.projectRoot, filePath)}`); return; } } catch (error) { console.warn(`⚠️ Failed to load config from ${fileName}:`, error.message); } } } console.log('📄 No configuration file found, using defaults'); } /** * ファイルから設定読み込み */ async loadConfigFromFile(filePath) { const ext = path.extname(filePath); switch (ext) { case '.js': return await this.loadJavaScriptConfig(filePath); case '.json': return await this.loadJsonConfig(filePath); default: if (path.basename(filePath) === 'package.json') { return await this.loadPackageJsonConfig(filePath); } throw new Error(`Unsupported config file format: ${ext}`); } } /** * JavaScript設定ファイル読み込み */ async loadJavaScriptConfig(filePath) { try { // ES modules対応 const fileUrl = pathToFileURL(filePath).href; const module = await import(fileUrl + '?t=' + Date.now()); // キャッシュ回避 // default export または module.exports const config = module.default || module; if (typeof config === 'function') { // 関数型設定(動的設定) return await config({ projectRoot: this.projectRoot }); } else { return config; } } catch (error) { throw new Error(`Failed to load JavaScript config: ${error.message}`); } } /** * JSON設定ファイル読み込み */ async loadJsonConfig(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to load JSON config: ${error.message}`); } } /** * package.json設定読み込み */ async loadPackageJsonConfig(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const packageJson = JSON.parse(content); // claudeVector フィールドから設定を取得 return packageJson.claudeVector || {}; } catch (error) { throw new Error(`Failed to load package.json config: ${error.message}`); } } /** * 環境変数読み込み */ loadEnvironmentVariables() { const envConfig = {}; for (const [envVar, configPath] of Object.entries(ENV_MAPPINGS)) { const value = process.env[envVar]; if (value !== undefined) { this.setNestedValue(envConfig, configPath, this.parseEnvValue(value)); } } this.configSources.environment = envConfig; } /** * 環境変数値のパース */ parseEnvValue(value) { // 真偽値 if (value === 'true') return true; if (value === 'false') return false; // 数値 if (/^\d+$/.test(value)) return parseInt(value); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); // 文字列 return value; } /** * コマンドライン引数パース */ parseCommandLineArgs(args) { const config = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const key = arg.substring(2); const value = args[i + 1]; // 設定キーのマッピング const configPath = this.mapArgToConfig(key); if (configPath && value && !value.startsWith('--')) { this.setNestedValue(config, configPath, this.parseEnvValue(value)); i++; // 値をスキップ } } } return config; } /** * 引数から設定パスへのマッピング */ mapArgToConfig(arg) { const mappings = { 'feedback': 'watch.feedback.level', 'patterns': 'watch.patterns', 'ignore': 'watch.ignorePatterns', 'batch-size': 'watch.throttling.batchSize', 'max-concurrent': 'watch.throttling.maxConcurrent', 'threshold': 'search.threshold', 'max-results': 'search.maxResults', 'log-level': 'output.logLevel', 'verbose': 'output.verbosity' }; return mappings[arg]; } /** * 全設定の統合 */ mergeAllConfigs() { // 優先度順に統合: defaults < file < environment < runtime < commandLine this.config = this.deepMerge( this.configSources.defaults, this.configSources.file, this.configSources.environment, this.configSources.runtime, this.configSources.commandLine ); // 後処理 this.postProcessConfig(); } /** * 設定の後処理 */ postProcessConfig() { // プロジェクト名の自動設定 if (!this.config.projectName && this.projectRoot) { this.config.projectName = path.basename(this.projectRoot); } // OpenAI APIキーの環境変数フォールバック if (!this.config.openai.apiKey) { this.config.openai.apiKey = process.env.OPENAI_API_KEY; } // パスの正規化 this.config.watch.patterns = this.normalizePatterns(this.config.watch.patterns); this.config.watch.ignorePatterns = this.normalizePatterns(this.config.watch.ignorePatterns); } /** * パターンの正規化 */ normalizePatterns(patterns) { if (typeof patterns === 'string') { return [patterns]; } return patterns || []; } /** * 設定検証 */ validateConfig() { this.validationErrors = []; this.validationWarnings = []; // 必須項目チェック this.validateRequired(); // 型チェック this.validateTypes(); // 値の範囲チェック this.validateRanges(); // 依存関係チェック this.validateDependencies(); // 互換性チェック this.validateCompatibility(); // 結果報告 if (this.validationErrors.length > 0) { console.error('❌ Configuration validation errors:'); this.validationErrors.forEach(error => console.error(` - ${error}`)); } if (this.validationWarnings.length > 0) { console.warn('⚠️ Configuration warnings:'); this.validationWarnings.forEach(warning => console.warn(` - ${warning}`)); } } /** * 必須項目検証 */ validateRequired() { if (this.config.watch.enabled && !this.config.openai.apiKey) { this.validationErrors.push('OpenAI API key is required when watch is enabled'); } } /** * 型検証 */ validateTypes() { const typeChecks = [ { path: 'watch.enabled', type: 'boolean' }, { path: 'watch.throttling.batchSize', type: 'number' }, { path: 'watch.throttling.maxConcurrent', type: 'number' }, { path: 'watch.files.maxFiles', type: 'number' }, { path: 'search.threshold', type: 'number' }, { path: 'search.maxResults', type: 'number' } ]; for (const check of typeChecks) { const value = this.getNestedValue(this.config, check.path); if (value !== undefined && typeof value !== check.type) { this.validationErrors.push(`${check.path} must be of type ${check.type}, got ${typeof value}`); } } } /** * 範囲検証 */ validateRanges() { const rangeChecks = [ { path: 'watch.throttling.batchSize', min: 1, max: 100 }, { path: 'watch.throttling.maxConcurrent', min: 1, max: 20 }, { path: 'watch.files.maxFiles', min: 1, max: 100000 }, { path: 'search.threshold', min: 0, max: 1 }, { path: 'search.maxResults', min: 1, max: 1000 } ]; for (const check of rangeChecks) { const value = this.getNestedValue(this.config, check.path); if (typeof value === 'number') { if (value < check.min || value > check.max) { this.validationWarnings.push(`${check.path} (${value}) is outside recommended range [${check.min}, ${check.max}]`); } } } } /** * 依存関係検証 */ validateDependencies() { // 監視機能とAPI設定の依存関係 if (this.config.watch.enabled && !this.config.openai.apiKey) { this.validationErrors.push('Watch mode requires OpenAI API key'); } } /** * 互換性検証 */ validateCompatibility() { // フィードバックレベルの検証 const validLevels = ['silent', 'normal', 'verbose']; if (!validLevels.includes(this.config.watch.feedback.level)) { this.validationErrors.push(`Invalid feedback level: ${this.config.watch.feedback.level}`); } } /** * 設定監視開始 */ async startConfigWatching() { if (!this.configFilePath || this.configWatcher) { return; } try { this.configWatcher = watch(this.configFilePath, async (eventType) => { if (eventType === 'change') { console.log('🔄 Configuration file changed, reloading...'); await this.reloadConfig(); } }); console.log('👀 Started watching configuration file'); } catch (error) { console.warn('⚠️ Failed to start config watching:', error.message); } } /** * 設定リロード */ async reloadConfig() { try { const oldConfig = { ...this.config }; // 設定ファイルを再読み込み if (this.configFilePath) { this.configSources.file = await this.loadConfigFromFile(this.configFilePath); } // 設定を再統合 this.mergeAllConfigs(); this.validateConfig(); this.emit('config-reloaded', { oldConfig, newConfig: this.config, changes: this.getConfigChanges(oldConfig, this.config) }); console.log('✅ Configuration reloaded successfully'); } catch (error) { console.error('❌ Failed to reload configuration:', error.message); this.emit('error', { type: 'reload', error }); } } /** * 設定変更の検出 */ getConfigChanges(oldConfig, newConfig) { const changes = []; const compare = (obj1, obj2, path = '') => { for (const key in obj2) { const currentPath = path ? `${path}.${key}` : key; if (obj1[key] !== obj2[key]) { if (typeof obj2[key] === 'object' && typeof obj1[key] === 'object') { compare(obj1[key], obj2[key], currentPath); } else { changes.push({ path: currentPath, oldValue: obj1[key], newValue: obj2[key] }); } } } }; compare(oldConfig, newConfig); return changes; } /** * ユーティリティ関数群 */ deepMerge(...objects) { return objects.reduce((result, current) => { if (!current) return result; for (const key in current) { if (current[key] && typeof current[key] === 'object' && !Array.isArray(current[key])) { result[key] = this.deepMerge(result[key] || {}, current[key]); } else { result[key] = current[key]; } } return result; }, {}); } setNestedValue(obj, path, value) { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { if (!(keys[i] in current) || typeof current[keys[i]] !== 'object') { current[keys[i]] = {}; } current = current[keys[i]]; } current[keys[keys.length - 1]] = value; } getNestedValue(obj, path) { return path.split('.').reduce((current, key) => current?.[key], obj); } /** * 設定の取得・更新 */ get(path) { if (path) { return this.getNestedValue(this.config, path); } return { ...this.config }; } set(path, value) { this.setNestedValue(this.configSources.runtime, path, value); this.mergeAllConfigs(); this.validateConfig(); this.emit('config-updated', { path, value, config: this.config }); } /** * 設定のエクスポート */ export(format = 'json') { switch (format) { case 'json': return JSON.stringify(this.config, null, 2); case 'js': return `export default ${JSON.stringify(this.config, null, 2)};`; default: throw new Error(`Unsupported export format: ${format}`); } } /** * 設定ファイルの作成 */ async createConfigFile(filePath, format = 'js') { const content = this.export(format); await fs.writeFile(filePath, content); console.log(`📄 Configuration file created: ${filePath}`); } /** * クリーンアップ */ destroy() { if (this.configWatcher) { this.configWatcher.close(); this.configWatcher = null; } this.emit('destroyed'); } }