use std::path::PathBuf;
use super::Init;
#[derive(Debug, clap::Args)]
pub struct StartupArgs {
#[arg(long)]
pub user: Option<String>,
#[arg(long, value_enum)]
pub init: Option<Init>,
}
#[derive(Debug, clap::Args)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: clap_complete::aot::Shell,
}
#[derive(Debug, clap::Args)]
pub struct StyleArgs {
#[arg(value_enum)]
pub level: Option<crate::style::StyleLevel>,
}
#[derive(Debug, clap::Args)]
pub struct DaemonArgs {
#[command(subcommand)]
pub cmd: Option<DaemonCmd>,
#[arg(long)]
pub no_restore: bool,
#[arg(long)]
pub foreground: bool,
#[arg(
long,
value_name = "BOOL",
num_args = 0..=1,
default_missing_value = "true",
value_parser = bool_flag
)]
pub log_json: Option<bool>,
#[arg(long, value_name = "LEVEL", value_parser = log_level_flag)]
pub log_level: Option<shep_core::config::LogLevel>,
#[arg(long, value_name = "PATH")]
pub socket: Option<PathBuf>,
#[arg(long, value_name = "DURATION", value_parser = duration_flag)]
pub max_cron_sleep: Option<shep_core::values::UpDuration>,
}
#[derive(Debug, clap::Subcommand)]
pub enum DaemonCmd {
Reload,
}
#[derive(Debug, clap::Args)]
pub struct InitArgs {
#[arg(value_name = "PATH")]
pub path: Option<PathBuf>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, clap::Args)]
pub struct RuntimeArgs {
pub target: Option<String>,
#[arg(long, hide = true)]
pub supervise: bool,
}
#[derive(Debug, clap::Args)]
pub struct DevArgs {
pub target: Option<String>,
#[arg(long)]
pub name: Option<String>,
}
fn bool_flag(value: &str) -> Result<bool, String> {
shep_core::config::parse_daemon_bool(value)
.ok_or_else(|| format!("expected one of 1, 0, true, false; got `{value}`"))
}
fn log_level_flag(value: &str) -> Result<shep_core::config::LogLevel, String> {
shep_core::config::LogLevel::from_name(value).ok_or_else(|| {
format!("expected one of off, error, warn, info, debug, trace; got `{value}`")
})
}
fn duration_flag(value: &str) -> Result<shep_core::values::UpDuration, String> {
value
.parse::<shep_core::values::UpDuration>()
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use crate::cli::{Cli, Commands};
#[test]
fn log_json_has_three_states() {
use clap::Parser;
let cases = [
(vec!["shep", "daemon"], None),
(vec!["shep", "daemon", "--log-json"], Some(true)),
(vec!["shep", "daemon", "--log-json=false"], Some(false)),
(vec!["shep", "daemon", "--log-json=1"], Some(true)),
];
for (argv, expected) in cases {
match Cli::try_parse_from(&argv).unwrap().command {
Commands::Daemon(args) => assert_eq!(args.log_json, expected, "{argv:?}"),
other => panic!("expected Daemon, got {other:?}"),
}
}
}
#[test]
fn a_bare_shep_daemon_still_boots_and_is_not_a_subcommand_error() {
use clap::Parser;
let parsed =
Cli::try_parse_from(["shep", "daemon"]).expect("`shep daemon` must still parse");
let Commands::Daemon(args) = parsed.command else {
panic!("`shep daemon` must still parse as the daemon verb");
};
assert!(
args.cmd.is_none(),
"bare `shep daemon` must remain the boot path"
);
}
#[test]
fn the_daemon_flags_still_parse_alongside_the_subcommand() {
use clap::Parser;
let argv = [
"shep",
"daemon",
"--foreground",
"--no-restore",
"--log-json=false",
"--log-level",
"info",
"--socket",
"run/shep.sock",
"--max-cron-sleep",
"30s",
];
let parsed = Cli::try_parse_from(argv).unwrap_or_else(|e| panic!("{argv:?} failed: {e}"));
let Commands::Daemon(args) = parsed.command else {
panic!("expected the daemon verb")
};
assert!(args.foreground);
assert!(args.no_restore);
assert_eq!(args.log_json, Some(false));
assert_eq!(args.log_level, Some(shep_core::config::LogLevel::Info));
assert_eq!(args.socket.as_deref(), Some(Path::new("run/shep.sock")));
assert_eq!(
args.max_cron_sleep
.map(shep_core::values::UpDuration::as_duration),
Some(std::time::Duration::from_secs(30))
);
assert!(
args.cmd.is_none(),
"flags alone must not select a subcommand"
);
}
#[test]
fn daemon_reload_parses_as_the_reload_subcommand() {
use clap::Parser;
let parsed = Cli::try_parse_from(["shep", "daemon", "reload"])
.expect("`shep daemon reload` must parse");
let Commands::Daemon(args) = parsed.command else {
panic!("expected the daemon verb")
};
assert!(
matches!(args.cmd, Some(DaemonCmd::Reload)),
"got {:?}",
args.cmd
);
}
#[test]
fn the_flag_bool_grammar_matches_the_env_grammar() {
use clap::Parser;
for wider in ["--log-json=yes", "--log-json=on", "--log-json=TRUE"] {
assert!(
Cli::try_parse_from(["shep", "daemon", wider]).is_err(),
"{wider} must not parse"
);
}
}
#[test]
fn runtime_parses_and_its_supervise_flag_is_hidden() {
use clap::Parser;
let bare = Cli::try_parse_from(["shep", "runtime"]).unwrap();
let Commands::Runtime(args) = bare.command else {
panic!("expected runtime")
};
assert_eq!(args.target, None, "no target means discover");
assert!(!args.supervise, "a person never sets this");
let with_target = Cli::try_parse_from(["shep", "runtime", "./Flockfile.toml"]).unwrap();
let Commands::Runtime(args) = with_target.command else {
panic!("expected runtime")
};
assert_eq!(args.target.as_deref(), Some("./Flockfile.toml"));
let supervised = Cli::try_parse_from(["shep", "runtime", "--supervise"]).unwrap();
let Commands::Runtime(args) = supervised.command else {
panic!("expected runtime")
};
assert!(args.supervise, "the init passes --supervise to its child");
use clap::CommandFactory;
let cmd = Cli::command();
let runtime = cmd.find_subcommand("runtime").unwrap();
assert!(!runtime.is_hide_set(), "runtime is a real, documented verb");
let supervise_arg = runtime
.get_arguments()
.find(|a| a.get_id().as_str() == "supervise")
.expect("RuntimeArgs must still carry a hidden `supervise` field");
assert!(
supervise_arg.is_hide_set(),
"--supervise must stay hidden from --help"
);
}
#[test]
fn style_verb_parses_the_same_grammar_as_the_style_flag() {
use crate::style::StyleLevel;
use clap::Parser;
let cli = Cli::try_parse_from(["shep", "style"]).unwrap();
match cli.command {
Commands::Style(args) => {
assert_eq!(args.level, None, "bare `shep style` still reports")
}
other => panic!("expected Style, got {other:?}"),
}
for (raw, expected) in [
("full", StyleLevel::Full),
("plain", StyleLevel::Plain),
("bare", StyleLevel::Bare),
] {
let cli = Cli::try_parse_from(["shep", "style", raw]).unwrap();
match cli.command {
Commands::Style(args) => assert_eq!(args.level, Some(expected), "style {raw}"),
other => panic!("expected Style, got {other:?}"),
}
}
let bad_flag = Cli::try_parse_from(["shep", "--style", "loud", "flock"]).unwrap_err();
let bad_verb = Cli::try_parse_from(["shep", "style", "loud"]).unwrap_err();
assert_eq!(
bad_flag.kind(),
bad_verb.kind(),
"a bad value fails the same way through either spelling"
);
}
}