//! CLI command handlers for PoM subcommands. mod incident; mod serve; mod status; mod tasks; mod transition; 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, }, } /// Resolve an optional `--target` filter to the target names to act on. /// `None` selects every configured target; an unknown name is an error rather /// than a bare `process::exit`, so every command reports failure the same way. fn resolve_targets(config: &Config, target: Option<&str>) -> Result> { match target { Some(t) if config.get_target(t).is_none() => { Err(PomError::Config(format!("Unknown target: {t}"))) } Some(t) => Ok(vec![t.to_string()]), None => Ok(config.target_names()), } } pub(crate) async fn cmd_health( pool: &sqlx::SqlitePool, config: &Config, target: Option<&str>, json: bool, ) -> Result<()> { let targets = resolve_targets(config, target)?; 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 = resolve_targets(config, target)?; 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_versions( pool: &sqlx::SqlitePool, config: &Config, json: bool, ) -> Result<()> { let rows = pom::versions::collect(pool, config).await?; if json { println!("{}", serde_json::to_string_pretty(&rows)?); } else { print!("{}", display::format_versions(&rows)); } 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 = pom::tls::https_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(()) }