From 8be8fa6b48905bc6ed57429933cf33086e42ed72 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:36:42 +0000 Subject: [PATCH 1/2] feat(clap_usage)!: expose spec(), publish against usage-lib 4 BREAKING CHANGE: clap_usage now requires usage-lib 4. The published 2.0.3 still requires usage-lib ^2.0.3 even though the in-repo crate has built against the workspace version for a while, so every consumer was pinned to 2.x specs. That pin is why hk, pitchfork and communique could not adopt `effect=`: generate() renders straight to a writer, so the Spec never escapes and there is nothing to annotate, and 2.x has no `effect` field to set anyway. All three ended up inlining generate()'s four lines and depending on usage-lib directly. Add `spec()`, which returns the Spec with `bin` already set. generate() is now spec() plus two writeln!s, so its output is unchanged: let mut spec = clap_usage::spec(&mut cmd, "mycli"); spec.cmd.subcommands.get_mut("rm").unwrap().effect = Some(Destructive); println!("{spec}"); Version goes straight to 4.0.0 to match usage-lib and usage-cli; a crate whose entire purpose is producing usage-lib types is less confusing tracking their major than carrying its own. Two tests: that a spec can be annotated before rendering, and that generate() still emits the same header and body. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- clap_usage/Cargo.toml | 2 +- clap_usage/src/generate.rs | 26 +++++++++++++++++++++++- clap_usage/src/lib.rs | 2 +- clap_usage/tests/spec_accessor.rs | 33 +++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 clap_usage/tests/spec_accessor.rs diff --git a/Cargo.lock b/Cargo.lock index e6dea89fc..3f15a72ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,7 +319,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_usage" -version = "2.0.3" +version = "4.0.0" dependencies = [ "clap", "insta", diff --git a/Cargo.toml b/Cargo.toml index faba13617..4fffe9b61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ authors = ["Jeff Dickey @jdx"] license = "MIT" [workspace.dependencies] -clap_usage = { path = "./clap_usage", version = "2.0.3" } +clap_usage = { path = "./clap_usage", version = "4.0.0" } usage-cli = { path = "./cli" } usage-lib = { path = "./lib", version = "4.0.0", features = ["clap"] } diff --git a/clap_usage/Cargo.toml b/clap_usage/Cargo.toml index 60b2c984c..70ae7a866 100644 --- a/clap_usage/Cargo.toml +++ b/clap_usage/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap_usage" edition = "2021" -version = "2.0.3" +version = "4.0.0" include = [ "/Cargo.toml", "/Cargo.lock", diff --git a/clap_usage/src/generate.rs b/clap_usage/src/generate.rs index df2864a40..2f61bbdf3 100644 --- a/clap_usage/src/generate.rs +++ b/clap_usage/src/generate.rs @@ -1,9 +1,33 @@ use clap::Command; use std::io::Write; -pub fn generate>(cmd: &mut Command, bin_name: S, buf: &mut dyn Write) { +/// The usage spec for a clap command, ready to inspect or annotate. +/// +/// [`generate`] writes this straight out. Take it here instead when you need +/// to set something clap cannot express — most often `effect=`, which says +/// whether a command reads, writes or destroys: +/// +/// ```no_run +/// # use clap::Command; +/// # use usage::SpecCommandEffect; +/// # let mut cmd = Command::new("mycli").subcommand(Command::new("rm")); +/// let mut spec = clap_usage::spec(&mut cmd, "mycli"); +/// if let Some(rm) = spec.cmd.subcommands.get_mut("rm") { +/// rm.effect = Some(SpecCommandEffect::Destructive); +/// } +/// println!("{spec}"); +/// ``` +pub fn spec>(cmd: &mut Command, bin_name: S) -> usage::Spec { let mut spec: usage::Spec = cmd.clone().into(); spec.bin = bin_name.into(); + spec +} + +/// Write the usage spec for a clap command, with the `@generated` header. +/// +/// Use [`spec`] instead if you need to modify the spec before writing it. +pub fn generate>(cmd: &mut Command, bin_name: S, buf: &mut dyn Write) { + let spec = spec(cmd, bin_name); writeln!(buf, "// @generated by usage-cli from clap metadata") .expect("write @generated comment"); diff --git a/clap_usage/src/lib.rs b/clap_usage/src/lib.rs index 1f05b7fb4..39380bbbf 100644 --- a/clap_usage/src/lib.rs +++ b/clap_usage/src/lib.rs @@ -1,3 +1,3 @@ mod generate; -pub use crate::generate::generate; +pub use crate::generate::{generate, spec}; diff --git a/clap_usage/tests/spec_accessor.rs b/clap_usage/tests/spec_accessor.rs new file mode 100644 index 000000000..fc6adad3f --- /dev/null +++ b/clap_usage/tests/spec_accessor.rs @@ -0,0 +1,33 @@ +use clap::Command; +use usage::SpecCommandEffect; + +/// The reason `spec` exists: `generate` writes straight to a writer, so there +/// is no way to set something clap cannot express, such as `effect=`. +#[test] +fn spec_can_be_annotated_before_rendering() { + let mut cmd = Command::new("mycli") + .subcommand(Command::new("ls").about("List things")) + .subcommand(Command::new("rm").about("Remove things")); + + let mut spec = clap_usage::spec(&mut cmd, "mycli"); + assert_eq!(spec.bin, "mycli"); + + spec.cmd.subcommands.get_mut("ls").unwrap().effect = Some(SpecCommandEffect::Read); + spec.cmd.subcommands.get_mut("rm").unwrap().effect = Some(SpecCommandEffect::Destructive); + + let rendered = spec.to_string(); + assert!(rendered.contains("cmd ls"), "{rendered}"); + assert!(rendered.contains("effect=read"), "{rendered}"); + assert!(rendered.contains("effect=destructive"), "{rendered}"); +} + +/// `generate` must keep producing exactly what it always did. +#[test] +fn generate_still_writes_the_header_and_spec() { + let mut cmd = Command::new("mycli").subcommand(Command::new("ls")); + let mut buf = vec![]; + clap_usage::generate(&mut cmd, "mycli", &mut buf); + let out = String::from_utf8(buf).unwrap(); + assert!(out.starts_with("// @generated by usage-cli from clap metadata\n")); + assert!(out.contains("bin mycli")); +} From 3d4004f640b81478226e7490eec9ac6b2f7cfb23 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:42:10 +0000 Subject: [PATCH 2/2] fix(clap_usage): re-export usage so the accessor is usable on its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `spec()` example set `effect` via `usage::SpecCommandEffect`, but clap_usage exported only `generate` and `spec`, so a consumer depending on clap_usage alone could not name that type. They would have had to keep the direct usage-lib dependency this accessor exists to remove — the example as written would not have compiled for them. Re-export the whole `usage` crate rather than a curated list of types, so nothing here goes stale as usage grows: use clap_usage::usage::SpecCommandEffect; The integration test now reaches the enum through `clap_usage::usage::`, which is the path an external consumer has, so it demonstrates the API is self-sufficient instead of quietly relying on a second dependency. Co-Authored-By: Claude Opus 5 --- clap_usage/src/generate.rs | 2 +- clap_usage/src/lib.rs | 19 +++++++++++++++++++ clap_usage/tests/spec_accessor.rs | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/clap_usage/src/generate.rs b/clap_usage/src/generate.rs index 2f61bbdf3..39a579773 100644 --- a/clap_usage/src/generate.rs +++ b/clap_usage/src/generate.rs @@ -9,7 +9,7 @@ use std::io::Write; /// /// ```no_run /// # use clap::Command; -/// # use usage::SpecCommandEffect; +/// # use clap_usage::usage::SpecCommandEffect; /// # let mut cmd = Command::new("mycli").subcommand(Command::new("rm")); /// let mut spec = clap_usage::spec(&mut cmd, "mycli"); /// if let Some(rm) = spec.cmd.subcommands.get_mut("rm") { diff --git a/clap_usage/src/lib.rs b/clap_usage/src/lib.rs index 39380bbbf..222c0319f 100644 --- a/clap_usage/src/lib.rs +++ b/clap_usage/src/lib.rs @@ -1,3 +1,22 @@ +//! Build [`usage`] specs from [`clap`] commands. +//! +//! [`generate`] writes a spec straight out; [`spec`] hands you the [`usage::Spec`] +//! first so you can set things clap cannot express. +//! +//! The `usage` crate is re-exported, so depending on `clap_usage` alone is +//! enough to name the spec types you get back: +//! +//! ```no_run +//! use clap_usage::usage::SpecCommandEffect; +//! ``` + mod generate; +/// The [`usage`] crate, re-exported. +/// +/// [`spec`] returns `usage` types, so consumers need to name them. Re-exporting +/// the whole crate means `clap_usage` on its own is a sufficient dependency, +/// and nothing here goes stale as `usage` grows. +pub use usage; + pub use crate::generate::{generate, spec}; diff --git a/clap_usage/tests/spec_accessor.rs b/clap_usage/tests/spec_accessor.rs index fc6adad3f..43d36c937 100644 --- a/clap_usage/tests/spec_accessor.rs +++ b/clap_usage/tests/spec_accessor.rs @@ -1,5 +1,7 @@ use clap::Command; -use usage::SpecCommandEffect; +// Deliberately reached through clap_usage, proving a consumer needs no +// direct usage-lib dependency to annotate the spec it gets back. +use clap_usage::usage::SpecCommandEffect; /// The reason `spec` exists: `generate` writes straight to a writer, so there /// is no way to set something clap cannot express, such as `effect=`.