mod commands;
mod config;
mod context;
mod error;
mod output;
mod parsers;
mod prelude;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use crate::error::CliError;
use crate::output::OutputFormat;
#[derive(Parser, Debug)]
#[command(
name = "net-mesh",
bin_name = "net-mesh",
version,
about = "Unified command-line interface for the Net mesh.",
long_about = "net-mesh is the operational counterpart to net-deck — \
a non-interactive command-line tool that wraps \
the Rust SDK for one-shot operator commands, CI \
scripting, daemon authoring, and ad-hoc cluster \
inspection. See NET_CLI_PLAN.md for the full \
surface."
)]
struct Cli {
#[arg(long, global = true, env = "NET_MESH_CONFIG")]
config: Option<PathBuf>,
#[arg(
long,
global = true,
env = "NET_MESH_PROFILE",
default_value = "default"
)]
profile: String,
#[arg(long, global = true, value_enum)]
output: Option<OutputFormat>,
#[arg(long, short = 'q', global = true)]
quiet: bool,
#[arg(long, short = 'v', global = true, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(long, global = true, env = "NO_COLOR")]
no_color: bool,
#[arg(long, global = true, value_parser = humantime::parse_duration, default_value = "30s")]
timeout: std::time::Duration,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Version,
#[command(subcommand)]
Identity(commands::identity::IdentityCommand),
#[command(subcommand)]
Admin(commands::admin::AdminCommand),
#[command(subcommand)]
Ice(commands::ice::IceCommand),
#[command(subcommand)]
Snapshot(commands::snapshot::SnapshotCommand),
#[command(subcommand)]
Audit(commands::audit::AuditCommand),
#[command(subcommand)]
Log(LogCommand),
#[command(subcommand)]
Failures(FailuresCommand),
#[command(subcommand)]
Cap(commands::cap::CapCommand),
#[command(subcommand)]
Peer(PeerCommand),
#[command(subcommand)]
Daemon(DaemonCommand),
#[command(subcommand)]
Netdb(commands::netdb::NetdbCommand),
#[command(subcommand)]
Subnet(commands::subnet::SubnetCommand),
#[command(subcommand)]
Gateway(commands::gateway::GatewayCommand),
#[command(subcommand)]
Channel(commands::channel::ChannelCommand),
#[command(subcommand)]
Aggregator(commands::aggregator::AggregatorCommand),
Completion(commands::completion::CompletionArgs),
Man,
}
#[derive(Subcommand, Debug)]
enum LogCommand {
Tail(commands::logs::LogTailArgs),
}
#[derive(Subcommand, Debug)]
enum FailuresCommand {
Tail(commands::logs::FailuresTailArgs),
}
#[derive(Subcommand, Debug)]
enum PeerCommand {
Ls(commands::peer::LsArgs),
}
#[derive(Subcommand, Debug)]
enum DaemonCommand {
Ls(commands::daemon::LsArgs),
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
let cli = Cli::parse();
install_tracing(cli.verbose, cli.quiet);
match dispatch(cli).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("net-mesh: {}", e);
ExitCode::from(e.code())
}
}
}
async fn dispatch(cli: Cli) -> Result<(), CliError> {
let output = cli.output;
let config_path = cli.config.as_deref();
let profile = cli.profile.as_str();
match cli.command {
Command::Version => commands::version::run(output).await,
Command::Identity(cmd) => commands::identity::run(cmd, output).await,
Command::Admin(cmd) => commands::admin::run(cmd, output, config_path, profile).await,
Command::Ice(cmd) => commands::ice::run(cmd, output, config_path, profile).await,
Command::Snapshot(cmd) => commands::snapshot::run(cmd, output, config_path, profile).await,
Command::Audit(cmd) => commands::audit::run(cmd, output, config_path, profile).await,
Command::Log(LogCommand::Tail(args)) => {
commands::logs::run_log_tail(args, output, config_path, profile).await
}
Command::Failures(FailuresCommand::Tail(args)) => {
commands::logs::run_failures_tail(args, output, config_path, profile).await
}
Command::Cap(cmd) => commands::cap::run(cmd, output, config_path, profile).await,
Command::Peer(PeerCommand::Ls(args)) => {
commands::peer::run_ls(args, output, config_path, profile).await
}
Command::Daemon(DaemonCommand::Ls(args)) => {
commands::daemon::run_ls(args, output, config_path, profile).await
}
Command::Netdb(cmd) => commands::netdb::run(cmd, output, config_path, profile).await,
Command::Subnet(cmd) => commands::subnet::run(cmd, output, config_path, profile).await,
Command::Gateway(cmd) => commands::gateway::run(cmd, output, config_path, profile).await,
Command::Channel(cmd) => commands::channel::run(cmd, output, config_path, profile).await,
Command::Aggregator(cmd) => {
commands::aggregator::run(cmd, output, config_path, profile).await
}
Command::Completion(args) => commands::completion::run::<Cli>(args),
Command::Man => commands::man::run::<Cli>(),
}
}
fn install_tracing(verbose: u8, quiet: bool) {
use tracing_subscriber::{fmt, EnvFilter};
let level = if quiet && verbose == 0 {
"error"
} else {
match verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
}
};
let filter = EnvFilter::try_from_env("NET_MESH_LOG")
.unwrap_or_else(|_| EnvFilter::new(format!("net={level},net_sdk={level}")));
let _ = fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.compact()
.try_init();
}
mod humantime {
use std::time::Duration;
pub(crate) fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty duration".into());
}
if s.chars().all(|c| c.is_ascii_digit()) {
let value: u64 = s.parse().map_err(|_| format!("invalid integer {s:?}"))?;
return Ok(Duration::from_secs(value));
}
let mut total = Duration::ZERO;
let mut digits = String::new();
let mut units = String::new();
let mut between_components = false;
for c in s.chars() {
if c.is_ascii_digit() {
if !units.is_empty() {
total = total
.checked_add(apply_unit(&digits, &units)?)
.ok_or_else(|| format!("duration overflow in {s:?}"))?;
digits.clear();
units.clear();
}
digits.push(c);
between_components = false;
} else if c.is_alphabetic() {
if digits.is_empty() {
return Err(format!("unit {c:?} with no preceding number"));
}
units.push(c);
between_components = false;
} else if c.is_whitespace() {
if !units.is_empty() {
total = total
.checked_add(apply_unit(&digits, &units)?)
.ok_or_else(|| format!("duration overflow in {s:?}"))?;
digits.clear();
units.clear();
between_components = true;
} else if !between_components {
return Err(format!("unexpected whitespace inside duration {s:?}"));
}
} else {
return Err(format!("invalid character {c:?} in duration"));
}
}
if units.is_empty() {
return Err(format!("missing unit on trailing number in duration {s:?}"));
}
total
.checked_add(apply_unit(&digits, &units)?)
.ok_or_else(|| format!("duration overflow in {s:?}"))
}
fn apply_unit(digits: &str, unit: &str) -> Result<Duration, String> {
let value: u64 = digits
.parse()
.map_err(|_| format!("invalid numeric value {digits:?}"))?;
let secs = match unit {
"ns" => return Ok(Duration::from_nanos(value)),
"us" | "µs" => return Ok(Duration::from_micros(value)),
"ms" => return Ok(Duration::from_millis(value)),
"s" | "sec" | "secs" => value,
"m" | "min" | "mins" => value
.checked_mul(60)
.ok_or_else(|| format!("duration overflow at {value}{unit}"))?,
"h" | "hr" | "hrs" => value
.checked_mul(3600)
.ok_or_else(|| format!("duration overflow at {value}{unit}"))?,
"d" | "day" | "days" => value
.checked_mul(86_400)
.ok_or_else(|| format!("duration overflow at {value}{unit}"))?,
other => return Err(format!("unknown duration unit {other:?}")),
};
Ok(Duration::from_secs(secs))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_integer_is_seconds() {
assert_eq!(parse_duration("30").unwrap(), Duration::from_secs(30));
}
#[test]
fn unit_suffixes() {
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
}
#[test]
fn composite_units() {
assert_eq!(
parse_duration("1h30m").unwrap(),
Duration::from_secs(3600 + 30 * 60)
);
assert_eq!(
parse_duration("1h 30m").unwrap(),
Duration::from_secs(3600 + 30 * 60)
);
}
#[test]
fn rejects_empty_and_garbage() {
assert!(parse_duration("").is_err());
assert!(parse_duration("abc").is_err());
assert!(parse_duration("10x").is_err());
}
#[test]
fn rejects_trailing_unitless_number() {
assert!(parse_duration("1m5").is_err());
}
#[test]
fn rejects_whitespace_inside_a_number() {
assert!(parse_duration("10 5s").is_err());
}
#[test]
fn rejects_overflow_on_unit_multiplication() {
assert!(parse_duration("999999999999999d").is_err());
}
}
}