Implement manpage generation for goose-cli (#6980)
Signed-off-by: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
This commit is contained in:
Generated
+17
@@ -1833,6 +1833,16 @@ version = "0.7.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
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]]
|
[[package]]
|
||||||
name = "cliclack"
|
name = "cliclack"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@@ -4253,6 +4263,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
"clap_mangen",
|
||||||
"cliclack",
|
"cliclack",
|
||||||
"console 0.16.2",
|
"console 0.16.2",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@@ -8027,6 +8038,12 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roff"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ generate-openapi:
|
|||||||
@echo "Generating frontend API..."
|
@echo "Generating frontend API..."
|
||||||
cd ui/desktop && npx @hey-api/openapi-ts
|
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
|
# make GUI with latest binary
|
||||||
lint-ui:
|
lint-ui:
|
||||||
cd ui/desktop && npm run lint:check
|
cd ui/desktop && npm run lint:check
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ workspace = true
|
|||||||
name = "goose"
|
name = "goose"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "generate_manpages"
|
||||||
|
path = "src/bin/generate_manpages.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap_mangen = "0.2.31"
|
||||||
goose = { path = "../goose" }
|
goose = { path = "../goose" }
|
||||||
goose-acp = { path = "../goose-acp" }
|
goose-acp = { path = "../goose-acp" }
|
||||||
goose-mcp = { path = "../goose-mcp" }
|
goose-mcp = { path = "../goose-mcp" }
|
||||||
@@ -66,3 +71,4 @@ disable-update = []
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
use tracing::warn;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, display_name = "", about, long_about = None)]
|
#[command(name = "goose", author, version, display_name = "", about, long_about = None)]
|
||||||
struct Cli {
|
pub struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ pub mod session;
|
|||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
|
pub use cli::Cli;
|
||||||
pub use session::CliSession;
|
pub use session::CliSession;
|
||||||
|
|||||||
Reference in New Issue
Block a user