UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

516 lines (439 loc) 16.1 kB
/** * WatchCommand - ccvector watch コマンド群の実装 * * 機能: * - ccvector watch start - ファイル監視開始 * - ccvector watch stop - 監視停止 * - ccvector watch status - 監視状態確認 * - ccvector watch restart - 監視再起動 * - ccvector watch logs - ログ表示 */ import { WatchManager } from '../watch/watch-manager.js'; import { AdaptiveThrottler } from '../watch/adaptive-throttler.js'; import { ProcessController } from '../watch/process-controller.js'; import { FeedbackSystem } from '../watch/feedback-system.js'; import { ConfigManager } from '../config/unified-config.js'; import path from 'path'; import fs from 'fs/promises'; import fsSync from 'fs'; import dotenv from 'dotenv'; export class WatchCommand { constructor() { this.watchManager = null; this.configManager = null; this.projectRoot = null; } /** * メインエントリーポイント */ async execute(subCommand, args = []) { try { // プロジェクトルートの検出 this.projectRoot = await this.detectProjectRoot(); // 環境変数の読み込み this.loadEnvironmentVariables(); switch (subCommand) { case 'start': await this.startWatch(args); break; case 'stop': await this.stopWatch(args); break; case 'restart': await this.restartWatch(args); break; case 'status': await this.showStatus(args); break; case 'logs': await this.showLogs(args); break; case 'config': await this.manageConfig(args); break; default: this.showWatchHelp(); break; } // startコマンド以外は正常終了 if (subCommand !== 'start') { process.exit(0); } } catch (error) { console.error(`❌ Watch command error: ${error.message}`); if (error.message.includes('already running')) { console.log('\n💡 To stop the running process: ccvector watch stop'); console.log(' To check status: ccvector watch status'); } else if (error.message.includes('not running')) { console.log('\n💡 To start monitoring: ccvector watch start'); } else if (error.message.includes('OpenAI API key')) { console.log('\n💡 Set your OpenAI API key:'); console.log(' export OPENAI_API_KEY="your-api-key-here"'); console.log(' Or add it to .env file in your project'); } process.exit(1); } } /** * 監視開始 */ async startWatch(args) { console.log('🚀 Starting Claude Vector Watch...'); // 設定管理の初期化 this.configManager = new ConfigManager(); const config = await this.configManager.initialize(this.projectRoot, { watchConfig: true, commandLineArgs: args }); // 既存プロセスの確認 const runningProcesses = await ProcessController.getRunningProcesses(this.projectRoot); if (runningProcesses.length > 0 && !config.watch.process.allowMultiple) { throw new Error(`Watch process already running (PID: ${runningProcesses[0].pid})`); } // WatchManager の初期化 this.watchManager = new WatchManager(config.watch); await this.watchManager.initialize(this.projectRoot); // 監視開始 await this.watchManager.start(); // フォアグラウンドモードの場合はここで待機 if (!config.watch.process.daemonMode) { console.log('\n🔄 Monitoring files... (Press Ctrl+C to stop)'); // プロセス終了まで待機 await new Promise((resolve) => { process.on('SIGINT', resolve); process.on('SIGTERM', resolve); }); } } /** * 監視停止 */ async stopWatch(args) { const force = args.includes('--force') || args.includes('-f'); try { console.log('🛑 Stopping watch process...'); const result = await ProcessController.stopProcess(this.projectRoot, force); console.log(`✅ Watch process stopped (PID: ${result.pid})`); } catch (error) { if (error.message.includes('No running watch process')) { console.log('ℹ️ No watch process is currently running'); } else { throw error; } } } /** * 監視再起動 */ async restartWatch(args) { console.log('🔄 Restarting watch process...'); try { await this.stopWatch(['--force']); // 少し待機してからスタート await new Promise(resolve => setTimeout(resolve, 1000)); await this.startWatch(args); } catch (error) { console.log('⚠️ Stop failed, starting new process...'); await this.startWatch(args); } } /** * 監視状態表示 */ async showStatus(args) { console.log('📊 Watch Status\n'); try { const runningProcesses = await ProcessController.getRunningProcesses(this.projectRoot); if (runningProcesses.length === 0) { console.log('❌ No watch process running'); console.log('\n💡 To start monitoring: ccvector watch start'); return; } for (const proc of runningProcesses) { const uptime = Date.now() - new Date(proc.status.startTime).getTime(); const uptimeStr = this.formatDuration(uptime); console.log(`✅ Watch process running:`); console.log(` 📍 PID: ${proc.pid}`); console.log(` ⏱️ Uptime: ${uptimeStr}`); console.log(` 📁 Project: ${proc.status.cwd}`); console.log(` 📊 Files watched: ${proc.status.filesWatched || 'unknown'}`); console.log(` 🔄 Changes processed: ${proc.status.changesProcessed || 0}`); console.log(` 💾 Memory: ${Math.round(proc.status.memoryUsage?.heapUsed / 1024 / 1024) || 0}MB`); if (proc.status.lastActivity) { const lastActivity = new Date(proc.status.lastActivity); console.log(` 📝 Last activity: ${lastActivity.toLocaleString()}`); } } // ログファイル情報 const logFile = path.join(this.projectRoot, '.claude-watch.log'); if (fsSync.existsSync(logFile)) { const logStats = await fs.stat(logFile); const logSize = Math.round(logStats.size / 1024); console.log(`\n📄 Log file: ${logSize}KB (${logFile})`); } } catch (error) { console.error(`❌ Failed to get status: ${error.message}`); } } /** * ログ表示 */ async showLogs(args) { const logFile = path.join(this.projectRoot, '.claude-watch.log'); const follow = args.includes('--follow') || args.includes('-f'); const lines = args.includes('--lines') ? parseInt(args[args.indexOf('--lines') + 1]) || 50 : 50; if (!fsSync.existsSync(logFile)) { console.log('📄 No log file found'); console.log('💡 Start watching to generate logs: ccvector watch start'); return; } try { console.log(`📄 Watch Logs (last ${lines} lines):\n`); const content = await fs.readFile(logFile, 'utf-8'); const logLines = content.trim().split('\n').slice(-lines); for (const line of logLines) { try { const logEntry = JSON.parse(line); const timestamp = new Date(logEntry.timestamp).toLocaleString(); console.log(`[${timestamp}] ${logEntry.level}: ${logEntry.message}`); } catch { // 非JSON行はそのまま表示 console.log(line); } } if (follow) { console.log('\n👀 Following logs... (Press Ctrl+C to stop)\n'); // ファイル監視でリアルタイム表示 const { watch } = await import('fs'); const watcher = watch(logFile); let lastSize = (await fs.stat(logFile)).size; watcher.on('change', async () => { try { const stats = await fs.stat(logFile); if (stats.size > lastSize) { const newContent = await fs.readFile(logFile, 'utf-8'); const newLines = newContent.slice(lastSize).trim().split('\n'); for (const line of newLines) { if (line) { try { const logEntry = JSON.parse(line); const timestamp = new Date(logEntry.timestamp).toLocaleString(); console.log(`[${timestamp}] ${logEntry.level}: ${logEntry.message}`); } catch { console.log(line); } } } lastSize = stats.size; } } catch (error) { // ファイル読み込みエラーは無視 } }); await new Promise((resolve) => { process.on('SIGINT', () => { watcher.close(); resolve(); }); }); } } catch (error) { console.error(`❌ Failed to read logs: ${error.message}`); } } /** * 設定管理 */ async manageConfig(args) { const action = args[0]; switch (action) { case 'show': await this.showConfig(); break; case 'create': await this.createConfig(args.slice(1)); break; case 'edit': console.log('💡 Edit your configuration file:'); console.log(` ${path.join(this.projectRoot, '.claude.config.js')}`); break; default: console.log(` 🔧 Configuration Management Usage: ccvector watch config <action> Actions: show Show current configuration create [format] Create configuration file (js or json) edit Show configuration file path Examples: ccvector watch config show ccvector watch config create js ccvector watch config create json `); } } /** * 設定表示 */ async showConfig() { try { const configManager = new ConfigManager(); const config = await configManager.initialize(this.projectRoot); console.log('🔧 Current Configuration:\n'); console.log('📁 Project:', this.projectRoot); console.log('📄 Config file:', configManager.configFilePath || 'Using defaults'); console.log('\n📋 Settings:'); // 主要設定の表示 console.log(` • Watch patterns: ${config.watch.patterns.join(', ')}`); console.log(` • Ignore patterns: ${config.watch.ignorePatterns.slice(0, 3).join(', ')}...`); console.log(` • Feedback level: ${config.watch.feedback.level}`); console.log(` • Batch size: ${config.watch.throttling.batchSize}`); console.log(` • Max concurrent: ${config.watch.throttling.maxConcurrent}`); console.log(` • Max files: ${config.watch.files.maxFiles}`); if (configManager.validationWarnings.length > 0) { console.log('\n⚠️ Configuration warnings:'); configManager.validationWarnings.forEach(warning => console.log(` - ${warning}`) ); } } catch (error) { console.error(`❌ Failed to show config: ${error.message}`); } } /** * 設定ファイル作成 */ async createConfig(args) { const format = args[0] || 'js'; const fileName = format === 'json' ? '.claude.config.json' : '.claude.config.js'; const filePath = path.join(this.projectRoot, fileName); if (fsSync.existsSync(filePath)) { console.log(`⚠️ Configuration file already exists: ${fileName}`); console.log('💡 Use --force to overwrite'); return; } try { const configManager = new ConfigManager(); await configManager.createConfigFile(filePath, format); console.log(`✅ Configuration file created: ${fileName}`); console.log('💡 Edit the file to customize your settings'); } catch (error) { console.error(`❌ Failed to create config: ${error.message}`); } } /** * 環境変数読み込み */ loadEnvironmentVariables() { // プロジェクトルートから.envファイルを探す const possibleEnvFiles = [ '.env', '.env.local', '.env.development', '.env.development.local' ]; // 現在のディレクトリから始めて上位ディレクトリを探す let currentDir = this.projectRoot || process.cwd(); for (let i = 0; i < 10; i++) { for (const envFile of possibleEnvFiles) { const envPath = path.join(currentDir, envFile); try { if (fsSync.existsSync(envPath)) { dotenv.config({ path: envPath }); console.log(`📋 Loaded environment variables from: ${path.relative(process.cwd(), envPath)}`); return; } } catch (error) { // エラーは無視して次のファイルを試す } } // 上位ディレクトリに移動 const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; // ルートディレクトリに到達 currentDir = parentDir; } } /** * プロジェクトルート検出 */ async detectProjectRoot() { let currentDir = process.cwd(); // package.json または .git を探す while (currentDir !== path.dirname(currentDir)) { const packageJsonPath = path.join(currentDir, 'package.json'); const gitDir = path.join(currentDir, '.git'); if (fsSync.existsSync(packageJsonPath) || fsSync.existsSync(gitDir)) { return currentDir; } currentDir = path.dirname(currentDir); } // 見つからない場合は現在のディレクトリを使用 return process.cwd(); } /** * 期間フォーマット */ formatDuration(ms) { const seconds = Math.floor(ms / 1000) % 60; const minutes = Math.floor(ms / (1000 * 60)) % 60; const hours = Math.floor(ms / (1000 * 60 * 60)); if (hours > 0) { return `${hours}h ${minutes}m ${seconds}s`; } else if (minutes > 0) { return `${minutes}m ${seconds}s`; } else { return `${seconds}s`; } } /** * Watchヘルプ表示 */ showWatchHelp() { console.log(` 👀 ccvector watch - Real-time File Monitoring Usage: ccvector watch <command> [options] Commands: start Start file monitoring stop Stop monitoring process restart Restart monitoring process status Show monitoring status logs Show monitoring logs config Manage configuration Start Options: --feedback <level> Feedback level: silent, normal, verbose --patterns <glob> Watch patterns (comma-separated) --ignore <glob> Ignore patterns (comma-separated) --batch-size <num> Batch size for processing (default: 5) --max-concurrent <n> Max concurrent operations (default: 3) Stop Options: --force, -f Force stop process Logs Options: --follow, -f Follow logs in real-time --lines <num> Number of lines to show (default: 50) Config Commands: config show Show current configuration config create [fmt] Create config file (js/json) config edit Show config file path Examples: ccvector watch start ccvector watch start --feedback verbose --batch-size 10 ccvector watch stop ccvector watch status ccvector watch logs --follow ccvector watch config show Features: ✅ Zero-configuration operation ✅ Adaptive performance control ✅ OpenAI API rate limiting ✅ Real-time file monitoring ✅ Graceful process management ✅ Structured logging ✅ Configuration management Environment Variables: OPENAI_API_KEY OpenAI API key (required) CLAUDE_FEEDBACK_LEVEL Feedback level (silent/normal/verbose) CLAUDE_BATCH_SIZE Default batch size CLAUDE_MAX_FILES Maximum files to watch `); } }