Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <shell>` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) |
| `weaveffi man --out <dir>` | Generate roff man pages for the CLI to the specified directory |

Reference the JSON Schema from your IDL for editor autocompletion:

Expand Down
1 change: 1 addition & 0 deletions crates/weaveffi-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
79 changes: 79 additions & 0 deletions crates/weaveffi-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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(())
}
Comment on lines +295 to +310

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clap_mangen ships a helper that does all of this for you: clap_mangen::generate_to renders the top-level page plus one page per subcommand, names them weaveffi-<sub>.1 via clap's display names, skips the help subcommand, and recurses into nested subcommands if we ever add any.

That also gets rid of the .leak() workaround, which permanently leaks each formatted name (clap's Command::name needs a 'static string when the string feature is off, which is why you needed it, but we'd rather not have that pattern in the codebase).

Your existing man_generation test passes unchanged with this version:

Suggested change
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}"))?;
let mut cmd = Cli::command();
cmd.build();
let mut buffer = Vec::new();
clap_mangen::Man::new(cmd.clone())
.render(&mut buffer)
.into_diagnostic()
.wrap_err("failed to render man page for weaveffi")?;
std::fs::write(out_dir.join("weaveffi.1"), buffer)
.into_diagnostic()
.wrap_err("failed to write weaveffi.1")?;
for sub in cmd.get_subcommands() {
let sub_name = sub.get_name();
if sub_name == "help" {
continue;
}
let sub_cmd = sub.clone().name(&*format!("weaveffi-{sub_name}").leak());
let mut sub_buffer = Vec::new();
clap_mangen::Man::new(sub_cmd)
.render(&mut sub_buffer)
.into_diagnostic()
.wrap_err_with(|| format!("failed to render man page for weaveffi-{sub_name}"))?;
std::fs::write(out_dir.join(format!("weaveffi-{sub_name}.1")), sub_buffer)
.into_diagnostic()
.wrap_err_with(|| format!("failed to write weaveffi-{sub_name}.1"))?;
}
if !quiet {
println!("Man pages written to {}", out_dir.display());
}
Ok(())
}
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" => {
Expand Down Expand Up @@ -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"
);
}
}
12 changes: 12 additions & 0 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading