use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(version, about, long_about = None)] pub struct Cli { /// Start the agent server #[arg(short, long, group = "mode")] pub serve: bool, #[command(subcommand)] pub command: Option, } #[derive(Subcommand)] pub enum Commands { #[command(about = "Import certificates for agent from zip file or separate cert and key files")] ImportCerts { // Zip file input, mutually exclusive with separate cert and key file inputs /// Zip file containing ca.pem cert.pem and key.pem #[arg(value_name = "ZIP_FILE", group = "input_source")] zip: Option, /// Certificate name in zip file, required if using zip input #[arg( long, group = "input_source", requires = "zip", default_value = "cert.pem", value_name = "CERT_NAME" )] cert_name: Option, /// Key name in zip file, required if using zip input #[arg( long, group = "input_source", requires = "zip", default_value = "key.pem", value_name = "KEY_NAME" )] key_name: Option, /// CA certificate name in zip file, required if using zip input #[arg( long, group = "input_source", requires = "zip", default_value = "ca.pem", value_name = "CA_NAME" )] ca_name: Option, // Separate cert and key file inputs, required if not using zip input /// Certificate file path #[arg( long, group = "input_source", requires = "key", conflicts_with = "zip", value_name = "CERT_FILE" )] cert: Option, /// Key file path #[arg( long, group = "input_source", requires = "cert", conflicts_with = "zip", value_name = "KEY_FILE" )] key: Option, /// Master CA certificate file path for verifying master identity, optional if the CA certificate is already trusted by the system /// This is required if the master server uses a self-signed certificate that is not trusted by the system #[arg( long, group = "input_source", conflicts_with = "zip", value_name = "CA_CERT_FILE" )] ca_cert: Option, }, } #[cfg(test)] mod tests { use clap::Parser; use super::{Cli, Commands}; #[test] fn parses_serve_flag_without_subcommand() { let parsed = Cli::try_parse_from(["nxmesh-agent", "--serve"]); assert!(parsed.is_ok()); let parsed = parsed.ok(); assert!(parsed.is_some()); let parsed = parsed.unwrap_or_else(|| unreachable!()); assert!(parsed.serve); assert!(parsed.command.is_none()); } #[test] fn parses_import_certs_with_zip_defaults() { let parsed = Cli::try_parse_from(["nxmesh-agent", "import-certs", "bundle.zip"]); assert!(parsed.is_ok()); let parsed = parsed.ok(); assert!(parsed.is_some()); let parsed = parsed.unwrap_or_else(|| unreachable!()); match parsed.command { Some(Commands::ImportCerts { zip, cert_name, key_name, ca_name, cert, key, ca_cert, }) => { assert_eq!(zip.as_deref(), Some("bundle.zip")); assert_eq!(cert_name.as_deref(), Some("cert.pem")); assert_eq!(key_name.as_deref(), Some("key.pem")); assert_eq!(ca_name.as_deref(), Some("ca.pem")); assert!(cert.is_none()); assert!(key.is_none()); assert!(ca_cert.is_none()); } _ => unreachable!(), } } #[test] fn rejects_import_certs_with_separate_files() { let parsed = Cli::try_parse_from([ "nxmesh-agent", "import-certs", "--cert", "agent.crt", "--key", "agent.key", "--ca-cert", "ca.crt", ]); assert!(parsed.is_err()); } #[test] fn rejects_conflicting_zip_and_separate_inputs() { let parsed = Cli::try_parse_from([ "nxmesh-agent", "import-certs", "bundle.zip", "--cert", "agent.crt", "--key", "agent.key", ]); assert!(parsed.is_err()); } }