use std::{io::Cursor, sync::LazyLock};
use clap::{ArgAction, Parser, Subcommand, ValueEnum};
use getset::{CopyGetters, Getters};
use vergen_pretty::{Pretty, vergen_pretty_env};
static LONG_VERSION: LazyLock<String> = LazyLock::new(|| {
let pretty = Pretty::builder().env(vergen_pretty_env!()).build();
let mut cursor = Cursor::new(vec![]);
let mut output = env!("CARGO_PKG_VERSION").to_string();
output.push_str("\n\n");
pretty
.display(&mut cursor)
.expect("writing to Vec never fails");
output += &String::from_utf8_lossy(cursor.get_ref());
output
});
#[derive(Clone, CopyGetters, Debug, Getters, Parser)]
#[command(author, version, about, long_version = LONG_VERSION.as_str(), long_about = None)]
pub(crate) struct Cli {
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn up logging verbosity",
conflicts_with = "quiet"
)]
#[getset(get_copy = "pub(crate)")]
verbose: u8,
#[clap(
short,
long,
action = ArgAction::Count,
help = "Turn down logging verbosity",
conflicts_with = "verbose"
)]
#[getset(get_copy = "pub(crate)")]
quiet: u8,
#[command(subcommand)]
#[getset(get = "pub(crate)")]
command: Commands,
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub(crate) enum ShellKind {
#[default]
Fish,
Bash,
}
#[derive(Clone, Debug, Subcommand)]
pub(crate) enum Commands {
#[clap(about = "Start the agent daemon")]
Start {
#[clap(short, long, value_name = "PATH")]
socket: Option<String>,
#[clap(long, value_name = "PATH")]
vault: Option<String>,
#[clap(long, default_value_t = false)]
foreground: bool,
#[clap(long, value_enum, default_value_t = ShellKind::Fish)]
shell: ShellKind,
#[clap(long, default_value_t = false, hide = true)]
passphrase_pipe: bool,
#[clap(long, value_name = "BACKEND", default_value_t = default_backend())]
backend: String,
#[clap(long, default_value_t = false)]
passphrase_stdin: bool,
},
#[clap(about = "Add an identity key to the agent")]
AddKey {
#[clap(value_name = "KEY_PATH")]
key_path: String,
#[clap(long, default_value_t = false)]
passphrase_stdin: bool,
#[clap(long, default_value_t = false)]
no_hint: bool,
},
#[clap(about = "List identities held by the agent")]
List {
#[clap(long, default_value_t = false)]
no_hint: bool,
},
#[clap(about = "Remove an identity from the agent")]
RemoveKey {
#[clap(value_name = "FINGERPRINT")]
fingerprint: String,
},
#[clap(about = "Show the agent's current status")]
Status,
#[clap(about = "Lock the agent (clear keys from memory)")]
Lock,
#[clap(about = "Unlock the agent (reload keys from vault)")]
Unlock,
#[clap(about = "Stop the agent daemon")]
Stop {
#[clap(long, value_name = "PATH")]
socket: Option<String>,
#[clap(long, value_enum, default_value_t = ShellKind::Fish)]
shell: ShellKind,
},
}
#[allow(unreachable_code)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn default_backend() -> String {
#[cfg(feature = "fido2")]
return "fido2".to_string();
#[cfg(feature = "systemd-creds")]
return "systemd-creds".to_string();
#[cfg(feature = "ssh-agent-piggyback")]
return "ssh-agent-piggyback".to_string();
#[cfg(feature = "tpm")]
return "tpm".to_string();
#[cfg(feature = "fprintd")]
return "fprintd".to_string();
#[cfg(feature = "secret-service")]
return "secret-service".to_string();
#[cfg(feature = "macos-keychain")]
return "macos-keychain".to_string();
"passphrase".to_string()
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn verify_cli() {
<Cli as CommandFactory>::command().debug_assert();
}
#[test]
fn start_command_defaults() -> anyhow::Result<()> {
let cli = Cli::try_parse_from(["mpa", "start"])?;
assert!(matches!(
cli.command(),
Commands::Start {
socket: None,
vault: None,
foreground: false,
shell: ShellKind::Fish,
passphrase_pipe: false,
..
}
));
Ok(())
}
#[test]
fn add_key_command() -> anyhow::Result<()> {
let cli = Cli::try_parse_from(["mpa", "add-key", "/tmp/key"])?;
match cli.command() {
Commands::AddKey { key_path, .. } => assert_eq!(key_path, "/tmp/key"),
_ => panic!("expected AddKey"),
}
Ok(())
}
#[test]
fn list_command() -> anyhow::Result<()> {
let cli = Cli::try_parse_from(["mpa", "list"])?;
assert!(matches!(cli.command(), Commands::List { .. }));
Ok(())
}
#[test]
fn lock_unlock_commands() -> anyhow::Result<()> {
assert!(matches!(
Cli::try_parse_from(["mpa", "lock"])?.command(),
Commands::Lock
));
assert!(matches!(
Cli::try_parse_from(["mpa", "unlock"])?.command(),
Commands::Unlock
));
Ok(())
}
#[test]
fn stop_command_defaults() -> anyhow::Result<()> {
let cli = Cli::try_parse_from(["mpa", "stop"])?;
assert!(matches!(
cli.command(),
Commands::Stop {
socket: None,
shell: ShellKind::Fish,
}
));
Ok(())
}
#[test]
fn stop_command_bash_shell() -> anyhow::Result<()> {
let cli = Cli::try_parse_from(["mpa", "stop", "--shell", "bash"])?;
assert!(matches!(
cli.command(),
Commands::Stop {
shell: ShellKind::Bash,
..
}
));
Ok(())
}
}