//! PoM CLI entry point, parses subcommands and dispatches to handlers or MCP server. use clap::{Parser, Subcommand}; use rmcp::ServiceExt; use tokio::io::{stdin, stdout}; use tracing::{info, warn}; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use pom::config::{self, Config}; use pom::db; use pom::error::Result; use pom::tools::PomServer; mod cli; #[derive(Parser)] #[command( name = "pom", version, about = "Peace of Mind: health checks and test orchestration" )] struct Cli { /// Path to config file (default: ~/.config/pom/pom.toml) #[arg(long, global = true)] config: Option, /// Create the database if it does not exist yet. Without this, opening a /// missing database is an error: auto-creating is what let a wrong path /// become a silent second store rather than a complaint. #[arg(long, global = true)] init: bool, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { /// Check health of targets Health { /// Target name (omit for all) target: Option, /// Output as JSON #[arg(long)] json: bool, }, /// Run tests on a target via SSH Test { /// Target name target: String, /// Filter tests #[arg(long, short)] filter: Option, /// Output as JSON #[arg(long)] json: bool, }, /// Show status dashboard Status { /// Output as JSON #[arg(long)] json: bool, }, /// Show what version each target is running, and how far behind Versions { /// Output as JSON #[arg(long)] json: bool, }, /// View history History { #[command(subcommand)] kind: cli::HistoryKind, }, /// Prune old records Prune { /// Number of days to keep (default 30) #[arg(long, default_value = "30")] days: i64, }, /// Run DNS and WHOIS checks Dns { /// Target name (omit for all) target: Option, /// Output as JSON #[arg(long)] json: bool, }, /// Run as a daemon, checking health at intervals Serve, /// Show peer mesh status Mesh { /// Output as JSON #[arg(long)] json: bool, }, } #[tokio::main] async fn main() -> Result<()> { // Install the pure-Rust ring crypto provider before any TLS operation. It // verifies signatures for every certificate pom validates: reqwest (built // rustls-no-provider) and the TLS-expiry check both read the process default // via CryptoProvider::get_default. aws-lc-rs is kept out of the dependency // tree entirely (see Cargo.toml), so its C/asm build dep and the aws-lc-sys // advisory surface never reach a build host or the cert-verification path. pom::tls::install_crypto_provider(); let cli = Cli::parse(); let config_path = cli.config.as_deref(); let config = Config::load(config_path)?; let on_missing = if cli.init { db::OnMissingDb::Create } else { db::OnMissingDb::Fail }; match cli.command { None => run_mcp_server(config, on_missing).await, Some(cmd) => run_cli(cmd, config, on_missing).await, } } async fn run_mcp_server(config: Config, on_missing: db::OnMissingDb) -> Result<()> { tracing_subscriber::registry() .with(fmt::layer().with_writer(std::io::stderr)) .with(EnvFilter::from_default_env().add_directive("pom=info".parse()?)) .init(); info!("Starting PoM MCP server"); let db = config.db_path()?; let pool = db::connect(&db.path, on_missing).await?; info!("Database ready at {db}"); let server = PomServer::new(pool, config); let transport = (stdin(), stdout()); info!("MCP server ready"); let service = server.serve(transport).await?; let quit_reason = service.waiting().await?; info!(?quit_reason, "MCP server shutting down"); Ok(()) } async fn run_cli(cmd: Commands, config: Config, on_missing: db::OnMissingDb) -> Result<()> { let log_level = if matches!(cmd, Commands::Serve) { "pom=info" } else { "pom=warn" }; tracing_subscriber::registry() .with(fmt::layer().with_writer(std::io::stderr)) .with(EnvFilter::from_default_env().add_directive(log_level.parse()?)) .init(); let db = config.db_path()?; info!("Database at {db}"); // An unconfigured path is read out of XDG_DATA_HOME, which the service unit // sets and an interactive shell does not — the two then open different // files on the same host and neither says so. Warn rather than info, so it // clears the CLI's `pom=warn` filter and is visible without RUST_LOG. if db.source == config::DbPathSource::XdgDataHome { warn!( "database path came from the environment, not the config. Set \ storage.db_path in pom.toml to pin it: {}", db.path.display() ); } let pool = db::connect(&db.path, on_missing).await?; match cmd { Commands::Health { target, json } => { cli::cmd_health(&pool, &config, target.as_deref(), json).await } Commands::Test { target, filter, json, } => cli::cmd_test(&pool, &config, &target, filter.as_deref(), json).await, Commands::Status { json } => cli::cmd_status(&pool, &config, json).await, Commands::Versions { json } => cli::cmd_versions(&pool, &config, json).await, Commands::History { kind } => cli::cmd_history(&pool, kind).await, Commands::Prune { days } => cli::cmd_prune(&pool, days).await, Commands::Dns { target, json } => { cli::cmd_dns(&pool, &config, target.as_deref(), json).await } Commands::Serve => cli::cmd_serve(&pool, &config).await, Commands::Mesh { json } => cli::cmd_mesh(&config, json).await, } }