Skip to main content

max / makenotwork

5.9 KB · 195 lines History Blame Raw
1 //! PoM CLI entry point, parses subcommands and dispatches to handlers or MCP server.
2
3 use clap::{Parser, Subcommand};
4 use rmcp::ServiceExt;
5 use tokio::io::{stdin, stdout};
6 use tracing::{info, warn};
7 use tracing_subscriber::{EnvFilter, fmt, prelude::*};
8
9 use pom::config::{self, Config};
10 use pom::db;
11 use pom::error::Result;
12 use pom::tools::PomServer;
13
14 mod cli;
15
16 #[derive(Parser)]
17 #[command(
18 name = "pom",
19 version,
20 about = "Peace of Mind: health checks and test orchestration"
21 )]
22 struct Cli {
23 /// Path to config file (default: ~/.config/pom/pom.toml)
24 #[arg(long, global = true)]
25 config: Option<std::path::PathBuf>,
26
27 /// Create the database if it does not exist yet. Without this, opening a
28 /// missing database is an error: auto-creating is what let a wrong path
29 /// become a silent second store rather than a complaint.
30 #[arg(long, global = true)]
31 init: bool,
32
33 #[command(subcommand)]
34 command: Option<Commands>,
35 }
36
37 #[derive(Subcommand)]
38 enum Commands {
39 /// Check health of targets
40 Health {
41 /// Target name (omit for all)
42 target: Option<String>,
43 /// Output as JSON
44 #[arg(long)]
45 json: bool,
46 },
47 /// Run tests on a target via SSH
48 Test {
49 /// Target name
50 target: String,
51 /// Filter tests
52 #[arg(long, short)]
53 filter: Option<String>,
54 /// Output as JSON
55 #[arg(long)]
56 json: bool,
57 },
58 /// Show status dashboard
59 Status {
60 /// Output as JSON
61 #[arg(long)]
62 json: bool,
63 },
64 /// Show what version each target is running, and how far behind
65 Versions {
66 /// Output as JSON
67 #[arg(long)]
68 json: bool,
69 },
70 /// View history
71 History {
72 #[command(subcommand)]
73 kind: cli::HistoryKind,
74 },
75 /// Prune old records
76 Prune {
77 /// Number of days to keep (default 30)
78 #[arg(long, default_value = "30")]
79 days: i64,
80 },
81 /// Run DNS and WHOIS checks
82 Dns {
83 /// Target name (omit for all)
84 target: Option<String>,
85 /// Output as JSON
86 #[arg(long)]
87 json: bool,
88 },
89 /// Run as a daemon, checking health at intervals
90 Serve,
91 /// Show peer mesh status
92 Mesh {
93 /// Output as JSON
94 #[arg(long)]
95 json: bool,
96 },
97 }
98
99 #[tokio::main]
100 async fn main() -> Result<()> {
101 // Install the pure-Rust ring crypto provider before any TLS operation. It
102 // verifies signatures for every certificate pom validates: reqwest (built
103 // rustls-no-provider) and the TLS-expiry check both read the process default
104 // via CryptoProvider::get_default. aws-lc-rs is kept out of the dependency
105 // tree entirely (see Cargo.toml), so its C/asm build dep and the aws-lc-sys
106 // advisory surface never reach a build host or the cert-verification path.
107 pom::tls::install_crypto_provider();
108
109 let cli = Cli::parse();
110
111 let config_path = cli.config.as_deref();
112 let config = Config::load(config_path)?;
113
114 let on_missing = if cli.init {
115 db::OnMissingDb::Create
116 } else {
117 db::OnMissingDb::Fail
118 };
119
120 match cli.command {
121 None => run_mcp_server(config, on_missing).await,
122 Some(cmd) => run_cli(cmd, config, on_missing).await,
123 }
124 }
125
126 async fn run_mcp_server(config: Config, on_missing: db::OnMissingDb) -> Result<()> {
127 tracing_subscriber::registry()
128 .with(fmt::layer().with_writer(std::io::stderr))
129 .with(EnvFilter::from_default_env().add_directive("pom=info".parse()?))
130 .init();
131
132 info!("Starting PoM MCP server");
133
134 let db = config.db_path()?;
135 let pool = db::connect(&db.path, on_missing).await?;
136 info!("Database ready at {db}");
137
138 let server = PomServer::new(pool, config);
139 let transport = (stdin(), stdout());
140
141 info!("MCP server ready");
142 let service = server.serve(transport).await?;
143 let quit_reason = service.waiting().await?;
144 info!(?quit_reason, "MCP server shutting down");
145
146 Ok(())
147 }
148
149 async fn run_cli(cmd: Commands, config: Config, on_missing: db::OnMissingDb) -> Result<()> {
150 let log_level = if matches!(cmd, Commands::Serve) {
151 "pom=info"
152 } else {
153 "pom=warn"
154 };
155 tracing_subscriber::registry()
156 .with(fmt::layer().with_writer(std::io::stderr))
157 .with(EnvFilter::from_default_env().add_directive(log_level.parse()?))
158 .init();
159
160 let db = config.db_path()?;
161 info!("Database at {db}");
162 // An unconfigured path is read out of XDG_DATA_HOME, which the service unit
163 // sets and an interactive shell does not — the two then open different
164 // files on the same host and neither says so. Warn rather than info, so it
165 // clears the CLI's `pom=warn` filter and is visible without RUST_LOG.
166 if db.source == config::DbPathSource::XdgDataHome {
167 warn!(
168 "database path came from the environment, not the config. Set \
169 storage.db_path in pom.toml to pin it: {}",
170 db.path.display()
171 );
172 }
173 let pool = db::connect(&db.path, on_missing).await?;
174
175 match cmd {
176 Commands::Health { target, json } => {
177 cli::cmd_health(&pool, &config, target.as_deref(), json).await
178 }
179 Commands::Test {
180 target,
181 filter,
182 json,
183 } => cli::cmd_test(&pool, &config, &target, filter.as_deref(), json).await,
184 Commands::Status { json } => cli::cmd_status(&pool, &config, json).await,
185 Commands::Versions { json } => cli::cmd_versions(&pool, &config, json).await,
186 Commands::History { kind } => cli::cmd_history(&pool, kind).await,
187 Commands::Prune { days } => cli::cmd_prune(&pool, days).await,
188 Commands::Dns { target, json } => {
189 cli::cmd_dns(&pool, &config, target.as_deref(), json).await
190 }
191 Commands::Serve => cli::cmd_serve(&pool, &config).await,
192 Commands::Mesh { json } => cli::cmd_mesh(&config, json).await,
193 }
194 }
195