From d0f12706bf3c1e4361649549b0181f20561710ae Mon Sep 17 00:00:00 2001 From: kubuntugamer Date: Fri, 9 Oct 2026 17:22:55 -0500 Subject: [PATCH] fix(cli): --help prints usage to stdout and exits 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asking for help is not an error, but `designcraft-cli --help` shared the catch-all arm with unknown commands, so it wrote the usage block to stderr and exited 1 — unusable in a pipeline and wrong by convention (`--version` already exits 0). Split the arm: `--help`, `-h` and `help` now print the same usage block to stdout and succeed, while an unknown command keeps it on stderr and still fails. The usage text and links line are hoisted into a shared `USAGE` const and `usage_footer()` so the two paths can't drift. Fixes #243 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- apps/designcraft-cli/src/main.rs | 31 ++++++++++++++++++++++--------- apps/designcraft-cli/tests/cli.rs | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/apps/designcraft-cli/src/main.rs b/apps/designcraft-cli/src/main.rs index 9e9eba25d..cfa00d122 100644 --- a/apps/designcraft-cli/src/main.rs +++ b/apps/designcraft-cli/src/main.rs @@ -33,6 +33,19 @@ mod perf; use designcraft_engine::Session; use serde_json::{Value, json}; +/// The usage block. Shared by `--help` (stdout, success) and an unknown command (stderr, failure). +const USAGE: &str = "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version"; + +/// The links line under the usage block. +fn usage_footer() -> String { + format!( + "\nCommunity: {} · {} · {}", + designcraft_engine::links::DISCORD, + designcraft_engine::links::APP_PAGE, + designcraft_engine::links::GITHUB + ) +} + /// stdout went away. A reader that stopped early (a closed pipe) ends the program quietly, as /// ripgrep does; any other write error is reported. fn stdout_failed(e: std::io::Error) -> ! { @@ -75,16 +88,16 @@ fn main() -> ExitCode { outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}\nIssues {ISSUES}"); ExitCode::SUCCESS } + // Asking for help is not an error: it goes to stdout and succeeds. An unknown command + // still prints the same usage to stderr and fails. + Some("--help" | "-h" | "help") => { + outln!("{USAGE}"); + outln!("{}", usage_footer()); + ExitCode::SUCCESS + } _ => { - eprintln!( - "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version" - ); - eprintln!( - "\nCommunity: {} · {} · {}", - designcraft_engine::links::DISCORD, - designcraft_engine::links::APP_PAGE, - designcraft_engine::links::GITHUB - ); + eprintln!("{USAGE}"); + eprintln!("{}", usage_footer()); ExitCode::FAILURE } } diff --git a/apps/designcraft-cli/tests/cli.rs b/apps/designcraft-cli/tests/cli.rs index 75f6d90b9..db4445a5d 100644 --- a/apps/designcraft-cli/tests/cli.rs +++ b/apps/designcraft-cli/tests/cli.rs @@ -67,6 +67,26 @@ fn run_cmd_references_and_describe() { assert!(v.as_array().unwrap().iter().all(|c| c.to_string().contains("footnote"))); } +/// `designcraft-cli --help` printed usage to stderr and exited 1; asking for help is not an +/// error (#243). It now goes to stdout and succeeds, while an unknown command keeps the same +/// usage on stderr and fails. +#[test] +fn help_goes_to_stdout_and_unknown_args_to_stderr() { + for flag in ["--help", "-h", "help"] { + let out = cli().arg(flag).output().unwrap(); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(out.status.success(), "{flag}: {:?}", out.status); + assert!(stdout.starts_with("usage: designcraft-cli"), "{flag}: {stdout:?}"); + assert!(stdout.contains("designcraft-cli --version"), "{flag}: {stdout:?}"); + assert!(out.stderr.is_empty(), "{flag} wrote to stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + // An unknown option is still an error: usage on stderr, nothing on stdout, non-zero exit. + let out = cli().arg("--no-such-option").output().unwrap(); + assert!(!out.status.success(), "{:?}", out.status); + assert!(out.stdout.is_empty(), "unexpected stdout: {}", String::from_utf8_lossy(&out.stdout)); + assert!(String::from_utf8_lossy(&out.stderr).starts_with("usage: designcraft-cli")); +} + /// `designcraft-cli commands | head -1` panicked with "failed printing to /// stdout: Broken pipe (os error 32)" and exit status 101. #[test]