use argh::FromArgs; use std::path::PathBuf; /// Minimalist Git-based static site deployment orchestrator #[derive(Debug, FromArgs)] pub struct Cli { #[argh(subcommand)] pub command: Command, } #[derive(Debug, FromArgs)] #[argh(subcommand)] pub enum Command { Serve(ServeCmd), Validate(ValidateCmd), Run(RunCmd), Status(StatusCmd), Switch(SwitchCmd), Cleanup(CleanupCmd), } impl Command { #[must_use] pub fn config(&self) -> Option<&std::path::Path> { match self { Self::Serve(c) => c.config.as_deref(), Self::Validate(c) => c.config.as_deref(), Self::Run(c) => c.config.as_deref(), Self::Status(c) => c.config.as_deref(), Self::Switch(c) => c.config.as_deref(), Self::Cleanup(c) => c.config.as_deref(), } } } /// Start the deployment server (foreground) #[derive(Debug, FromArgs)] #[argh(subcommand, name = "serve")] pub struct ServeCmd { /// path to configuration file #[argh(option)] pub config: Option, } /// Validate configuration file and print summary #[derive(Debug, FromArgs)] #[argh(subcommand, name = "validate")] pub struct ValidateCmd { /// path to configuration file #[argh(option)] pub config: Option, } /// Trigger a one-off build for a site (synchronous, no server) #[derive(Debug, FromArgs)] #[argh(subcommand, name = "run")] pub struct RunCmd { /// path to configuration file #[argh(option)] pub config: Option, /// site name (as defined in witryna.toml) #[argh(positional)] pub site: String, /// stream full build output to stderr in real-time #[argh(switch, short = 'v')] pub verbose: bool, } /// Show deployment status for configured sites #[derive(Debug, FromArgs)] #[argh(subcommand, name = "status")] pub struct StatusCmd { /// path to configuration file #[argh(option)] pub config: Option, /// site name (if omitted, shows all sites) #[argh(positional)] pub site: Option, /// output in JSON format #[argh(switch)] pub json: bool, } /// Switch the active build symlink (rollback) #[derive(Debug, FromArgs)] #[argh(subcommand, name = "switch")] pub struct SwitchCmd { /// path to configuration file #[argh(option)] pub config: Option, /// site name (as defined in witryna.toml) #[argh(positional)] pub site: String, /// build timestamp to switch to (from `witryna status `) #[argh(positional)] pub build: String, } /// Remove old builds and logs #[derive(Debug, FromArgs)] #[argh(subcommand, name = "cleanup")] pub struct CleanupCmd { /// path to configuration file #[argh(option)] pub config: Option, /// site name (if omitted, cleans all sites) #[argh(positional)] pub site: Option, /// number of builds to keep per site (overrides `max_builds_to_keep`) #[argh(option)] pub keep: Option, } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::indexing_slicing)] mod tests { use super::*; #[test] fn run_parses_site_name() { let cli = Cli::from_args(&["witryna"], &["run", "my-site"]).unwrap(); match cli.command { Command::Run(cmd) => { assert_eq!(cmd.site, "my-site"); assert!(!cmd.verbose); } _ => unreachable!("expected Run command"), } } #[test] fn run_parses_verbose_flag() { let cli = Cli::from_args(&["witryna"], &["run", "my-site", "--verbose"]).unwrap(); match cli.command { Command::Run(cmd) => { assert_eq!(cmd.site, "my-site"); assert!(cmd.verbose); } _ => unreachable!("expected Run command"), } } #[test] fn status_parses_without_flags() { let cli = Cli::from_args(&["witryna"], &["status"]).unwrap(); match cli.command { Command::Status(cmd) => { assert!(cmd.site.is_none()); assert!(!cmd.json); } _ => unreachable!("expected Status command"), } } #[test] fn status_parses_site_filter() { let cli = Cli::from_args(&["witryna"], &["status", "my-site"]).unwrap(); match cli.command { Command::Status(cmd) => { assert_eq!(cmd.site.as_deref(), Some("my-site")); assert!(!cmd.json); } _ => unreachable!("expected Status command"), } } #[test] fn status_parses_json_flag() { let cli = Cli::from_args(&["witryna"], &["status", "--json"]).unwrap(); match cli.command { Command::Status(cmd) => { assert!(cmd.site.is_none()); assert!(cmd.json); } _ => unreachable!("expected Status command"), } } #[test] fn cleanup_parses_without_args() { let cli = Cli::from_args(&["witryna"], &["cleanup"]).unwrap(); match cli.command { Command::Cleanup(cmd) => { assert!(cmd.site.is_none()); assert!(cmd.keep.is_none()); } _ => unreachable!("expected Cleanup command"), } } #[test] fn cleanup_parses_site_name() { let cli = Cli::from_args(&["witryna"], &["cleanup", "my-site"]).unwrap(); match cli.command { Command::Cleanup(cmd) => { assert_eq!(cmd.site.as_deref(), Some("my-site")); assert!(cmd.keep.is_none()); } _ => unreachable!("expected Cleanup command"), } } #[test] fn cleanup_parses_keep_flag() { let cli = Cli::from_args(&["witryna"], &["cleanup", "--keep", "3"]).unwrap(); match cli.command { Command::Cleanup(cmd) => { assert!(cmd.site.is_none()); assert_eq!(cmd.keep, Some(3)); } _ => unreachable!("expected Cleanup command"), } } #[test] fn cleanup_parses_site_and_keep() { let cli = Cli::from_args(&["witryna"], &["cleanup", "my-site", "--keep", "3"]).unwrap(); match cli.command { Command::Cleanup(cmd) => { assert_eq!(cmd.site.as_deref(), Some("my-site")); assert_eq!(cmd.keep, Some(3)); } _ => unreachable!("expected Cleanup command"), } } #[test] fn switch_parses_site_and_build() { let cli = Cli::from_args( &["witryna"], &["switch", "my-site", "20260126-143000-123456"], ) .unwrap(); match cli.command { Command::Switch(cmd) => { assert_eq!(cmd.site, "my-site"); assert_eq!(cmd.build, "20260126-143000-123456"); assert!(cmd.config.is_none()); } _ => unreachable!("expected Switch command"), } } #[test] fn switch_parses_config_flag() { let cli = Cli::from_args( &["witryna"], &[ "switch", "--config", "/etc/witryna.toml", "my-site", "20260126-143000-123456", ], ) .unwrap(); match cli.command { Command::Switch(cmd) => { assert_eq!(cmd.site, "my-site"); assert_eq!(cmd.build, "20260126-143000-123456"); assert_eq!( cmd.config, Some(std::path::PathBuf::from("/etc/witryna.toml")) ); } _ => unreachable!("expected Switch command"), } } #[test] fn config_flag_is_optional() { let cli = Cli::from_args(&["witryna"], &["status"]).unwrap(); assert!(cli.command.config().is_none()); } #[test] fn config_flag_explicit_path() { let cli = Cli::from_args(&["witryna"], &["status", "--config", "/etc/witryna.toml"]).unwrap(); assert_eq!( cli.command.config(), Some(std::path::Path::new("/etc/witryna.toml")) ); } }