feat(cli): add Clap subcommand hierarchy and global argument flags (GH-20) #49

Merged
KofTwentyTwo merged 1 commits from feature/GH-20-clap-subcommand-hierarchy into main 2026-07-23 00:50:36 +00:00
3 changed files with 159 additions and 101 deletions

No files matched your search

+1
View File
@@ -15,4 +15,5 @@ tokio.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true
+122
View File
@@ -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<String>,
/// 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);
}
}
+36 -101
View File
@@ -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 <james@kof22.com>")]
#[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<dyn std::error::Error>> {
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");
}
}
}