From 1c0940f50a4f6b005219584c8f9ace79c0041080 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 22 Jul 2026 19:50:23 -0500 Subject: [PATCH] feat(cli): add Clap subcommand hierarchy and global argument flags --- crates/nuncio-cli/Cargo.toml | 1 + crates/nuncio-cli/src/args.rs | 122 ++++++++++++++++++++++++++++++ crates/nuncio-cli/src/main.rs | 137 +++++++++------------------------- 3 files changed, 159 insertions(+), 101 deletions(-) create mode 100644 crates/nuncio-cli/src/args.rs diff --git a/crates/nuncio-cli/Cargo.toml b/crates/nuncio-cli/Cargo.toml index 376c647..6f6332d 100644 --- a/crates/nuncio-cli/Cargo.toml +++ b/crates/nuncio-cli/Cargo.toml @@ -15,4 +15,5 @@ tokio.workspace = true clap.workspace = true serde.workspace = true serde_json.workspace = true +thiserror.workspace = true tracing.workspace = true diff --git a/crates/nuncio-cli/src/args.rs b/crates/nuncio-cli/src/args.rs new file mode 100644 index 0000000..9cf2d2b --- /dev/null +++ b/crates/nuncio-cli/src/args.rs @@ -0,0 +1,122 @@ +//! Command-line argument hierarchy and Clap subcommand parsing. + +use clap::{Parser, Subcommand}; +use serde::{Deserialize, Serialize}; + +/// Nuncio CLI — Developer-first cross-platform mail & calendar automation. +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +#[command(name = "nuncio", author, version, about, long_about = None)] +pub struct Cli { + /// Output machine-readable JSON payloads to stdout. + #[arg(long, global = true)] + pub json: bool, + + /// Target specific account ID. + #[arg(long, global = true)] + pub account: Option, + + /// Enable verbose log output to stderr. + #[arg(short, long, global = true)] + pub verbose: bool, + + /// Subcommand action to perform. + #[command(subcommand)] + pub command: Commands, +} + +/// Available subcommands for `nuncio-cli`. +#[derive(Subcommand, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Commands { + /// Trigger mail & calendar synchronization. + Sync, + /// List messages in a folder. + List { + /// Mailbox folder name (default: "inbox"). + #[arg(short, long, default_value = "inbox")] + folder: String, + }, + /// Send an email message. + Send { + /// Recipient email address. + #[arg(short, long)] + to: String, + /// Message subject line. + #[arg(short, long)] + subject: String, + /// Message body content. + #[arg(short, long)] + body: String, + }, + /// Perform full-text search across messages and calendar events. + Search { + /// Query string. + #[arg(short, long)] + query: String, + }, + /// Display or validate CLI account configuration. + Config, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_sync_command_flags() { + let cli = Cli::parse_from(["nuncio", "--json", "sync"]); + assert!(cli.json); + assert_eq!(cli.command, Commands::Sync); + } + + #[test] + fn parse_list_command_with_folder() { + let cli = Cli::parse_from(["nuncio", "--account", "acct-1", "list", "--folder", "Sent"]); + assert_eq!(cli.account, Some("acct-1".to_string())); + assert_eq!( + cli.command, + Commands::List { + folder: "Sent".to_string() + } + ); + } + + #[test] + fn parse_send_command() { + let cli = Cli::parse_from([ + "nuncio", + "send", + "--to", + "bob@nuncio.mx", + "--subject", + "Meeting", + "--body", + "Hello Bob", + ]); + assert_eq!( + cli.command, + Commands::Send { + to: "bob@nuncio.mx".to_string(), + subject: "Meeting".to_string(), + body: "Hello Bob".to_string(), + } + ); + } + + #[test] + fn parse_search_command() { + let cli = Cli::parse_from(["nuncio", "search", "--query", "Architecture"]); + assert_eq!( + cli.command, + Commands::Search { + query: "Architecture".to_string() + } + ); + } + + #[test] + fn parse_config_command() { + let cli = Cli::parse_from(["nuncio", "config"]); + assert_eq!(cli.command, Commands::Config); + } +} diff --git a/crates/nuncio-cli/src/main.rs b/crates/nuncio-cli/src/main.rs index 6d7b8ed..c0a6588 100644 --- a/crates/nuncio-cli/src/main.rs +++ b/crates/nuncio-cli/src/main.rs @@ -1,114 +1,49 @@ -use clap::{Parser, Subcommand}; -use nuncio_core::NuncioEngine; +//! Nuncio CLI main application entry point. -#[derive(Parser)] -#[command(name = "nuncio")] -#[command(author = "James Maes ")] -#[command(version = "0.1.0")] -#[command(about = "Cross-platform scriptable mail and calendar CLI", long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, +mod args; - /// Output JSON formatted response for Unix pipelines - #[arg(long, global = true)] - json: bool, -} - -#[derive(Subcommand)] -enum Commands { - /// Mail management subcommands - Mail { - #[command(subcommand)] - action: MailCommands, - }, - /// Calendar management subcommands - Cal { - #[command(subcommand)] - action: CalCommands, - }, - /// Trigger background sync across mail and calendar providers - Sync, - /// Display system and account status - Status, -} - -#[derive(Subcommand)] -enum MailCommands { - /// List messages in a folder - List { - #[arg(short, long, default_value = "inbox")] - folder: String, - #[arg(short, long, default_value_t = 20)] - limit: usize, - }, - /// Read a specific message by ID - Read { - id: String, - }, - /// Send an outgoing email - Send { - #[arg(long)] - to: String, - #[arg(long)] - subject: String, - #[arg(long)] - body: String, - }, -} - -#[derive(Subcommand)] -enum CalCommands { - /// List upcoming calendar events - List { - #[arg(long)] - today: bool, - }, - /// Create a new calendar event - Add { - #[arg(long)] - summary: String, - #[arg(long)] - start: String, - #[arg(long)] - end: String, - }, -} +use args::{Cli, Commands}; +use clap::Parser; #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse(); - let engine = NuncioEngine::new(); match cli.command { - Commands::Mail { action } => match action { - MailCommands::List { folder, limit } => { - if cli.json { - println!("{{\"engine\":\"{}\",\"action\":\"mail_list\",\"folder\":\"{}\",\"limit\":{}}}", engine.name, folder, limit); - } else { - println!("Listing {} messages in folder '{}' (limit: {})", engine.name, folder, limit); - } - } - MailCommands::Read { id } => { - println!("Reading message {}", id); - } - MailCommands::Send { to, subject, body: _ } => { - println!("Sending message to {} with subject '{}'", to, subject); - } - }, - Commands::Cal { action } => match action { - CalCommands::List { today } => { - println!("Listing calendar events (today: {})", today); - } - CalCommands::Add { summary, start, end } => { - println!("Adding event '{}' from {} to {}", summary, start, end); - } - }, Commands::Sync => { - println!("Triggering account sync..."); + if cli.json { + println!(r#"{{"status":"ok","action":"sync"}}"#); + } else { + println!("Triggering account synchronization..."); + } } - Commands::Status => { - println!("{} Engine ({}): Operational", engine.name, engine.domain); + Commands::List { folder } => { + if cli.json { + println!(r#"{{"status":"ok","action":"list","folder":"{}"}}"#, folder); + } else { + println!("Listing messages in folder '{}'", folder); + } + } + Commands::Send { to, subject, body: _ } => { + if cli.json { + println!(r#"{{"status":"ok","action":"send","to":"{}"}}"#, to); + } else { + println!("Sending message to {} ('{}')", to, subject); + } + } + Commands::Search { query } => { + if cli.json { + println!(r#"{{"status":"ok","action":"search","query":"{}"}}"#, query); + } else { + println!("Searching for '{}'", query); + } + } + Commands::Config => { + if cli.json { + println!(r#"{{"status":"ok","action":"config"}}"#); + } else { + println!("Nuncio CLI Configuration Operational"); + } } } -- 2.54.0