diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..ab191a4 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,134 @@ +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<PathBuf>, + + #[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<String>, + /// 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"))); + } +} |
