use std::ffi::OsString;
use std::fmt::Debug;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
use clap::{Parser, Subcommand};
use radicle::crypto::{PublicKey, PublicKeyError};
use radicle::node::{Address, NodeId, PeerAddr, PeerAddrParseError};
use radicle::prelude::RepoId;
const ABOUT: &str = "Control and query the Radicle Node";
#[derive(Parser, Debug)]
#[command(about = ABOUT, long_about, disable_version_flag = true)]
pub struct Args {
#[command(subcommand)]
pub(super) command: Option<Command>,
}
/// Address used for the [`Operation::Connect`]
#[derive(Clone, Debug)]
pub(super) enum Addr {
/// Fully-specified address of the form `<NID>@<ADDR>`
Peer(PeerAddr<NodeId, Address>),
/// Just the `NID`, to be used for address lookups.
Node(NodeId),
}
#[derive(Error, Debug)]
pub(super) enum AddrParseError {
#[error("{0}, expected <NID> or <NID>@<ADDR>")]
PeerAddr(#[from] PeerAddrParseError<PublicKey>),
#[error(transparent)]
NodeId(#[from] PublicKeyError),
}
impl FromStr for Addr {
type Err = AddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.contains("@") {
PeerAddr::from_str(s)
.map(Self::Peer)
.map_err(AddrParseError::PeerAddr)
} else {
NodeId::from_str(s)
.map(Self::Node)
.map_err(AddrParseError::NodeId)
}
}
}
#[derive(Clone, Debug)]
pub enum Only {
Nid,
}
#[derive(Error, Debug)]
#[error("could not parse value `{0}`")]
pub struct OnlyParseError(String);
impl FromStr for Only {
type Err = OnlyParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"nid" => Ok(Only::Nid),
_ => Err(OnlyParseError(value.to_string())),
}
}
}
#[derive(Clone, Debug)]
struct OnlyParser;
impl clap::builder::TypedValueParser for OnlyParser {
type Value = Only;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
<Only as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
use clap::builder::PossibleValue;
Some(Box::new([PossibleValue::new("nid")].into_iter()))
}
}
#[derive(Subcommand, Debug)]
pub(super) enum Command {
/// Instruct the node to connect to another node
Connect {
/// The Node ID, and optionally the address and port, of the node to connect to
#[arg(value_name = "NID[@ADDR]")]
addr: Addr,
/// How long to wait for the connection to be established
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration)]
timeout: Option<std::time::Duration>,
},
/// Show the config
Config {
/// Only show external addresses from the node's config
#[arg(long)]
addresses: bool,
},
/// Interact with the node database
#[command(subcommand, hide = true)]
Db(DbOperation),
/// Watch and print events.
///
/// This command will connect to the node and print events to
/// standard output as they occur.
///
/// If no timeout or count is specified, it will run indefinitely.
Events {
/// How long to wait to receive an event before giving up
///
/// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration)]
timeout: Option<std::time::Duration>,
/// Exit after <COUNT> events
#[arg(long, short = 'n')]
count: Option<usize>,
},
/// Show the routing table
Routing {
/// Output the routing table as json
#[arg(long)]
json: bool,
/// Show the routing table entries for the given RID
#[arg(long = "rid", value_name = "RID")]
repo: Option<RepoId>,
/// Show the routing table entries for the given NID
#[arg(long)]
nid: Option<NodeId>,
},
/// Start the node
Start {
/// Start the node in the foreground
#[arg(long)]
foreground: bool,
/// Verbose output
#[arg(long, short)]
verbose: bool,
/// Start node binary at path
#[arg(long, default_value = "radicle-node")]
path: PathBuf,
/// Additional options to pass to the binary
///
/// See `radicle-node --help` for additional options
#[arg(value_name = "NODE_OPTIONS", last = true, num_args = 1..)]
options: Vec<OsString>,
},
/// Show the log
Logs {
/// Only show <COUNT> lines of the log
#[arg(long, value_name = "COUNT", default_value_t = 60)]
lines: usize,
},
/// Show the status
Status {
/// If node is running, only print the Node ID and exit, otherwise exit with a non-zero exit status.
#[arg(long, value_parser = OnlyParser)]
only: Option<Only>,
},
/// Manage the inventory
Inventory {
/// List the inventory of the given NID, defaults to `self`
#[arg(long)]
nid: Option<NodeId>,
},
/// Show debug information related to the running node.
///
/// This includes metrics fetching, peer connections, rate limiting, etc.
Debug,
/// Show the active sessions of the running node.
///
/// Deprecated, use `status` instead.
#[command(hide = true)]
Sessions,
/// Stop the node
Stop,
}
impl Default for Command {
fn default() -> Self {
Command::Status { only: None }
}
}
/// Operations related to the [`Command::Db`]
#[derive(Debug, Subcommand)]
pub(super) enum DbOperation {
/// Execute an SQL operation on the local node database.
///
/// The command only returns the number of rows that are affected by the
/// query. This means that `SELECT` queries will not return their output.
///
/// The command should only be used for executing queries given you know
/// what you are doing.
Exec {
#[arg(value_name = "SQL")]
query: String,
},
}