Skip to main content

omni_dev/cli/
help.rs

1//! Help command implementation for comprehensive CLI documentation.
2
3use anyhow::Result;
4use clap::{builder::StyledStr, Command, CommandFactory, Parser};
5
6/// Help command for displaying comprehensive usage information.
7#[derive(Parser)]
8pub struct HelpCommand {
9    // No subcommands needed - this command shows all help
10}
11
12/// Help generator for creating comprehensive CLI documentation.
13pub struct HelpGenerator {
14    app: Command,
15}
16
17impl HelpGenerator {
18    /// Creates a new help generator with the current CLI app.
19    pub fn new() -> Self {
20        use crate::cli::Cli;
21
22        // Build the clap app to get the command structure
23        let app = Cli::command();
24
25        Self { app }
26    }
27}
28
29impl Default for HelpGenerator {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl HelpGenerator {
36    /// Generates comprehensive help for all commands.
37    pub fn generate_all_help(&self) -> Result<String> {
38        let mut help_sections = Vec::new();
39
40        // Add main app help
41        let main_help = self.render_command_help(&self.app, "");
42        help_sections.push(main_help);
43
44        // Collect help for all subcommands recursively
45        self.collect_help_recursive(&self.app, "", &mut help_sections);
46
47        // Join all sections with separators
48        let separator = format!("\n\n{}\n\n", "=".repeat(80));
49        Ok(help_sections.join(&separator))
50    }
51
52    /// Recursively collects help for all subcommands.
53    ///
54    /// IMPORTANT: Commands are sorted lexicographically to ensure consistent,
55    /// predictable output order. This is critical for:
56    /// - User experience (predictable command discovery)
57    /// - Golden/snapshot tests (deterministic output)
58    /// - Documentation generation (stable ordering)
59    ///
60    /// When adding new commands, ensure this sorting is preserved.
61    fn collect_help_recursive(&self, cmd: &Command, prefix: &str, help_sections: &mut Vec<String>) {
62        // Collect all subcommands and sort them lexicographically by name
63        let mut subcommands: Vec<_> = cmd.get_subcommands().collect();
64        subcommands.sort_by(|a, b| a.get_name().cmp(b.get_name()));
65
66        for subcmd in subcommands {
67            // Skip the help command itself to avoid infinite recursion
68            if subcmd.get_name() == "help" {
69                continue;
70            }
71
72            let current_path = if prefix.is_empty() {
73                subcmd.get_name().to_string()
74            } else {
75                format!("{} {}", prefix, subcmd.get_name())
76            };
77
78            // Render help for this subcommand
79            let subcmd_help = self.render_command_help(subcmd, &current_path);
80            help_sections.push(subcmd_help);
81
82            // Recursively collect help for nested subcommands (also sorted)
83            self.collect_help_recursive(subcmd, &current_path, help_sections);
84        }
85    }
86
87    /// Renders help for a specific command.
88    fn render_command_help(&self, cmd: &Command, path: &str) -> String {
89        let mut output = String::new();
90
91        // Command header
92        let cmd_name = if path.is_empty() {
93            cmd.get_name().to_string()
94        } else {
95            format!("omni-dev {path}")
96        };
97
98        let about = cmd.get_about().map_or_else(
99            || "No description available".to_string(),
100            |s| self.styled_str_to_string(s),
101        );
102
103        output.push_str(&format!("{cmd_name} - {about}\n\n"));
104
105        // Render the actual help content
106        let help_str = cmd.clone().render_help();
107        output.push_str(&help_str.to_string());
108
109        output
110    }
111
112    /// Converts a `StyledStr` to a regular `String` (removes ANSI codes for plain text).
113    fn styled_str_to_string(&self, styled: &StyledStr) -> String {
114        styled.to_string()
115    }
116}
117
118impl HelpCommand {
119    /// Executes the help command, showing comprehensive help for all commands.
120    pub fn execute(self) -> Result<()> {
121        let generator = HelpGenerator::new();
122        let help_output = generator.generate_all_help()?;
123        println!("{help_output}");
124        Ok(())
125    }
126}
127
128#[cfg(test)]
129#[allow(clippy::unwrap_used, clippy::expect_used)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn help_generator_default() {
135        let gen = HelpGenerator::default();
136        assert_eq!(gen.app.get_name(), "omni-dev");
137    }
138
139    #[test]
140    fn generate_all_help_contains_all_top_level_commands() {
141        let gen = HelpGenerator::new();
142        let output = gen.generate_all_help().unwrap();
143        assert!(output.contains("omni-dev ai"));
144        assert!(output.contains("omni-dev git"));
145        assert!(output.contains("omni-dev commands"));
146        assert!(output.contains("omni-dev config"));
147        assert!(output.contains("omni-dev help-all"));
148    }
149
150    #[test]
151    fn generate_all_help_contains_nested_commands() {
152        let gen = HelpGenerator::new();
153        let output = gen.generate_all_help().unwrap();
154        // Deeply nested commands should be present
155        assert!(output.contains("omni-dev git commit message view"));
156        assert!(output.contains("omni-dev git commit message amend"));
157        assert!(output.contains("omni-dev git commit message twiddle"));
158        assert!(output.contains("omni-dev git commit message check"));
159        assert!(output.contains("omni-dev git branch info"));
160        assert!(output.contains("omni-dev git branch create pr"));
161    }
162
163    #[test]
164    fn generate_all_help_uses_section_separators() {
165        let gen = HelpGenerator::new();
166        let output = gen.generate_all_help().unwrap();
167        let separator = "=".repeat(80);
168        assert!(output.contains(&separator));
169    }
170
171    #[test]
172    fn generate_all_help_is_deterministic() {
173        let gen1 = HelpGenerator::new();
174        let gen2 = HelpGenerator::new();
175        let output1 = gen1.generate_all_help().unwrap();
176        let output2 = gen2.generate_all_help().unwrap();
177        assert_eq!(output1, output2, "Help output should be deterministic");
178    }
179
180    #[test]
181    fn render_command_help_includes_about() {
182        let gen = HelpGenerator::new();
183        let help = gen.render_command_help(&gen.app, "");
184        // The main app help should include the about text
185        assert!(help.contains("comprehensive development toolkit"));
186    }
187
188    #[test]
189    fn styled_str_to_string_plain_text() {
190        let gen = HelpGenerator::new();
191        let styled = StyledStr::from("hello world");
192        let result = gen.styled_str_to_string(&styled);
193        assert_eq!(result, "hello world");
194    }
195}