diff --git a/Cargo.lock b/Cargo.lock index 2b12bd3e..315433a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clap_mangen" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -1222,6 +1232,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "roff" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -1801,6 +1817,7 @@ dependencies = [ "camino", "clap", "clap_complete", + "clap_mangen", "criterion", "insta", "miette", diff --git a/Cargo.toml b/Cargo.toml index 20066426..fd67819c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ insta = { version = "1", features = ["yaml", "json"] } notify = "6" schemars = { version = "0.8", features = ["preserve_order"] } rayon = "1.10" +clap_mangen = "0.2" [workspace.lints.rust] unsafe_code = "deny" diff --git a/README.md b/README.md index 1b859ae8..79b7eb80 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,7 @@ weaveffi schema-version # prints 0.7.0 | `weaveffi schema-version` | Print the current IR schema version (`0.7.0`) | | `weaveffi doctor` | Check for required toolchains; `--target swift` to scope to one language, `--format json` for CI | | `weaveffi completions ` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) | +| `weaveffi man --out ` | Generate roff man pages for the CLI to the specified directory | Reference the JSON Schema from your IDL for editor autocompletion: diff --git a/crates/weaveffi-cli/Cargo.toml b/crates/weaveffi-cli/Cargo.toml index 0b3a88b1..4b49a16f 100644 --- a/crates/weaveffi-cli/Cargo.toml +++ b/crates/weaveffi-cli/Cargo.toml @@ -45,6 +45,7 @@ similar = { workspace = true } tempfile = { workspace = true } notify = { workspace = true } schemars = { workspace = true } +clap_mangen = { workspace = true } [dev-dependencies] assert_cmd = "2" diff --git a/crates/weaveffi-cli/src/main.rs b/crates/weaveffi-cli/src/main.rs index 07bdc04e..5e1a84ca 100644 --- a/crates/weaveffi-cli/src/main.rs +++ b/crates/weaveffi-cli/src/main.rs @@ -140,6 +140,11 @@ enum Commands { /// Shell to generate completions for shell: clap_complete::Shell, }, + Man { + /// Output directory for generated man pages + #[arg(long)] + out: String, + }, SchemaVersion, Watch { /// Input IDL/IR file (yaml|yml|json|toml) @@ -264,6 +269,7 @@ fn main() -> Result<()> { doctor::cmd_doctor(target.as_deref(), format.as_deref())? } Commands::Completions { shell } => cmd_completions(shell), + Commands::Man { out } => cmd_man(&out, quiet)?, Commands::SchemaVersion => println!("{CURRENT_SCHEMA_VERSION}"), Commands::Watch { input, @@ -286,6 +292,23 @@ fn cmd_completions(shell: clap_complete::Shell) { ); } +fn cmd_man(out: &str, quiet: bool) -> Result<()> { + let out_dir = std::path::PathBuf::from(out); + std::fs::create_dir_all(&out_dir) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create directory: {out}"))?; + + clap_mangen::generate_to(Cli::command(), &out_dir) + .into_diagnostic() + .wrap_err("failed to generate man pages")?; + + if !quiet { + println!("Man pages written to {}", out_dir.display()); + } + + Ok(()) +} + fn cmd_schema(format: &str) -> Result<()> { match format { "json-schema" => { @@ -399,4 +422,60 @@ mod tests { assert!(cmd.status.success(), "schema-version failed: {stdout}"); assert_eq!(stdout.trim(), CURRENT_SCHEMA_VERSION); } + + #[test] + fn man_generation() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + let out_dir = temp_dir.path().join("man"); + + let cmd = assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args(["man", "--out", out_dir.to_str().unwrap()]) + .output() + .expect("failed to run weaveffi man"); + + let stdout = String::from_utf8_lossy(&cmd.stdout); + assert!(cmd.status.success(), "man generation failed: {stdout}"); + assert!( + stdout.contains("Man pages written to"), + "should print where pages were written (not quiet): {stdout}" + ); + + // Verify top-level + let main_page = out_dir.join("weaveffi.1"); + assert!(main_page.exists(), "weaveffi.1 not generated"); + assert!( + std::fs::metadata(&main_page).unwrap().len() > 0, + "weaveffi.1 is empty" + ); + + // Verify subcommands exist. At minimum we know `generate` and `doctor` exist. + let generate_page = out_dir.join("weaveffi-generate.1"); + assert!(generate_page.exists(), "weaveffi-generate.1 not generated"); + assert!( + std::fs::metadata(&generate_page).unwrap().len() > 0, + "weaveffi-generate.1 is empty" + ); + + let doctor_page = out_dir.join("weaveffi-doctor.1"); + assert!(doctor_page.exists(), "weaveffi-doctor.1 not generated"); + assert!( + std::fs::metadata(&doctor_page).unwrap().len() > 0, + "weaveffi-doctor.1 is empty" + ); + + // Verify quiet respects global flag + let quiet_cmd = assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args(["--quiet", "man", "--out", out_dir.to_str().unwrap()]) + .output() + .expect("failed to run weaveffi --quiet man"); + + let quiet_stdout = String::from_utf8_lossy(&quiet_cmd.stdout); + assert!(quiet_cmd.status.success()); + assert!( + !quiet_stdout.contains("Man pages written to"), + "quiet mode should suppress output" + ); + } } diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 934f5a74..50695db5 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -325,3 +325,15 @@ weaveffi doctor --target ruby --format json | jq '.[] | select(.ok == false)' ``` Each entry has `id`, `name`, `ok`, `version`, `hint`, and `applies_to` fields. + +## Generating Man Pages + +The CLI can generate its own man pages. Pass an output directory to `--out`: + +```bash +mkdir -p ./man +weaveffi man --out ./man +man ./man/weaveffi.1 +``` + +This writes `weaveffi.1` as well as one man page per subcommand (e.g., `weaveffi-generate.1`) into the specified directory.