use clap::{Parser, Subcommand}; use std::path::PathBuf; /// Witryna - minimalist Git-based static site deployment orchestrator #[derive(Debug, Parser)] #[command( name = "witryna", version, author, about = "Minimalist Git-based static site deployment orchestrator", long_about = "Minimalist Git-based static site deployment orchestrator.\n\n\ Witryna listens for webhook HTTP requests, pulls the corresponding Git \ repository (with automatic Git LFS fetch and submodule initialization), \ runs a user-defined build command inside an ephemeral container and \ publishes the resulting assets via atomic symlink switching.\n\n\ A health-check endpoint is available at GET /health (returns 200 OK).\n\n\ Witryna does not serve files, terminate TLS, or manage DNS. \ It is designed to sit behind a reverse proxy (Nginx, Caddy, etc.).", subcommand_required = true, arg_required_else_help = true )] pub struct Cli { /// Path to the configuration file. /// If not specified, searches: ./witryna.toml, $XDG_CONFIG_HOME/witryna/witryna.toml, /etc/witryna/witryna.toml #[arg(long, global = true, value_name = "FILE")] pub config: Option, #[command(subcommand)] pub command: Command, } #[derive(Debug, Subcommand)] pub enum Command { /// Start the deployment server (foreground) Serve, /// Validate configuration file and print summary Validate, /// Trigger a one-off build for a site (synchronous, no server) Run { /// Site name (as defined in witryna.toml) site: String, /// Stream full build output to stderr in real-time #[arg(long, short)] verbose: bool, }, /// Show deployment status for configured sites Status { /// Show last 10 deployments for a single site #[arg(long, short)] site: Option, /// Output in JSON format #[arg(long)] json: bool, }, } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::indexing_slicing)] mod tests { use super::*; #[test] fn run_parses_site_name() { let cli = Cli::try_parse_from(["witryna", "run", "my-site"]).unwrap(); match cli.command { Command::Run { site, verbose } => { assert_eq!(site, "my-site"); assert!(!verbose); } _ => panic!("expected Run command"), } } #[test] fn run_parses_verbose_flag() { let cli = Cli::try_parse_from(["witryna", "run", "my-site", "--verbose"]).unwrap(); match cli.command { Command::Run { site, verbose } => { assert_eq!(site, "my-site"); assert!(verbose); } _ => panic!("expected Run command"), } } #[test] fn status_parses_without_flags() { let cli = Cli::try_parse_from(["witryna", "status"]).unwrap(); match cli.command { Command::Status { site, json } => { assert!(site.is_none()); assert!(!json); } _ => panic!("expected Status command"), } } #[test] fn status_parses_site_filter() { let cli = Cli::try_parse_from(["witryna", "status", "--site", "my-site"]).unwrap(); match cli.command { Command::Status { site, json } => { assert_eq!(site.as_deref(), Some("my-site")); assert!(!json); } _ => panic!("expected Status command"), } } #[test] fn status_parses_json_flag() { let cli = Cli::try_parse_from(["witryna", "status", "--json"]).unwrap(); match cli.command { Command::Status { site, json } => { assert!(site.is_none()); assert!(json); } _ => panic!("expected Status command"), } } #[test] fn config_flag_is_optional() { let cli = Cli::try_parse_from(["witryna", "status"]).unwrap(); assert!(cli.config.is_none()); } #[test] fn config_flag_explicit_path() { let cli = Cli::try_parse_from(["witryna", "--config", "/etc/witryna.toml", "status"]).unwrap(); assert_eq!(cli.config, Some(PathBuf::from("/etc/witryna.toml"))); } }