diff --git a/apps/deckcraft-cli/src/main.rs b/apps/deckcraft-cli/src/main.rs index c450393f..78767ae6 100644 --- a/apps/deckcraft-cli/src/main.rs +++ b/apps/deckcraft-cli/src/main.rs @@ -54,6 +54,14 @@ fn main() -> ExitCode { Some("render") => report(render(rest)), Some("convert") => report(convert(rest)), Some("run") => report(run(rest)), + Some("--help" | "-h" | "help") => { + outln!("{USAGE}"); + ExitCode::SUCCESS + } + Some("commands") if rest.first().is_some_and(|a| is_help(a)) => { + outln!("{USAGE}"); + ExitCode::SUCCESS + } Some("commands") => { let s = Session::new(); let f = rest.first().map(|x| x.to_lowercase()); @@ -82,6 +90,10 @@ fn main() -> ExitCode { } } +fn is_help(arg: &str) -> bool { + matches!(arg, "--help" | "-h") +} + fn report(r: Result<(), String>) -> ExitCode { match r { Ok(()) => ExitCode::SUCCESS, @@ -103,6 +115,10 @@ fn open(s: &mut Session, input: Option<&str>, sample: bool) -> Result<(), String } fn info(args: &[String]) -> Result<(), String> { + if args.first().is_some_and(|a| is_help(a)) { + outln!("{USAGE}"); + return Ok(()); + } let path = args.first().ok_or("info needs a FILE")?; let mut s = Session::new(); open(&mut s, Some(path), false)?; @@ -127,6 +143,11 @@ fn render(args: &[String]) -> Result<(), String> { "--all" => all = true, "--edit" => edit = true, "--scale" => scale = it.next().and_then(|v| v.parse().ok()).ok_or("--scale needs a number")?, + "--help" | "-h" => { + outln!("{USAGE}"); + return Ok(()); + } + other if other.starts_with('-') => return Err(format!("unknown option `{other}`")), other if input.is_none() && !sample @@ -162,6 +183,10 @@ fn render(args: &[String]) -> Result<(), String> { } fn convert(args: &[String]) -> Result<(), String> { + if args.first().is_some_and(|a| is_help(a)) { + outln!("{USAGE}"); + return Ok(()); + } let (Some(input), Some(out)) = (args.first(), args.get(1)) else { return Err("convert needs IN and OUT".into()) }; let mut s = Session::new(); open(&mut s, Some(input), false)?; @@ -192,6 +217,10 @@ fn run(args: &[String]) -> Result<(), String> { "--save" => save = Some(it.next().cloned().ok_or("--save needs a path")?), "--export" => export = Some(it.next().cloned().ok_or("--export needs a path")?), "--print" => print = true, + "--help" | "-h" => { + outln!("{USAGE}"); + return Ok(()); + } other => return Err(format!("unknown option `{other}`")), } } @@ -217,6 +246,10 @@ fn run(args: &[String]) -> Result<(), String> { } fn describe(id: Option<&str>) -> Result<(), String> { + if id.is_some_and(is_help) { + outln!("{USAGE}"); + return Ok(()); + } let id = id.ok_or("describe needs a command id")?; let c = deckcraft_engine::find_command(id).ok_or_else(|| format!("unknown command `{id}`"))?; outln!("{} — {}\n where: {}\n shortcut: {}\n params: {}", c.id, c.label, c.menu.join(" ▸ "), c.shortcut.unwrap_or("—"), c.params); @@ -233,6 +266,10 @@ fn app(args: &[String]) -> Result<(), String> { match a.as_str() { "--port" => port = it.next().cloned().ok_or("--port needs a value")?, "--method" => method = Some(it.next().cloned().ok_or("--method needs a name")?), + "--help" | "-h" => { + outln!("{USAGE}"); + return Ok(()); + } other => rest.push(other.to_string()), } } @@ -261,6 +298,10 @@ fn mcp(args: &[String]) -> Result<(), String> { match a.as_str() { "--connect" => connect = Some(it.next().cloned().ok_or("--connect needs a port or host:port")?), "--sample" => sample = true, + "--help" | "-h" => { + outln!("{USAGE}"); + return Ok(()); + } other => return Err(format!("unknown mcp option `{other}`")), } } @@ -278,3 +319,35 @@ fn mcp(args: &[String]) -> Result<(), String> { let stdin = std::io::stdin(); server.serve(stdin.lock(), std::io::stdout()).map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn args(a: &[&str]) -> Vec { + a.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn render_help_prints_usage_and_writes_nothing() { + assert!(render(&args(&["--help"])).is_ok()); + assert!(render(&args(&["-h"])).is_ok()); + assert!(!std::path::Path::new("--help").exists()); + } + + #[test] + fn render_rejects_unknown_flag() { + let err = render(&args(&["--sample", "--scael", "2", "out.png"])).unwrap_err(); + assert_eq!(err, "unknown option `--scael`"); + assert!(!std::path::Path::new("--scael").exists()); + } + + #[test] + fn other_subcommands_accept_help() { + assert!(info(&args(&["--help"])).is_ok()); + assert!(convert(&args(&["-h"])).is_ok()); + assert!(run(&args(&["--help"])).is_ok()); + assert!(describe(Some("--help")).is_ok()); + assert!(mcp(&args(&["-h"])).is_ok()); + } +}