//! CLI command handlers for PoM subcommands. mod incident; mod serve; mod status; mod tasks; pub(crate) use serve::cmd_serve; pub(crate) use status::cmd_status; use clap::Subcommand; use pom::checks::{dns, http, ssh, whois}; use pom::config::Config; use pom::db; use pom::display; use pom::error::{PomError, Result}; #[derive(Subcommand)] pub(crate) enum HistoryKind { /// Health check history Health { /// Filter by target target: Option, /// Number of results #[arg(short, default_value = "10")] n: i64, /// Output as JSON #[arg(long)] json: bool, }, /// Test run history Tests { /// Filter by target target: Option, /// Number of results #[arg(short, default_value = "10")] n: i64, /// Output as JSON #[arg(long)] json: bool, }, } pub(crate) async fn cmd_health( pool: &sqlx::SqlitePool, config: &Config, target: Option<&str>, json: bool, ) -> Result<()> { let targets: Vec = match target { Some(t) => { if config.get_target(t).is_none() { eprintln!("Unknown target: {t}"); std::process::exit(1); } vec![t.to_string()] } None => config.target_names(), }; let mut snapshots = Vec::new(); for name in &targets { let target_config = config.get_target(name).unwrap(); if let Some(health_config) = &target_config.health { let snapshot = http::check_health(name, health_config, health_config.expect.as_ref()).await; db::insert_health_check(pool, &snapshot).await?; snapshots.push(snapshot); } else { eprintln!("{name}: no health endpoint configured"); } } if json { println!("{}", serde_json::to_string_pretty(&snapshots)?); } else { print!("{}", display::format_health_snapshots(&snapshots)); } Ok(()) } pub(crate) async fn cmd_test( pool: &sqlx::SqlitePool, config: &Config, target_name: &str, filter: Option<&str>, json: bool, ) -> Result<()> { let target = config.get_target(target_name).ok_or_else(|| { PomError::Config(format!("Unknown target: {target_name}")) })?; let tests_config = target.tests.as_ref().ok_or_else(|| { PomError::Config(format!("Target '{target_name}' has no test configuration")) })?; eprintln!("Running tests on {target_name}..."); let run = ssh::run_tests(target_name, tests_config, filter).await; let run_id = db::insert_test_run(pool, &run).await?; // Store per-test details and detect regressions if !run.summary.details.is_empty() { db::insert_test_details(pool, run_id, &run.summary.details).await?; } let regressions = db::get_test_regressions(pool, target_name, run_id).await.unwrap_or_default(); if json { let summary = serde_json::json!({ "target": run.target, "passed": run.passed, "exit_code": run.exit_code, "duration_secs": run.duration_secs, "started_at": run.started_at, "finished_at": run.finished_at, "filter": run.filter, "summary": run.summary, "regressions": regressions, }); println!("{}", serde_json::to_string_pretty(&summary)?); } else { print!("{}", display::format_test_result(target_name, &run)); if !regressions.is_empty() { print!("{}", display::format_regressions(®ressions)); } } Ok(()) } pub(crate) async fn cmd_history( pool: &sqlx::SqlitePool, kind: HistoryKind, ) -> Result<()> { match kind { HistoryKind::Health { target, n, json } => { let history = db::get_health_history(pool, target.as_deref(), n).await?; if json { println!("{}", serde_json::to_string_pretty(&history)?); } else { print!("{}", display::format_health_history(&history)); } } HistoryKind::Tests { target, n, json } => { let history = db::get_test_history(pool, target.as_deref(), n).await?; if json { let summaries: Vec = history .iter() .map(|r| serde_json::json!({ "id": r.id, "target": r.target, "passed": r.passed, "exit_code": r.exit_code, "duration_secs": r.duration_secs, "started_at": r.started_at, "summary": r.summary, })) .collect(); println!("{}", serde_json::to_string_pretty(&summaries)?); } else { print!("{}", display::format_test_history(&history)); } } } Ok(()) } pub(crate) async fn cmd_prune( pool: &sqlx::SqlitePool, days: i64, ) -> Result<()> { let result = db::prune_old_records(pool, days).await?; print!("{}", display::format_prune(&result, days)); Ok(()) } pub(crate) async fn cmd_dns( pool: &sqlx::SqlitePool, config: &Config, target: Option<&str>, json: bool, ) -> Result<()> { let targets: Vec = match target { Some(t) => { if config.get_target(t).is_none() { eprintln!("Unknown target: {t}"); std::process::exit(1); } vec![t.to_string()] } None => config.target_names(), }; let mut all_dns_results = Vec::new(); let mut all_whois_results = Vec::new(); for name in &targets { let target_config = config.get_target(name).unwrap(); // DNS checks if !target_config.dns.is_empty() { let results = dns::check_dns(name, &target_config.dns).await; for result in &results { if let Err(e) = db::insert_dns_check(pool, result).await { tracing::error!("{name}: failed to store DNS check: {e}"); } } all_dns_results.extend(results); } // WHOIS check if let Some(ref whois_config) = target_config.whois { let result = whois::check_whois(name, whois_config).await; if let Err(e) = db::insert_whois_check(pool, &result).await { tracing::error!("{name}: failed to store WHOIS check: {e}"); } all_whois_results.push(result); } } if json { let output = serde_json::json!({ "dns": all_dns_results, "whois": all_whois_results, }); println!("{}", serde_json::to_string_pretty(&output)?); } else if all_dns_results.is_empty() && all_whois_results.is_empty() { println!("No DNS or WHOIS checks configured for the selected target(s)."); } else { print!("{}", display::format_dns_results(&all_dns_results, &all_whois_results)); } Ok(()) } pub(crate) async fn cmd_mesh( config: &Config, json: bool, ) -> Result<()> { let listen = &config.serve.listen; let url = format!("http://{listen}/api/mesh"); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build()?; let response = client.get(&url).send().await.map_err(|e| { PomError::Config(format!("Could not reach local PoM instance at {listen}: {e}")) })?; let data: serde_json::Value = response.json().await?; if json { println!("{}", serde_json::to_string_pretty(&data)?); return Ok(()); } print!("{}", display::format_mesh(&data)); Ok(()) }