1use 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
18pub struct ManPageGenerator;
20
21impl ManPageGenerator {
22 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 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 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 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 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 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 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 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}