Skip to main content

vtcode_core/cli/
man_pages.rs

1//! Man page generation for VT Code CLI.
2//!
3//! Structural sections (NAME, SYNOPSIS, DESCRIPTION, OPTIONS, SUBCOMMANDS,
4//! VERSION) are generated from the clap [`Cli`] command tree via
5//! [`clap_mangen`], so they cannot drift from `--help` output. VT Code
6//! specific sections (ENVIRONMENT, FILES, SAFETY, EXAMPLES) are appended on
7//! top with [`roff`].
8
9use std::io::Write;
10
11use anyhow::{Context, Result, bail};
12use clap::CommandFactory;
13use clap_mangen::Man;
14use roff::{Roff, bold, roman};
15
16use crate::cli::args::{Cli, long_version};
17
18/// Man page generator for VT Code CLI
19pub struct ManPageGenerator;
20
21impl ManPageGenerator {
22    /// Generate man page for the main VT Code command
23    pub fn generate_main_man_page() -> Result<String> {
24        let mut cmd = Cli::command();
25        cmd = cmd.name("vtcode");
26        if cmd.get_about().is_none() && cmd.get_long_about().is_none() {
27            cmd = cmd.about("Advanced coding agent with Decision Ledger");
28        }
29        if cmd.get_version().is_none() {
30            let version: &'static str = Box::leak(long_version().into_boxed_str());
31            cmd = cmd.version(version);
32        }
33        Self::render_page(cmd, None, &|buf| Self::append_main_sections(buf))
34    }
35
36    /// Generate man page for a specific command
37    pub fn generate_command_man_page(command: &str) -> Result<String> {
38        let mut cmd = Cli::command();
39        cmd.build();
40        let Some(sub) = cmd.find_subcommand(command).filter(|sub| !sub.is_hide_set()).cloned() else {
41            bail!("Unknown command: {command}");
42        };
43        let name = sub.get_name().to_owned();
44        let examples: &[&str] = match name.as_str() {
45            "ask" => &[
46                "vtcode ask \"what is a monad?\"",
47                "echo \"summarize this\" | vtcode ask",
48                "vtcode ask --output-format json \"explain ownership in Rust\"",
49            ],
50            "benchmark" => &["vtcode benchmark"],
51            "check" => &["vtcode check ast-grep"],
52            "chat" => &["vtcode chat"],
53            "create-project" => &[
54                "vtcode create-project myapp --feature web --feature auth",
55                "vtcode create-project simple_app",
56            ],
57            "init" => &["vtcode init", "vtcode init --force"],
58            "man" => &["vtcode man", "vtcode man chat", "vtcode man chat --output chat.1"],
59            _ => &[],
60        };
61        let title = format!("vtcode-{name}");
62        let sub = sub.display_name(&title);
63        Self::render_page(sub, Some(title), &|buf| Self::append_command_sections(buf, examples))
64    }
65
66    /// Generate the main man page plus one page per visible subcommand into
67    /// `dir` (`vtcode.1`, `vtcode-<command>.1`, ...). Returns the number of
68    /// pages written.
69    pub fn generate_all_man_pages(dir: &std::path::Path) -> Result<usize> {
70        std::fs::create_dir_all(dir)
71            .with_context(|| format!("failed to create man page directory {}", dir.display()))?;
72
73        let mut cmd = Cli::command();
74        cmd.build();
75        let main_page = Self::generate_main_man_page()?;
76        let mut count = Self::write_page(dir, "vtcode", &main_page)?;
77        for sub in cmd.get_subcommands().filter(|sub| !sub.is_hide_set()) {
78            let name = sub.get_name();
79            count += Self::write_page(dir, &format!("vtcode-{name}"), &Self::generate_command_man_page(name)?)?;
80        }
81        Ok(count)
82    }
83
84    /// Write one man page to `dir/<name>.1`, returning 1 on success.
85    fn write_page(dir: &std::path::Path, name: &str, content: &str) -> Result<usize> {
86        let path = dir.join(format!("{name}.1"));
87        std::fs::write(&path, content).with_context(|| format!("failed to write man page {}", path.display()))?;
88        Ok(1)
89    }
90
91    /// Render a clap-derived man page and append VT Code-specific sections.
92    fn render_page(
93        cmd: clap::Command,
94        title: Option<String>,
95        append: &dyn Fn(&mut Vec<u8>) -> Result<()>,
96    ) -> Result<String> {
97        let mut man = Man::new(cmd);
98        if let Some(title) = title {
99            man = man.title(title.to_uppercase());
100        }
101        let man = man
102            .date(chrono::Utc::now().format("%Y-%m-%d").to_string())
103            .manual("VT Code")
104            .source("VT Code");
105
106        let mut buf = Vec::new();
107        man.render(&mut buf).context("failed to render clap-derived man page")?;
108        append(&mut buf)?;
109        String::from_utf8(buf).context("man page output is not valid UTF-8")
110    }
111
112    /// Append VT Code-specific sections to the main man page.
113    fn append_main_sections(buf: &mut Vec<u8>) -> Result<()> {
114        let mut roff = Roff::new();
115        roff.control("SH", ["ENVIRONMENT"]);
116        for (name, description) in [
117            ("GEMINI_API_KEY", "API key for Google Gemini (default provider)"),
118            ("OPENAI_API_KEY", "API key for OpenAI GPT models"),
119            ("ANTHROPIC_API_KEY", "API key for Anthropic Claude models"),
120            ("DEEPSEEK_API_KEY", "API key for DeepSeek models"),
121            ("META_API_KEY", "API key for Meta AI Muse models"),
122            ("MODEL_API_KEY", "Meta AI's documented API key variable"),
123            ("ZAI_API_KEY", "API key for Z.AI GLM models"),
124            ("MOONSHOT_API_KEY", "API key for Moonshot AI Kimi models"),
125            ("OPENROUTER_API_KEY", "API key for OpenRouter models"),
126            ("NVIDIA_API_KEY", "API key for NVIDIA NIM models"),
127            ("MERGE_GATEWAY_API_KEY", "API key for Merge Gateway routes"),
128            (
129                "MERGE_GATEWAY_BASE_URL",
130                "Optional Merge Gateway endpoint override; /v1/openai selects legacy compatibility",
131            ),
132            ("AI_GATEWAY_API_KEY", "API key for Vercel AI Gateway models"),
133            (
134                "VERCEL_AI_GATEWAY_BASE_URL",
135                "Optional Vercel AI Gateway endpoint override (default: https://ai-gateway.vercel.sh/v1)",
136            ),
137        ] {
138            roff.control("TP", []).text([bold(name)]).text([roman(description)]);
139        }
140
141        roff.control("SH", ["FILES"]);
142        roff.control("TP", []).text([bold("vtcode.toml")]).text([roman(
143            "Configuration file (current directory or the canonical user config directory)",
144        )]);
145        roff.control("TP", [])
146            .text([bold(".vtcode/")])
147            .text([roman("Project cache and context directory")]);
148
149        roff.control("SH", ["SAFETY"]);
150        roff.control("TP", []).text([roman(
151            "apply_patch: reserve for reviewed diffs or small batches. For large refactors or critical files, stage local backups and prefer edit_file/write_file to avoid partial rewrites if a patch fails.",
152        )]);
153        roff.control("TP", []).text([roman(
154            "Timeout governance: tune [timeouts] in vtcode.toml to clamp tool duration. VT Code warns once execution passes the configured warning threshold so you can cancel runaway commands.",
155        )]);
156
157        roff.control("SH", ["EXAMPLES"]);
158        for (label, example) in [
159            ("Start interactive chat:", "vtcode chat"),
160            ("Ask a question:", "vtcode ask \"Explain Rust ownership\""),
161            ("Create a web project:", "vtcode create-project myapp --feature web --feature auth"),
162            ("Generate man page:", "vtcode man chat"),
163            ("Run ast-grep checks for the current workspace:", "vtcode check ast-grep"),
164        ] {
165            roff.text([roman(label)]);
166            roff.text([bold(format!("  {example}"))]);
167        }
168
169        roff.control("SH", ["SEE ALSO"]);
170        roff.text([roman("Full documentation: https://github.com/vinhnx/vtcode")]);
171        roff.text([roman("Related commands: cargo(1), rustc(1), git(1)")]);
172
173        Self::append_roff(buf, &roff)
174    }
175
176    /// Append curated examples and a SEE ALSO section to a command man page.
177    fn append_command_sections(buf: &mut Vec<u8>, examples: &[&str]) -> Result<()> {
178        let mut roff = Roff::new();
179        if !examples.is_empty() {
180            roff.control("SH", ["EXAMPLES"]);
181            for example in examples {
182                roff.text([bold(format!("  {example}"))]);
183            }
184        }
185        roff.control("SH", ["SEE ALSO"]);
186        roff.text([bold("vtcode(1)")]);
187        Self::append_roff(buf, &roff)
188    }
189
190    /// Append rendered roff content, stripping the duplicate preamble that
191    /// [`Roff::render`] emits (clap_mangen already wrote one).
192    fn append_roff(buf: &mut Vec<u8>, roff: &Roff) -> Result<()> {
193        let rendered = roff.render();
194        let body = match rendered.find(".SH ") {
195            Some(index) => &rendered[index..],
196            None => return Ok(()),
197        };
198        buf.write_all(body.as_bytes()).context("failed to append man page sections")?;
199        Ok(())
200    }
201}