diff options
Diffstat (limited to 'makima/src/bin/makima.rs')
| -rw-r--r-- | makima/src/bin/makima.rs | 244 |
1 files changed, 243 insertions, 1 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index ac577b8..d09a1bd 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ - Cli, CliConfig, Commands, ConfigCommand, ContractCommand, RedTeamCommand, SupervisorCommand, ViewArgs, + Cli, CliConfig, Commands, ConfigCommand, ContractCommand, ContractsCommand, RedTeamCommand, SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -31,6 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, Commands::RedTeam(cmd) => run_red_team(cmd).await, + Commands::Contracts(cmd) => run_contracts(cmd).await, } } @@ -805,6 +806,247 @@ async fn run_red_team(cmd: RedTeamCommand) -> Result<(), Box<dyn std::error::Err } } +/// Run contracts commands (multi-contract operations). +async fn run_contracts(cmd: ContractsCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + use makima::daemon::cli::monitor::{ContractState, MonitorEvent, OutputFormat, now_timestamp, ContractSnapshot}; + use std::collections::HashMap; + + match cmd { + ContractsCommand::Monitor(args) => { + let client = ApiClient::new(args.api_url, args.api_key)?; + let poll_interval = std::time::Duration::from_secs(args.poll_interval); + let stale_threshold = args.stale_threshold; + + // Track previous state for change detection + let mut prev_states: HashMap<uuid::Uuid, ContractState> = HashMap::new(); + let mut first_run = true; + + eprintln!("Starting contract monitor (poll interval: {}s, Ctrl+C to stop)...", args.poll_interval); + + loop { + // Fetch contracts + let result = match client.list_contracts().await { + Ok(r) => r, + Err(e) => { + let event = MonitorEvent::Error { + message: format!("Failed to fetch contracts: {}", e), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + tokio::time::sleep(poll_interval).await; + continue; + } + }; + + let contracts = result.0.get("contracts") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default(); + + // Parse contract states + let mut current_states: Vec<ContractState> = Vec::new(); + for contract_json in &contracts { + // If specific contract IDs were provided, filter to those + if !args.contract_ids.is_empty() { + let contract_id = contract_json + .get("id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + if let Some(id) = contract_id { + if !args.contract_ids.contains(&id) { + continue; + } + } else { + continue; + } + } + + // For full state, we need to fetch individual contract details + let contract_id = match contract_json + .get("id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()) + { + Some(id) => id, + None => continue, + }; + + // Get full contract details including tasks + let full_contract = match client.get_contract(contract_id).await { + Ok(r) => r.0, + Err(_) => contract_json.clone(), + }; + + if let Some(state) = ContractState::from_json(&full_contract) { + // Apply filters + if let Some(ref status_filter) = args.status { + if state.status != *status_filter { + continue; + } + } + + if args.stale && !state.is_stale(stale_threshold) { + continue; + } + + current_states.push(state); + } + } + + // On first run, emit a snapshot + if first_run { + let snapshots: Vec<ContractSnapshot> = current_states + .iter() + .map(ContractSnapshot::from) + .collect(); + let event = MonitorEvent::Snapshot { + contracts: snapshots, + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + first_run = false; + + // Store initial states + for state in ¤t_states { + prev_states.insert(state.id, state.clone()); + } + } else { + // Detect changes and emit events + for state in ¤t_states { + if let Some(prev) = prev_states.get(&state.id) { + // Check for status change + if prev.status != state.status { + let event = MonitorEvent::StatusChange { + contract_id: state.id, + contract_name: state.name.clone(), + old_status: prev.status.clone(), + new_status: state.status.clone(), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + + // Check for phase change + if prev.phase != state.phase { + let event = MonitorEvent::PhaseChange { + contract_id: state.id, + contract_name: state.name.clone(), + old_phase: prev.phase.clone(), + new_phase: state.phase.clone(), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + + // Check for new pending question + if !prev.has_pending_question && state.has_pending_question { + if let Some(ref question) = state.pending_question { + let event = MonitorEvent::QuestionPending { + contract_id: state.id, + contract_name: state.name.clone(), + question: question.clone(), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + } + + // Check for task count changes (detect started/completed tasks) + if state.running_tasks > prev.running_tasks && !args.quiet { + // A task started + let event = MonitorEvent::TaskStarted { + contract_id: state.id, + contract_name: state.name.clone(), + task_id: state.id, // We don't have the actual task ID here + task_name: state.current_activity.clone().unwrap_or_else(|| "Unknown".to_string()), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + + if state.completed_tasks > prev.completed_tasks && !args.quiet { + // A task completed + let event = MonitorEvent::TaskCompleted { + contract_id: state.id, + contract_name: state.name.clone(), + task_id: state.id, + task_name: "Task".to_string(), + result: "completed".to_string(), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + + // Check for staleness + if !prev.is_stale(stale_threshold) && state.is_stale(stale_threshold) { + let event = MonitorEvent::ContractStale { + contract_id: state.id, + contract_name: state.name.clone(), + last_activity: state.last_activity.clone().unwrap_or_else(|| "unknown".to_string()), + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + } else { + // New contract appeared - emit as snapshot of just this contract + if !args.quiet { + let event = MonitorEvent::Snapshot { + contracts: vec![ContractSnapshot::from(state)], + timestamp: now_timestamp(), + }; + emit_event(&event, args.format); + } + } + + // Update state + prev_states.insert(state.id, state.clone()); + } + } + + // Wait for next poll + tokio::time::sleep(poll_interval).await; + } + } + } +} + +/// Emit a monitor event in the specified format. +fn emit_event(event: &makima::daemon::cli::monitor::MonitorEvent, format: makima::daemon::cli::monitor::OutputFormat) { + use makima::daemon::cli::monitor::OutputFormat; + match format { + OutputFormat::Text => { + println!("{}", event.to_text()); + } + OutputFormat::Json => { + println!("{}", event.to_json()); + } + OutputFormat::Tui => { + // For now, TUI mode falls back to text with colors + let text = event.to_text(); + // Add color based on event type + let colored = match event { + makima::daemon::cli::monitor::MonitorEvent::QuestionPending { .. } => { + format!("\x1b[33m{}\x1b[0m", text) // Yellow + } + makima::daemon::cli::monitor::MonitorEvent::Error { .. } => { + format!("\x1b[31m{}\x1b[0m", text) // Red + } + makima::daemon::cli::monitor::MonitorEvent::ContractStale { .. } => { + format!("\x1b[90m{}\x1b[0m", text) // Gray + } + makima::daemon::cli::monitor::MonitorEvent::TaskStarted { .. } => { + format!("\x1b[32m{}\x1b[0m", text) // Green + } + makima::daemon::cli::monitor::MonitorEvent::TaskCompleted { .. } => { + format!("\x1b[34m{}\x1b[0m", text) // Blue + } + _ => text, + }; + println!("{}", colored); + } + } +} + /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { let result = client.list_contracts().await?; |
