Skip to main content

xbp_cli/cli/
help_render.rs

1//! Styled help rendering for Clap-generated usage text.
2
3use clap::CommandFactory;
4use colored::Colorize;
5
6/// Shared help layout for subcommands (no duplicate name/version header).
7pub const XBP_HELP_TEMPLATE: &str = "\
8{about-with-newline}\
9Usage: {usage}\n\n\
10{all-args}\
11{after-help}";
12
13/// Root help layout; the renderer adds a branded banner above Clap output.
14pub const XBP_ROOT_HELP_TEMPLATE: &str = "\
15{about-with-newline}\
16Usage: {usage}\n\n\
17{all-args}\
18{after-help}";
19
20pub const XBP_ROOT_AFTER_HELP: &str = "\
21Quick start:
22  xbp diag
23  xbp services
24  xbp workers list
25  xbp workers logs -f
26  xbp api health
27
28Discover:
29  xbp <command> -h          Command help and examples
30  xbp <command> <sub> -h    Subcommand help
31  xbp --commands            Full alphabetical command tree
32  xbp install               Browse installable targets";
33
34pub const CONFIG_AFTER_HELP: &str = "\
35Examples:
36  xbp config
37  xbp config --project
38  xbp config cloudflare
39  xbp config cloudflare status
40  xbp config openapi
41  xbp config openapi show
42  xbp config openrouter set-key
43  xbp config linear select-initiative";
44
45pub const VERSION_AFTER_HELP: &str = "\
46Examples:
47  xbp version
48  xbp version patch
49  xbp version 1.2.3
50  xbp version release
51  xbp version workspace check
52  xbp version workspace sync --version 3.16.5 --write";
53
54pub const DIAG_AFTER_HELP: &str = "\
55Examples:
56  xbp diag
57  xbp diag --nginx
58  xbp diag --ports 80,443
59  xbp diag --codetime --cursor";
60
61pub const NGINX_AFTER_HELP: &str = "\
62Examples:
63  xbp nginx list
64  xbp nginx enable api.example.com
65  xbp nginx disable api.example.com
66  xbp nginx upstream list";
67
68pub const COMMIT_AFTER_HELP: &str = "\
69Examples:
70  xbp commit
71  xbp commit --dry-run
72  xbp commit --push
73  xbp commit --scope cli";
74
75pub const LOGS_AFTER_HELP: &str = "\
76Examples:
77  xbp logs
78  xbp logs my-project
79  xbp logs --ssh-host bastion.example.com";
80
81pub const PUBLISH_AFTER_HELP: &str = "\
82Examples:
83  xbp publish --dry-run
84  xbp publish --target npm
85  xbp publish --service web
86  xbp publish --service api --include-prereqs
87  xbp publish --allow-dirty";
88
89pub const DOMAINS_AFTER_HELP: &str = "\
90Examples:
91  xbp domains list
92  xbp domains check --domain example.com
93  xbp domains search --query myapp --extension com";
94
95pub const LOGIN_AFTER_HELP: &str = "\
96Examples:
97  xbp login
98  xbp login status
99  xbp login logout
100  xbp whoami";
101
102pub const SSH_AFTER_HELP: &str = "\
103Examples:
104  xbp ssh
105  xbp ssh --host bastion.example.com
106  xbp ssh --host 10.0.0.5 --command \"uptime\"";
107
108pub const WORKTREE_WATCH_AFTER_HELP: &str = "\
109Examples:
110  xbp worktree-watch start
111  xbp worktree-watch start --detach
112  xbp worktree-watch start --parent C:\\Users\\floris\\Documents\\GitHub --detach
113  xbp worktree-watch stop
114  xbp worktree-watch sync
115  xbp worktree-watch sync --dry-run
116  xbp worktree-watch status
117  xbp worktree-watch status --stats
118  xbp worktree-watch status --stats --repo-activity
119  xbp worktree-watch status --stats --json
120  xbp worktree-watch status --records --record-limit 100
121  xbp worktree-watch status --stats --stats-gap-minutes 30
122
123Local stats are written under ~/.xbp/mutations/<owner>/<repo>/<branch>/stats.json
124and repo rollups to ~/.xbp/mutations/<owner>/<repo>/repo-activity.json.";
125
126pub const WORKTREE_WATCH_STATUS_AFTER_HELP: &str = "\
127Examples:
128  xbp worktree-watch status
129  xbp worktree-watch status --stats
130  xbp worktree-watch status --stats --repo-activity
131  xbp worktree-watch status --stats --json
132  xbp worktree-watch status --records --record-limit 100
133  xbp worktree-watch status --stats --stats-gap-minutes 30
134  xbp worktree-watch status --parent C:\\Users\\floris\\Documents\\GitHub --stats
135
136--stats recomputes activity from local JSONL spools and writes stats.json.
137--repo-activity rolls up every spooled branch into repo-activity.json.";
138
139#[cfg(feature = "openapi-gen")]
140pub const GENERATE_AFTER_HELP: &str = "\
141Examples:
142  xbp generate config
143  xbp generate config --force
144  xbp generate openapi --all
145  xbp generate openapi --service api --format both
146  xbp generate openapi --all --check
147  xbp generate systemd";
148
149#[cfg(not(feature = "openapi-gen"))]
150pub const GENERATE_AFTER_HELP: &str = "\
151Examples:
152  xbp generate config
153  xbp generate config --force
154  xbp generate systemd";
155
156pub const DONE_AFTER_HELP: &str = "\
157Examples:
158  xbp done
159  xbp done --since \"7 days ago\"
160  xbp done --output report.md";
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum HelpScope {
164    Root,
165    Subcommand,
166    Catalog,
167    Auto,
168}
169
170pub fn emit_styled_help(raw: &str, scope: HelpScope) {
171    crate::cli::ui::configure_color_output();
172    let resolved_scope = match scope {
173        HelpScope::Auto => detect_help_scope(raw),
174        other => other,
175    };
176
177    let mut skip_about_line = None::<String>;
178    let mut skip_until_usage = resolved_scope == HelpScope::Catalog;
179    if resolved_scope == HelpScope::Root {
180        print_root_banner(raw);
181    } else if resolved_scope != HelpScope::Catalog {
182        if let Some((_, tagline)) = parse_subcommand_heading(raw) {
183            skip_about_line = Some(tagline);
184            print_subcommand_banner(raw);
185        }
186    }
187
188    let lines: Vec<&str> = raw.lines().collect();
189    let mut index = 0usize;
190    while index < lines.len() {
191        let line = lines[index];
192        let trimmed = line.trim();
193        if skip_until_usage {
194            if trimmed.starts_with("Usage:") {
195                skip_until_usage = false;
196            } else {
197                index += 1;
198                continue;
199            }
200        }
201        if let Some(about) = skip_about_line.as_deref() {
202            if trimmed == about {
203                skip_about_line = None;
204                index += 1;
205                continue;
206            }
207        }
208
209        if is_command_name_line(line)
210            && index + 1 < lines.len()
211            && is_indented_help_text(lines[index + 1], 4)
212        {
213            println!("{}", style_command_pair(line, lines[index + 1]));
214            index += 2;
215            continue;
216        }
217
218        if is_option_flags_line(line)
219            && index + 1 < lines.len()
220            && is_indented_help_text(lines[index + 1], 4)
221        {
222            println!("{}", style_option_pair(line, lines[index + 1]));
223            index += 2;
224            continue;
225        }
226
227        if is_option_entry(line)
228            && index + 1 < lines.len()
229            && is_indented_help_text(lines[index + 1], 8)
230        {
231            println!("{}", style_option_pair(line, lines[index + 1]));
232            index += 2;
233            continue;
234        }
235
236        println!("{}", style_help_line(line));
237        index += 1;
238    }
239    println!();
240}
241
242/// Print the complete Clap command tree with styled formatting.
243///
244/// Includes nested subcommands and their flags (for example
245/// `worktree-watch status --stats`), not only the root command list.
246pub fn print_command_catalog() {
247    crate::cli::ui::configure_color_output();
248    println!();
249    println!("{}", "xbp commands".bright_magenta().bold());
250    println!(
251        "{}",
252        "Complete command reference (nested flags included)".bright_black()
253    );
254    crate::cli::ui::divider(44);
255
256    let cmd = crate::cli::commands::Cli::command();
257    print_catalog_command_tree(&cmd, &[], true);
258    println!();
259}
260
261fn print_catalog_command_tree(cmd: &clap::Command, path: &[&str], is_root: bool) {
262    if !is_root {
263        let command_path = format!("xbp {}", path.join(" "));
264        println!();
265        println!("{}", command_path.bright_cyan().bold());
266        if let Some(about) = cmd.get_about().or_else(|| cmd.get_long_about()) {
267            println!("  {}", about.to_string().bright_black());
268        }
269
270        let mut args: Vec<_> = cmd
271            .get_arguments()
272            .filter(|arg| {
273                !arg.is_hide_set()
274                    && !arg.is_global_set()
275                    && arg.get_id() != "help"
276                    && arg.get_id() != "version"
277                    && arg.get_id() != "debug"
278            })
279            .collect();
280        args.sort_by_key(|arg| arg.get_id().to_string());
281
282        if !args.is_empty() {
283            println!(
284                "  {} {}",
285                "▸".bright_blue().bold(),
286                "Options:".bright_blue().bold()
287            );
288            for arg in args {
289                println!("    {}", style_catalog_arg(arg));
290            }
291        }
292
293        if let Some(after) = cmd.get_after_help().or_else(|| cmd.get_after_long_help()) {
294            let after = after.to_string();
295            let mut in_examples = false;
296            for line in after.lines() {
297                let trimmed = line.trim();
298                if trimmed.is_empty() {
299                    continue;
300                }
301                if trimmed == "Examples:" || trimmed.starts_with("Examples:") {
302                    in_examples = true;
303                    println!(
304                        "  {} {}",
305                        "◇".bright_green().bold(),
306                        "Examples:".bright_green().bold()
307                    );
308                    continue;
309                }
310                if in_examples && (trimmed.starts_with("xbp ") || trimmed.starts_with("xbp.exe ")) {
311                    println!("    {}", highlight_inline_flags(trimmed).dimmed());
312                    continue;
313                }
314                // Keep short notes under examples, stop at unrelated sections.
315                if in_examples && !trimmed.starts_with('-') && !trimmed.starts_with("Local ") {
316                    in_examples = false;
317                }
318                if in_examples {
319                    println!("    {}", trimmed.bright_black());
320                }
321            }
322        }
323    }
324
325    let mut subs: Vec<_> = cmd
326        .get_subcommands()
327        .filter(|sub| !sub.is_hide_set() && sub.get_name() != "help")
328        .collect();
329    subs.sort_by_key(|sub| sub.get_name().to_string());
330
331    for sub in subs {
332        let mut next = path.to_vec();
333        next.push(sub.get_name());
334        print_catalog_command_tree(sub, &next, false);
335    }
336}
337
338fn style_catalog_arg(arg: &clap::Arg) -> String {
339    let mut flags = Vec::new();
340    if let Some(short) = arg.get_short() {
341        flags.push(format!("-{short}"));
342    }
343    if let Some(long) = arg.get_long() {
344        flags.push(format!("--{long}"));
345    }
346    if flags.is_empty() {
347        if let Some(name) = arg.get_value_names().and_then(|names| names.first()) {
348            flags.push(format!("<{name}>"));
349        } else {
350            flags.push(format!("<{}>", arg.get_id()));
351        }
352    } else if arg.get_action().takes_values() {
353        let value_name = arg
354            .get_value_names()
355            .and_then(|names| names.first())
356            .map(|name| name.to_string())
357            .unwrap_or_else(|| arg.get_id().to_string().to_ascii_uppercase());
358        if let Some(last) = flags.last_mut() {
359            *last = format!("{last} <{value_name}>");
360        }
361    }
362
363    let flags_text = flags.join(", ").bright_yellow().to_string();
364    let help = arg
365        .get_help()
366        .or_else(|| arg.get_long_help())
367        .map(|help| help.to_string())
368        .unwrap_or_default();
369    let default = arg
370        .get_default_values()
371        .first()
372        .and_then(|value| value.to_str())
373        .map(|value| format!(" [default: {value}]"))
374        .unwrap_or_default();
375
376    if help.is_empty() {
377        format!("{flags_text}{}", default.bright_black())
378    } else {
379        format!(
380            "{flags_text:<36} {}{}",
381            help.bright_black(),
382            default.bright_black()
383        )
384    }
385}
386
387pub fn emit_version_line(version: &str) {
388    crate::cli::ui::configure_color_output();
389    println!(
390        "{} {}",
391        "xbp".bright_magenta().bold(),
392        version.bright_white().bold()
393    );
394}
395
396pub fn is_root_help_text(raw: &str) -> bool {
397    raw.lines().any(|line| {
398        let trimmed = line.trim();
399        trimmed == "Usage: xbp [OPTIONS] [COMMAND]"
400            || trimmed == "Usage: xbp.exe [OPTIONS] [COMMAND]"
401            || trimmed.starts_with("Usage: xbp [OPTIONS] [COMMAND]")
402            || trimmed.starts_with("Usage: xbp.exe [OPTIONS] [COMMAND]")
403    })
404}
405
406fn detect_help_scope(raw: &str) -> HelpScope {
407    if is_root_help_text(raw) {
408        return HelpScope::Root;
409    }
410    let first_line = raw.lines().next().unwrap_or_default().trim();
411    if first_line.starts_with("xbp ") && first_line.chars().any(|ch| ch.is_ascii_digit()) {
412        return HelpScope::Root;
413    }
414    HelpScope::Subcommand
415}
416
417fn print_root_banner(raw: &str) {
418    let version = parse_version_line(raw).unwrap_or(env!("CARGO_PKG_VERSION"));
419    println!();
420    println!(
421        "{}  {}",
422        "XBP".bright_magenta().bold(),
423        format!("v{version}").bright_white()
424    );
425    println!("{}", "Deploy · operate · debug · ship".bright_black());
426    crate::cli::ui::divider(44);
427}
428
429fn print_subcommand_banner(raw: &str) {
430    let Some((command_path, tagline)) = parse_subcommand_heading(raw) else {
431        return;
432    };
433    println!();
434    println!("{}", command_path.bright_magenta().bold());
435    if !tagline.is_empty() {
436        println!("{}", tagline.bright_black());
437    }
438    crate::cli::ui::divider(command_path.len().max(28));
439}
440
441fn parse_version_line(raw: &str) -> Option<&str> {
442    let first = raw.lines().next()?.trim();
443    let rest = first.strip_prefix("xbp")?.trim();
444    if rest.is_empty() {
445        None
446    } else {
447        Some(rest)
448    }
449}
450
451fn parse_subcommand_heading(raw: &str) -> Option<(String, String)> {
452    let about = raw
453        .lines()
454        .map(str::trim)
455        .find(|line| !line.is_empty() && !line.starts_with("Usage:"))?;
456
457    let usage = raw
458        .lines()
459        .map(str::trim)
460        .find(|line| line.starts_with("Usage:"))?;
461    let command_path = extract_command_path_from_usage(usage)?;
462
463    Some((command_path, about.to_string()))
464}
465
466fn extract_command_path_from_usage(usage: &str) -> Option<String> {
467    let rest = usage.split_once(':')?.1.trim();
468    let path = rest.split('[').next()?.trim();
469    let normalized = path.replace(".exe", "");
470    if normalized.is_empty() {
471        None
472    } else {
473        Some(normalized)
474    }
475}
476
477fn style_help_line(line: &str) -> String {
478    let trimmed = line.trim_start();
479    if trimmed.is_empty() {
480        return String::new();
481    }
482
483    if matches!(
484        trimmed,
485        "Commands:" | "Options:" | "Arguments:" | "Subcommands:"
486    ) {
487        return format!(
488            "\n{} {}",
489            "▸".bright_blue().bold(),
490            trimmed.bright_blue().bold()
491        );
492    }
493
494    if trimmed.starts_with("Usage:") {
495        return style_usage_line(line);
496    }
497
498    if trimmed == "Discover:" {
499        return format!(
500            "\n{} {}",
501            "◇".bright_green().bold(),
502            trimmed.bright_green().bold()
503        );
504    }
505
506    if is_example_section_header(trimmed) {
507        return format!(
508            "\n{} {}",
509            "◇".bright_green().bold(),
510            trimmed.bright_green().bold()
511        );
512    }
513
514    if is_note_section_header(trimmed) {
515        return format!(
516            "\n{} {}",
517            "◇".bright_yellow().bold(),
518            trimmed.bright_yellow().bold()
519        );
520    }
521
522    if is_command_entry(line) {
523        return style_command_entry(line);
524    }
525
526    if is_option_flags_line(line) {
527        return format!("  {}", line.trim().bright_yellow());
528    }
529
530    if is_option_entry(line) {
531        return style_option_entry(line);
532    }
533
534    if is_command_name_line(line) {
535        return format!("  {}", line.trim().bright_cyan().bold());
536    }
537
538    if is_indented_help_text(line, 4) || is_indented_help_text(line, 8) {
539        return format!("      {}", trimmed.bright_black());
540    }
541
542    if is_example_command_line(trimmed) {
543        return format!("  {}", highlight_inline_flags(trimmed).dimmed());
544    }
545
546    if trimmed.starts_with("Run `") || trimmed.starts_with("Use `") || trimmed.starts_with("Pass `")
547    {
548        return trimmed.bright_black().to_string();
549    }
550
551    line.to_string()
552}
553
554fn style_usage_line(line: &str) -> String {
555    let (prefix, rest) = line.split_once(':').unwrap_or((line, ""));
556    format!(
557        "{} {}",
558        format!("{prefix}:").bright_cyan().bold(),
559        highlight_inline_flags(rest.trim()).bright_white()
560    )
561}
562
563fn is_example_section_header(line: &str) -> bool {
564    matches!(
565        line,
566        "Examples:"
567            | "Quick start:"
568            | "Discover:"
569            | "Available targets:"
570            | "List installable targets:"
571    ) || line.starts_with("Quick start")
572        || line.starts_with("List installable")
573}
574
575fn is_note_section_header(line: &str) -> bool {
576    line == "Notes:"
577        || line.starts_with("Tip:")
578        || line.starts_with("More info:")
579        || line.starts_with("Hint:")
580}
581
582fn is_command_entry(line: &str) -> bool {
583    if !line.starts_with("  ") || line.starts_with("    ") {
584        return false;
585    }
586    let trimmed = line.trim_start();
587    let Some((name, _)) = trimmed.split_once("  ") else {
588        return false;
589    };
590    !name.starts_with('-') && !name.contains('<') && name.len() <= 24
591}
592
593fn style_command_entry(line: &str) -> String {
594    let trimmed = line.trim_start();
595    let mut parts = trimmed.splitn(2, "  ");
596    let name = parts.next().unwrap_or_default();
597    let description = parts.next().unwrap_or_default().trim();
598    let alias = extract_alias_suffix(description);
599    let base_description = description
600        .split_once('[')
601        .map(|(left, _)| left.trim())
602        .unwrap_or(description);
603
604    let name = name.bright_cyan().bold();
605    if alias.is_empty() {
606        format!("  {name:<22} {}", base_description.bright_black())
607    } else {
608        format!(
609            "  {name:<22} {} {}",
610            base_description.bright_black(),
611            alias.bright_black()
612        )
613    }
614}
615
616fn extract_alias_suffix(description: &str) -> String {
617    let Some(start) = description.find('[') else {
618        return String::new();
619    };
620    description[start..].to_string()
621}
622
623fn is_option_flags_line(line: &str) -> bool {
624    let trimmed = line.trim_start();
625    line.starts_with("  ")
626        && !line.starts_with("    ")
627        && (trimmed.starts_with("--") || trimmed.starts_with("-"))
628}
629
630fn is_option_entry(line: &str) -> bool {
631    let trimmed = line.trim_start();
632    line.starts_with("      ")
633        && (trimmed.starts_with("--") || trimmed.starts_with("-"))
634        && !trimmed.starts_with("Usage:")
635}
636
637fn is_command_name_line(line: &str) -> bool {
638    if !line.starts_with("  ") || line.starts_with("    ") {
639        return false;
640    }
641    let trimmed = line.trim();
642    !trimmed.is_empty()
643        && !trimmed.ends_with(':')
644        && !trimmed.starts_with('-')
645        && !trimmed.contains(' ')
646        && trimmed.len() <= 24
647}
648
649fn is_indented_help_text(line: &str, min_spaces: usize) -> bool {
650    let leading = line.chars().take_while(|ch| *ch == ' ').count();
651    leading >= min_spaces && !line.trim_start().starts_with('-') && !line.trim().is_empty()
652}
653
654fn style_command_pair(name_line: &str, description_line: &str) -> String {
655    let name = name_line.trim().bright_cyan().bold();
656    let description = description_line.trim();
657    let alias = extract_alias_suffix(description);
658    let base_description = description
659        .split_once('[')
660        .map(|(left, _)| left.trim())
661        .unwrap_or(description);
662
663    if alias.is_empty() {
664        format!("  {name:<22} {}", base_description.bright_black())
665    } else {
666        format!(
667            "  {name:<22} {} {}",
668            base_description.bright_black(),
669            alias.bright_black()
670        )
671    }
672}
673
674fn style_option_pair(flags_line: &str, description_line: &str) -> String {
675    let flags = flags_line.trim();
676    let styled_flags = flags
677        .split(", ")
678        .map(|flag| flag.bright_yellow().to_string())
679        .collect::<Vec<_>>()
680        .join(", ");
681    format!(
682        "      {styled_flags:<28} {}",
683        description_line.trim().bright_black()
684    )
685}
686
687fn style_option_entry(line: &str) -> String {
688    let trimmed = line.trim_start();
689    let mut parts = trimmed.splitn(2, "  ");
690    let flags = parts.next().unwrap_or_default();
691    let description = parts.next().unwrap_or_default().trim();
692
693    let styled_flags = flags
694        .split(", ")
695        .map(|flag| flag.bright_yellow().to_string())
696        .collect::<Vec<_>>()
697        .join(", ");
698
699    if description.is_empty() {
700        format!("      {styled_flags}")
701    } else {
702        format!("      {styled_flags:<28} {}", description.bright_black())
703    }
704}
705
706fn is_example_command_line(line: &str) -> bool {
707    line.starts_with("xbp ") || line.starts_with("cargo ") || line.starts_with("git ")
708}
709
710fn highlight_inline_flags(text: &str) -> String {
711    let mut output = String::new();
712    let mut current = String::new();
713    let mut chars = text.chars().peekable();
714
715    while let Some(ch) = chars.next() {
716        if ch == '-' && matches!(chars.peek(), Some('-' | 'f' | 'h' | 'l' | 'p' | 'v' | 'n')) {
717            if !current.is_empty() {
718                output.push_str(&current);
719                current.clear();
720            }
721            let mut flag = String::from('-');
722            if chars.peek() == Some(&'-') {
723                flag.push(chars.next().expect("dash"));
724            }
725            while let Some(&next) = chars.peek() {
726                if next.is_ascii_alphanumeric() || next == '-' {
727                    flag.push(chars.next().expect("flag char"));
728                } else {
729                    break;
730                }
731            }
732            output.push_str(&flag.bright_yellow().to_string());
733            continue;
734        }
735
736        if ch == '<' {
737            if !current.is_empty() {
738                output.push_str(&current);
739                current.clear();
740            }
741            let mut placeholder = String::from('<');
742            for next in chars.by_ref() {
743                placeholder.push(next);
744                if next == '>' {
745                    break;
746                }
747            }
748            output.push_str(&placeholder.bright_green().to_string());
749            continue;
750        }
751
752        current.push(ch);
753    }
754
755    if !current.is_empty() {
756        output.push_str(&current);
757    }
758    output
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn detects_root_help_scope() {
767        let raw = "xbp 10.30.3\n\nAbout\nUsage: xbp [OPTIONS] [COMMAND]";
768        assert_eq!(detect_help_scope(raw), HelpScope::Root);
769    }
770
771    #[test]
772    fn workers_help_is_not_root() {
773        let raw = "Manage workers\nUsage: xbp.exe workers [OPTIONS] <COMMAND>";
774        assert!(!is_root_help_text(raw));
775    }
776
777    #[test]
778    fn styles_usage_line_with_flags() {
779        let styled = style_help_line("Usage: xbp workers logs [OPTIONS]");
780        assert!(styled.contains("Usage:"));
781        assert!(styled.contains("workers"));
782    }
783
784    #[test]
785    fn styles_command_entry_line() {
786        let styled = style_help_line("  list      List workers [aliases: ls]");
787        assert!(styled.contains("list"));
788    }
789
790    #[test]
791    fn catalog_scope_skips_duplicate_about() {
792        let raw = "Deploy services\nUsage: xbp [OPTIONS] [COMMAND]\n\nCommands:\n  diag\n      Run diagnostics";
793        emit_styled_help(raw, HelpScope::Catalog);
794    }
795}