summaryrefslogtreecommitdiff
path: root/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs303
1 files changed, 227 insertions, 76 deletions
diff --git a/src/cli.rs b/src/cli.rs
index ab191a4..4d958ea 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,57 +1,114 @@
-use clap::{Parser, Subcommand};
+use argh::FromArgs;
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
-)]
+/// Minimalist Git-based static site deployment orchestrator
+#[derive(Debug, FromArgs)]
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)]
+ #[argh(subcommand)]
pub command: Command,
}
-#[derive(Debug, Subcommand)]
+#[derive(Debug, FromArgs)]
+#[argh(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,
- },
+ 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<PathBuf>,
+}
+
+/// 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<PathBuf>,
+}
+
+/// 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<PathBuf>,
+ /// 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<PathBuf>,
+ /// site name (if omitted, shows all sites)
+ #[argh(positional)]
+ pub site: Option<String>,
+ /// 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<PathBuf>,
+ /// site name (as defined in witryna.toml)
+ #[argh(positional)]
+ pub site: String,
+ /// build timestamp to switch to (from `witryna status <site>`)
+ #[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<PathBuf>,
+ /// site name (if omitted, cleans all sites)
+ #[argh(positional)]
+ pub site: Option<String>,
+ /// number of builds to keep per site (overrides `max_builds_to_keep`)
+ #[argh(option)]
+ pub keep: Option<u32>,
}
#[cfg(test)]
@@ -61,74 +118,168 @@ mod tests {
#[test]
fn run_parses_site_name() {
- let cli = Cli::try_parse_from(["witryna", "run", "my-site"]).unwrap();
+ let cli = Cli::from_args(&["witryna"], &["run", "my-site"]).unwrap();
match cli.command {
- Command::Run { site, verbose } => {
- assert_eq!(site, "my-site");
- assert!(!verbose);
+ Command::Run(cmd) => {
+ assert_eq!(cmd.site, "my-site");
+ assert!(!cmd.verbose);
}
- _ => panic!("expected Run command"),
+ _ => unreachable!("expected Run command"),
}
}
#[test]
fn run_parses_verbose_flag() {
- let cli = Cli::try_parse_from(["witryna", "run", "my-site", "--verbose"]).unwrap();
+ let cli = Cli::from_args(&["witryna"], &["run", "my-site", "--verbose"]).unwrap();
match cli.command {
- Command::Run { site, verbose } => {
- assert_eq!(site, "my-site");
- assert!(verbose);
+ Command::Run(cmd) => {
+ assert_eq!(cmd.site, "my-site");
+ assert!(cmd.verbose);
}
- _ => panic!("expected Run command"),
+ _ => unreachable!("expected Run command"),
}
}
#[test]
fn status_parses_without_flags() {
- let cli = Cli::try_parse_from(["witryna", "status"]).unwrap();
+ let cli = Cli::from_args(&["witryna"], &["status"]).unwrap();
match cli.command {
- Command::Status { site, json } => {
- assert!(site.is_none());
- assert!(!json);
+ Command::Status(cmd) => {
+ assert!(cmd.site.is_none());
+ assert!(!cmd.json);
}
- _ => panic!("expected Status command"),
+ _ => unreachable!("expected Status command"),
}
}
#[test]
fn status_parses_site_filter() {
- let cli = Cli::try_parse_from(["witryna", "status", "--site", "my-site"]).unwrap();
+ let cli = Cli::from_args(&["witryna"], &["status", "my-site"]).unwrap();
match cli.command {
- Command::Status { site, json } => {
- assert_eq!(site.as_deref(), Some("my-site"));
- assert!(!json);
+ Command::Status(cmd) => {
+ assert_eq!(cmd.site.as_deref(), Some("my-site"));
+ assert!(!cmd.json);
}
- _ => panic!("expected Status command"),
+ _ => unreachable!("expected Status command"),
}
}
#[test]
fn status_parses_json_flag() {
- let cli = Cli::try_parse_from(["witryna", "status", "--json"]).unwrap();
+ 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::Status { site, json } => {
- assert!(site.is_none());
- assert!(json);
+ 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"))
+ );
}
- _ => panic!("expected Status command"),
+ _ => unreachable!("expected Switch command"),
}
}
#[test]
fn config_flag_is_optional() {
- let cli = Cli::try_parse_from(["witryna", "status"]).unwrap();
- assert!(cli.config.is_none());
+ let cli = Cli::from_args(&["witryna"], &["status"]).unwrap();
+ assert!(cli.command.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")));
+ Cli::from_args(&["witryna"], &["status", "--config", "/etc/witryna.toml"]).unwrap();
+ assert_eq!(
+ cli.command.config(),
+ Some(std::path::Path::new("/etc/witryna.toml"))
+ );
}
}