@claude-vector/cli
Version:
CLI for Claude-integrated vector search
735 lines (623 loc) • 18.9 kB
JavaScript
/**
* 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');
}
}