cli/auth: use Clap

This commit is contained in:
Richard Levitte 2025-10-03 15:36:03 +02:00 committed by Fintan Halpenny
parent dfd35480be
commit 384c506489
5 changed files with 53 additions and 74 deletions

View File

@ -1,17 +1,23 @@
Note that aliases must not be longer than 32 bytes, or you will get an error. Note that aliases must not be longer than 32 bytes, or you will get an error.
There are other rules as well: There are other rules as well:
``` (fail) ``` (stderr) (fail)
$ rad auth --alias "5fad63fe6b339fa92c588d926121bea6240773a7" $ rad auth --alias "5fad63fe6b339fa92c588d926121bea6240773a7"
✗ Error: rad auth: alias cannot be greater than 32 bytes error: invalid value '5fad63fe6b339fa92c588d926121bea6240773a7' for '--alias <STRING>': alias cannot be greater than 32 bytes
For more information, try '--help'.
``` ```
``` (fail) ``` (stderr) (fail)
$ rad auth --alias "john doe" $ rad auth --alias "john doe"
✗ Error: rad auth: alias cannot contain whitespace or control characters error: invalid value 'john doe' for '--alias <STRING>': alias cannot contain whitespace or control characters
For more information, try '--help'.
``` ```
``` (fail) ``` (stderr) (fail)
$ rad auth --alias "" $ rad auth --alias ""
✗ Error: rad auth: alias cannot be empty error: invalid value '' for '--alias <STRING>': alias cannot be empty
For more information, try '--help'.
``` ```

View File

@ -1,5 +1,6 @@
#![allow(clippy::or_fun_call)] #![allow(clippy::or_fun_call)]
use std::ffi::OsString; mod args;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
@ -11,73 +12,18 @@ use radicle::profile::env;
use radicle::{profile, Profile}; use radicle::{profile, Profile};
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help { pub use args::Args;
name: "auth", pub(crate) use args::ABOUT;
description: "Manage identities and profiles",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad auth [<option>...] pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
A passphrase may be given via the environment variable `RAD_PASSPHRASE` or
via the standard input stream if `--stdin` is used. Using either of these
methods disables the passphrase prompt.
Options
--alias When initializing an identity, sets the node alias
--stdin Read passphrase from stdin (default: false)
--help Print help
"#,
};
#[derive(Debug)]
pub struct Options {
pub stdin: bool,
pub alias: Option<Alias>,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut stdin = false;
let mut alias = None;
let mut parser = lexopt::Parser::from_args(args);
while let Some(arg) = parser.next()? {
match arg {
Long("alias") => {
let val = parser.value()?;
let val = term::args::alias(&val)?;
alias = Some(val);
}
Long("stdin") => {
stdin = true;
}
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => anyhow::bail!(arg.unexpected()),
}
}
Ok((Options { alias, stdin }, vec![]))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
match ctx.profile() { match ctx.profile() {
Ok(profile) => authenticate(options, &profile), Ok(profile) => authenticate(args, &profile),
Err(_) => init(options), Err(_) => init(args),
} }
} }
pub fn init(options: Options) -> anyhow::Result<()> { pub fn init(args: Args) -> anyhow::Result<()> {
term::headline("Initializing your radicle 👾 identity"); term::headline("Initializing your radicle 👾 identity");
if let Ok(version) = radicle::git::version() { if let Ok(version) = radicle::git::version() {
@ -92,7 +38,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
anyhow::bail!("A Git installation is required for Radicle to run."); anyhow::bail!("A Git installation is required for Radicle to run.");
} }
let alias: Alias = if let Some(alias) = options.alias { let alias: Alias = if let Some(alias) = args.alias {
alias alias
} else { } else {
let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok()); let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok());
@ -105,7 +51,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
user.ok_or_else(|| anyhow::anyhow!("An alias is required for Radicle to run."))? user.ok_or_else(|| anyhow::anyhow!("An alias is required for Radicle to run."))?
}; };
let home = profile::home()?; let home = profile::home()?;
let passphrase = if options.stdin { let passphrase = if args.stdin {
Some(term::passphrase_stdin()?) Some(term::passphrase_stdin()?)
} else { } else {
term::passphrase_confirm("Enter a passphrase:", env::RAD_PASSPHRASE)? term::passphrase_confirm("Enter a passphrase:", env::RAD_PASSPHRASE)?
@ -165,7 +111,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
/// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for /// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for
/// use. /// use.
pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> { pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
if !profile.keystore.is_encrypted()? { if !profile.keystore.is_encrypted()? {
term::success!("Authenticated as {}", term::format::tertiary(profile.id())); term::success!("Authenticated as {}", term::format::tertiary(profile.id()));
return Ok(()); return Ok(());
@ -186,7 +132,7 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
} }
let passphrase = if let Some(phrase) = profile::env::passphrase() { let passphrase = if let Some(phrase) = profile::env::passphrase() {
phrase phrase
} else if options.stdin { } else if args.stdin {
term::passphrase_stdin()? term::passphrase_stdin()?
} else if let Some(passphrase) = } else if let Some(passphrase) =
term::io::passphrase(term::io::PassphraseValidator::new(profile.keystore.clone()))? term::io::passphrase(term::io::PassphraseValidator::new(profile.keystore.clone()))?

View File

@ -0,0 +1,21 @@
use clap::Parser;
use radicle::node::Alias;
pub(crate) const ABOUT: &str = "Manage identities and profiles";
const LONG_ABOUT: &str = r#"
A passphrase may be given via the environment variable `RAD_PASSPHRASE` or
via the standard input stream if `--stdin` is used. Using either of these
methods disables the passphrase prompt.
"#;
#[derive(Debug, Parser)]
#[command(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
pub struct Args {
/// When initializing an identity, sets the node alias
#[arg(long, value_name = "STRING")]
pub alias: Option<Alias>,
/// Read passphrase from stdin
#[arg(long, default_value_t = false)]
pub stdin: bool,
}

View File

@ -37,7 +37,10 @@ impl CommandItem {
} }
const COMMANDS: &[CommandItem] = &[ const COMMANDS: &[CommandItem] = &[
CommandItem::Lexopt(crate::commands::auth::HELP), CommandItem::Clap {
name: "auth",
about: crate::commands::auth::ABOUT,
},
CommandItem::Clap { CommandItem::Clap {
name: "block", name: "block",
about: crate::commands::block::ABOUT, about: crate::commands::block::ABOUT,

View File

@ -45,6 +45,7 @@ struct CliArgs {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
Auth(auth::Args),
Block(block::Args), Block(block::Args),
Clean(clean::Args), Clean(clean::Args),
Clone(clone::Args), Clone(clone::Args),
@ -183,7 +184,9 @@ fn run(command: Command) -> Result<(), Option<anyhow::Error>> {
pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>> { pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>> {
match exe { match exe {
"auth" => { "auth" => {
term::run_command_args::<auth::Options, _>(auth::HELP, auth::run, args.to_vec()); if let Some(Commands::Auth(args)) = CliArgs::parse().command {
term::run_command_fn(auth::run, args);
}
} }
"block" => { "block" => {
if let Some(Commands::Block(args)) = CliArgs::parse().command { if let Some(Commands::Block(args)) = CliArgs::parse().command {