summaryrefslogtreecommitdiff
path: root/makima/src/bin/makima.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/bin/makima.rs')
-rw-r--r--makima/src/bin/makima.rs244
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 &current_states {
+ prev_states.insert(state.id, state.clone());
+ }
+ } else {
+ // Detect changes and emit events
+ for state in &current_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?;