@claude-vector/cli
Version:
CLI for Claude-integrated vector search
516 lines (439 loc) • 16.1 kB
JavaScript
/**
* 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
`);
}
}