import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import chokidar from 'chokidar'; import fs from 'fs/promises'; import path from 'path'; import { execSync } from 'child_process'; import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js'; import axios from 'axios'; interface VerificationCriteria { type: 'file_exists' | 'file_content' | 'content_match'; path: string; pattern?: string; match?: string; } export function createWatchCommand() { const watch = new Command('watch'); watch .description('Watch for changes and auto-verify roadmap tasks') .option('--no-auto-commit', 'Disable auto-commit on verification') .option('--no-auto-push', 'Disable auto-push after commit') .option('--run-tests', 'Run tests before committing') .option('--test-command ', 'Custom test command (default: npm test)') .action(async (options) => { console.log(chalk.bold.blue('🔭 Rigstate Watch Mode')); console.log(chalk.dim('Monitoring for task completion...')); console.log(''); // Get config let apiKey: string; let projectId: string | undefined; try { apiKey = getApiKey(); } catch (e) { console.log(chalk.red('Not authenticated. Run "rigstate login" first.')); return; } projectId = getProjectId(); if (!projectId) { const { loadManifest } = await import('../utils/manifest.js'); const manifest = await loadManifest(); if (manifest?.project_id) projectId = manifest.project_id; } if (!projectId) { console.log(chalk.red('No project context. Run "rigstate link" or "rigstate sync --project " first.')); return; } const apiUrl = getApiUrl(); // Settings const config = { autoCommit: options.autoCommit !== false, autoPush: options.autoPush !== false, runTests: options.runTests || false, testCommand: options.testCommand || 'npm test' }; console.log(chalk.dim(`Auto-commit: ${config.autoCommit ? 'ON' : 'OFF'}`)); console.log(chalk.dim(`Auto-push: ${config.autoPush ? 'ON' : 'OFF'}`)); console.log(''); // Fetch active task const fetchActiveTask = async () => { try { const response = await axios.get(`${apiUrl}/api/v1/roadmap`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` } }); if (!response.data.success) return null; const roadmap = response.data.data.roadmap || []; // Priority: IN_PROGRESS > ACTIVE > LOCKED const statusPriority: Record = { 'IN_PROGRESS': 0, 'ACTIVE': 1, 'LOCKED': 2 }; const activeTasks = roadmap .filter((t: any) => ['IN_PROGRESS', 'ACTIVE', 'LOCKED'].includes(t.status)) .sort((a: any, b: any) => { const pA = statusPriority[a.status] ?? 99; const pB = statusPriority[b.status] ?? 99; if (pA !== pB) return pA - pB; return (a.step_number || 0) - (b.step_number || 0); }); return activeTasks[0] || null; } catch (e) { return null; } }; // Check verification criteria const checkCriteria = async (criteria: VerificationCriteria): Promise => { try { const fullPath = path.resolve(process.cwd(), criteria.path); switch (criteria.type) { case 'file_exists': await fs.access(fullPath); return true; case 'file_content': const content = await fs.readFile(fullPath, 'utf-8'); return content.length > 0; case 'content_match': if (!criteria.match) return false; const fileContent = await fs.readFile(fullPath, 'utf-8'); return fileContent.includes(criteria.match); default: return false; } } catch (e) { return false; } }; // Complete task const completeTask = async (taskId: string, task: any) => { const spinner = ora('Completing task...').start(); try { // Run tests if enabled if (config.runTests) { spinner.text = 'Running tests...'; try { execSync(config.testCommand, { stdio: 'pipe' }); spinner.text = 'Tests passed!'; } catch (e) { spinner.fail('Tests failed. Task not completed.'); return; } } // Update status via API await axios.post(`${apiUrl}/api/v1/roadmap/update-status`, { project_id: projectId, chunk_id: taskId, status: 'COMPLETED' }, { headers: { Authorization: `Bearer ${apiKey}` } }); spinner.succeed(chalk.green(`✅ Task #${task.step_number} completed: ${task.title}`)); // Auto-commit if (config.autoCommit) { spinner.start('Committing changes...'); try { execSync('git add -A', { stdio: 'pipe' }); const commitMsg = `feat: Complete task #${task.step_number} - ${task.title}`; execSync(`git commit -m "${commitMsg}"`, { stdio: 'pipe' }); spinner.succeed('Changes committed'); // Auto-push if (config.autoPush) { spinner.start('Pushing to remote...'); try { execSync('git push', { stdio: 'pipe' }); spinner.succeed('Pushed to remote'); } catch (e) { spinner.warn('Push failed (no remote or conflict)'); } } } catch (e: any) { spinner.warn('Nothing to commit or commit failed'); } } console.log(''); console.log(chalk.blue('Watching for next task...')); } catch (e: any) { spinner.fail(`Failed to complete task: ${e.message}`); } }; // Main watch loop let currentTask: any = null; let isProcessing = false; const processActiveTask = async () => { if (isProcessing) return; isProcessing = true; const task = await fetchActiveTask(); if (!task) { if (currentTask) { console.log(chalk.green('🎉 All tasks completed! Watching for new tasks...')); currentTask = null; } isProcessing = false; return; } if (!currentTask || currentTask.id !== task.id) { currentTask = task; console.log(''); console.log(chalk.bold.yellow(`📌 Active Task #${task.step_number}: ${task.title}`)); console.log(chalk.dim(`Status: ${task.status}`)); if (task.verification_criteria) { console.log(chalk.dim('Verification: Auto-checking criteria...')); } } // Check verification criteria if present if (task.verification_criteria && Array.isArray(task.verification_criteria)) { let allPassed = true; for (const criteria of task.verification_criteria) { const passed = await checkCriteria(criteria); if (!passed) { allPassed = false; break; } } if (allPassed) { console.log(chalk.green('✓ All verification criteria passed!')); await completeTask(task.id, task); currentTask = null; } } isProcessing = false; }; // Initial check await processActiveTask(); // Set up file watcher const watcher = chokidar.watch('.', { ignored: [ /(^|[\/\\])\../, // dotfiles '**/node_modules/**', '**/.git/**', '**/.next/**', '**/dist/**' ], persistent: true, ignoreInitial: true }); watcher.on('all', async (event, filePath) => { if (['add', 'change', 'unlink'].includes(event)) { // Debounce - wait a bit for multiple rapid changes setTimeout(() => processActiveTask(), 500); } }); console.log(chalk.dim('Watching for file changes... (Ctrl+C to exit)')); // Periodic check every 30 seconds setInterval(() => processActiveTask(), 30000); // Keep process alive process.on('SIGINT', () => { console.log(''); console.log(chalk.dim('Watch mode stopped.')); watcher.close(); process.exit(0); }); }); return watch; }