mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
Implement manpage generation for goose-cli (#6980)
Signed-off-by: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
This commit is contained in:
parent
72601fd3cb
commit
58e86d58ed
6 changed files with 209 additions and 2 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -1833,6 +1833,16 @@ version = "0.7.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cliclack"
|
||||
version = "0.3.8"
|
||||
|
|
@ -4253,6 +4263,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"cliclack",
|
||||
"console 0.16.2",
|
||||
"dotenvy",
|
||||
|
|
@ -8027,6 +8038,12 @@ dependencies = [
|
|||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.12.0"
|
||||
|
|
|
|||
6
Justfile
6
Justfile
|
|
@ -196,6 +196,12 @@ generate-openapi:
|
|||
@echo "Generating frontend API..."
|
||||
cd ui/desktop && npx @hey-api/openapi-ts
|
||||
|
||||
# Generate manpages for the CLI
|
||||
generate-manpages:
|
||||
@echo "Generating manpages..."
|
||||
cargo run -p goose-cli --bin generate_manpages
|
||||
@echo "Manpages generated at target/man/"
|
||||
|
||||
# make GUI with latest binary
|
||||
lint-ui:
|
||||
cd ui/desktop && npm run lint:check
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ workspace = true
|
|||
name = "goose"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "generate_manpages"
|
||||
path = "src/bin/generate_manpages.rs"
|
||||
|
||||
[dependencies]
|
||||
clap_mangen = "0.2.31"
|
||||
goose = { path = "../goose" }
|
||||
goose-acp = { path = "../goose-acp" }
|
||||
goose-mcp = { path = "../goose-mcp" }
|
||||
|
|
@ -66,3 +71,4 @@ disable-update = []
|
|||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
tokio = { workspace = true }
|
||||
|
||||
|
|
|
|||
177
crates/goose-cli/src/bin/generate_manpages.rs
Normal file
177
crates/goose-cli/src/bin/generate_manpages.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
//! Generate manpages for the goose CLI.
|
||||
//!
|
||||
//! This binary generates ROFF-formatted manpages from the clap CLI definitions.
|
||||
//! Manpages are an essential part of the Linux/Unix ecosystem, providing users with
|
||||
//! offline documentation accessible via the `man` command (e.g., `man goose`).
|
||||
//!
|
||||
//! When goose is packaged for Linux distributions (deb, rpm, etc.), the generated
|
||||
//! manpages should be installed to `/usr/share/man/man1/` so users can access help
|
||||
//! without an internet connection, following Unix conventions that have existed
|
||||
//! since the 1970s.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run -p goose-cli --bin generate_manpages
|
||||
//! # or
|
||||
//! just generate-manpages
|
||||
//!
|
||||
//! Output: target/man/goose.1, target/man/goose-session.1, etc.
|
||||
|
||||
use clap::CommandFactory;
|
||||
use clap_mangen::Man;
|
||||
use goose_cli::Cli;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Manpages are a Unix/Linux convention - skip generation on Windows
|
||||
if cfg!(target_os = "windows") {
|
||||
eprintln!("Skipping manpage generation on Windows (manpages are a Unix/Linux convention)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let output_dir = PathBuf::from(package_dir)
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("target")
|
||||
.join("man");
|
||||
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let cmd = Cli::command();
|
||||
|
||||
// First pass: collect all command names for SEE ALSO sections
|
||||
let mut all_commands: Vec<String> = Vec::new();
|
||||
collect_command_names(&cmd, &mut all_commands, None);
|
||||
|
||||
// Second pass: generate manpages with SEE ALSO sections
|
||||
generate_manpages(&cmd, &output_dir, None, &all_commands)?;
|
||||
|
||||
let canonical_path = output_dir.canonicalize()?;
|
||||
eprintln!(
|
||||
"Successfully generated manpages at {}",
|
||||
canonical_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_command_names(cmd: &clap::Command, names: &mut Vec<String>, parent_name: Option<&str>) {
|
||||
let name = match parent_name {
|
||||
Some(parent) => format!("{}-{}", parent, cmd.get_name()),
|
||||
None => cmd.get_name().to_string(),
|
||||
};
|
||||
names.push(name.clone());
|
||||
|
||||
for subcmd in cmd.get_subcommands() {
|
||||
if subcmd.get_name() == "help" || subcmd.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
collect_command_names(subcmd, names, Some(&name));
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_manpages(
|
||||
cmd: &clap::Command,
|
||||
dir: &PathBuf,
|
||||
parent_name: Option<&str>,
|
||||
all_commands: &[String],
|
||||
) -> Result<()> {
|
||||
let name = match parent_name {
|
||||
Some(parent) => format!("{}-{}", parent, cmd.get_name()),
|
||||
None => cmd.get_name().to_string(),
|
||||
};
|
||||
|
||||
// Generate the base manpage
|
||||
let man = Man::new(cmd.clone());
|
||||
let mut buffer = Vec::new();
|
||||
man.render(&mut buffer)?;
|
||||
|
||||
// Add SEE ALSO section
|
||||
let see_also = generate_see_also(&name, parent_name, cmd, all_commands);
|
||||
buffer.extend_from_slice(see_also.as_bytes());
|
||||
|
||||
let manpage_path = dir.join(format!("{}.1", name));
|
||||
fs::write(&manpage_path, buffer)?;
|
||||
eprintln!(" Generated: {}.1", name);
|
||||
|
||||
for subcmd in cmd.get_subcommands() {
|
||||
if subcmd.get_name() == "help" || subcmd.is_hide_set() {
|
||||
continue;
|
||||
}
|
||||
generate_manpages(subcmd, dir, Some(&name), all_commands)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_see_also(
|
||||
current_name: &str,
|
||||
parent_name: Option<&str>,
|
||||
cmd: &clap::Command,
|
||||
all_commands: &[String],
|
||||
) -> String {
|
||||
let mut references: Vec<String> = Vec::new();
|
||||
|
||||
// Always reference the main goose command if we're not it
|
||||
if current_name != "goose" {
|
||||
references.push("goose".to_string());
|
||||
}
|
||||
|
||||
// Reference parent command if exists and not already added
|
||||
if let Some(parent) = parent_name {
|
||||
if parent != "goose" && !references.contains(&parent.to_string()) {
|
||||
references.push(parent.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// For the main command, list immediate subcommands
|
||||
// For subcommands, list sibling commands
|
||||
if current_name == "goose" {
|
||||
// Add all immediate subcommands (skip hidden ones)
|
||||
for subcmd in cmd.get_subcommands() {
|
||||
let subcmd_name = subcmd.get_name();
|
||||
if subcmd_name != "help" && !subcmd.is_hide_set() {
|
||||
let full_name = format!("goose-{}", subcmd_name);
|
||||
if !references.contains(&full_name) {
|
||||
references.push(full_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(parent) = parent_name {
|
||||
// Add sibling commands (other commands with same parent)
|
||||
let prefix = format!("{}-", parent);
|
||||
for cmd_name in all_commands {
|
||||
if cmd_name.starts_with(&prefix) && cmd_name != current_name {
|
||||
// Only add immediate siblings, not nested subcommands
|
||||
let suffix = &cmd_name.strip_prefix(&prefix).unwrap_or(cmd_name);
|
||||
if !suffix.contains('-') && !references.contains(cmd_name) {
|
||||
references.push(cmd_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort references for consistent output
|
||||
references.sort();
|
||||
|
||||
if references.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Format as ROFF
|
||||
let mut roff = String::from("\n.SH \"SEE ALSO\"\n");
|
||||
let formatted_refs: Vec<String> = references
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let escaped = r.replace('-', "\\-");
|
||||
format!(".BR {} (1)", escaped)
|
||||
})
|
||||
.collect();
|
||||
roff.push_str(&formatted_refs.join(",\n"));
|
||||
roff.push('\n');
|
||||
|
||||
roff
|
||||
}
|
||||
|
|
@ -35,8 +35,8 @@ use std::path::PathBuf;
|
|||
use tracing::warn;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, display_name = "", about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(name = "goose", author, version, display_name = "", about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ pub mod session;
|
|||
pub mod signal;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use cli::Cli;
|
||||
pub use session::CliSession;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue