1use crate::cli::CliProtocolMode;
2use crate::formatting::{output_yaml_with_options, serialize_json_output};
3use crate::protocol::build_cli_error;
4use crate::redaction::{
5 OutputOptions, OutputStyle, RedactionContext, RedactionPolicy, Redactor, is_secret_flag_name,
6};
7use serde_json::Value;
8
9#[cfg(feature = "cli-help")]
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum HelpScope {
19 OneLevel,
24 Recursive,
26}
27
28#[cfg(feature = "cli-help")]
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum HelpFormat {
34 Plain,
35 Markdown,
36 Json,
37 Yaml,
38}
39
40#[cfg(feature = "cli-help")]
41impl HelpFormat {
42 fn parse(s: &str) -> Option<Self> {
43 match s {
44 "plain" => Some(Self::Plain),
45 "markdown" => Some(Self::Markdown),
46 "json" => Some(Self::Json),
47 "yaml" => Some(Self::Yaml),
48 _ => None,
49 }
50 }
51}
52
53#[cfg(feature = "cli-help")]
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct HelpOptions {
59 pub scope: HelpScope,
60 pub format: HelpFormat,
61}
62
63#[cfg(feature = "cli-help")]
64impl HelpOptions {
65 pub const fn one_level_plain() -> Self {
67 Self {
68 scope: HelpScope::OneLevel,
69 format: HelpFormat::Plain,
70 }
71 }
72
73 pub const fn recursive_plain() -> Self {
75 Self {
76 scope: HelpScope::Recursive,
77 format: HelpFormat::Plain,
78 }
79 }
80}
81
82#[cfg(feature = "cli-help")]
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct HelpConfig {
92 pub default_scope: HelpScope,
95 pub default_format: HelpFormat,
97 pub recursive_flag: Option<&'static str>,
104 pub output_flag: Option<&'static str>,
106 pub allow_output_format: bool,
108 pub protocol_mode: CliProtocolMode,
110}
111
112#[cfg(feature = "cli-help")]
113impl HelpConfig {
114 pub const fn new(default_scope: HelpScope, default_format: HelpFormat) -> Self {
116 Self {
117 default_scope,
118 default_format,
119 recursive_flag: None,
120 output_flag: None,
121 allow_output_format: false,
122 protocol_mode: CliProtocolMode::Legacy,
123 }
124 }
125
126 pub const fn human_cli_default() -> Self {
134 Self {
135 default_scope: HelpScope::OneLevel,
136 default_format: HelpFormat::Plain,
137 recursive_flag: None,
138 output_flag: Some("--output"),
139 allow_output_format: true,
140 protocol_mode: CliProtocolMode::Legacy,
141 }
142 }
143
144 pub const fn agent_cli_default() -> Self {
146 Self {
147 default_scope: HelpScope::Recursive,
148 default_format: HelpFormat::Plain,
149 recursive_flag: None,
150 output_flag: Some("--output"),
151 allow_output_format: true,
152 protocol_mode: CliProtocolMode::Legacy,
153 }
154 }
155
156 pub const fn with_default_scope(mut self, scope: HelpScope) -> Self {
158 self.default_scope = scope;
159 self
160 }
161
162 pub const fn with_default_format(mut self, format: HelpFormat) -> Self {
164 self.default_format = format;
165 self
166 }
167
168 pub const fn with_recursive_flag(mut self, flag: Option<&'static str>) -> Self {
170 self.recursive_flag = flag;
171 self
172 }
173
174 pub const fn with_output_flag(mut self, flag: Option<&'static str>) -> Self {
176 self.output_flag = flag;
177 self
178 }
179
180 pub const fn with_output_format_override(mut self, enabled: bool) -> Self {
182 self.allow_output_format = enabled;
183 self
184 }
185
186 pub const fn with_protocol_v1(mut self) -> Self {
188 self.protocol_mode = CliProtocolMode::ProtocolV1;
189 self
190 }
191}
192
193#[cfg(feature = "cli-help")]
201pub fn cli_render_help_with_options(
202 cmd: &clap::Command,
203 subcommand_path: &[&str],
204 options: &HelpOptions,
205) -> String {
206 let target = walk_to_subcommand(cmd, subcommand_path);
207 let mut rendered = match options.format {
208 HelpFormat::Plain => {
209 let mut help = match options.scope {
210 HelpScope::OneLevel => render_help_one_level_plain(target),
211 HelpScope::Recursive => {
212 let mut buf = String::new();
213 render_help_recursive_plain(target, &[], &mut buf);
214 buf
215 }
216 };
217 append_afdata_version_line(&mut help);
218 help
219 }
220 HelpFormat::Markdown => {
221 let mut help = render_help_markdown(cmd, subcommand_path, options.scope);
222 append_afdata_version_line(&mut help);
223 help
224 }
225 HelpFormat::Json => {
226 serialize_json_output(&build_help_schema(cmd, subcommand_path, options.scope))
227 }
228 HelpFormat::Yaml => output_yaml_with_options(
229 &build_help_schema(cmd, subcommand_path, options.scope),
230 &OutputOptions {
231 redaction: Redactor::new().policy(RedactionPolicy::RedactionNone),
232 style: OutputStyle::Raw,
233 },
234 ),
235 };
236 while rendered.ends_with('\n') {
240 rendered.pop();
241 }
242 rendered.push('\n');
243 rendered
244}
245
246#[cfg(feature = "cli-help")]
247fn append_afdata_version_line(help: &mut String) {
248 const LINE: &str = concat!("AFDATA: ", env!("CARGO_PKG_VERSION"));
249 if help.lines().any(|line| line.trim() == LINE) {
250 return;
251 }
252 if !help.is_empty() && !help.ends_with('\n') {
253 help.push('\n');
254 }
255 help.push_str(LINE);
256 help.push('\n');
257}
258
259#[cfg(feature = "cli-help")]
260fn afdata_versions_value() -> Value {
261 serde_json::json!({ "afdata": env!("CARGO_PKG_VERSION") })
262}
263
264#[cfg(feature = "cli-help")]
271pub fn cli_render_help(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
272 cli_render_help_with_options(cmd, subcommand_path, &HelpOptions::recursive_plain())
273}
274
275#[cfg(feature = "cli-help-markdown")]
282pub fn cli_render_help_markdown(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
283 cli_render_help_with_options(
284 cmd,
285 subcommand_path,
286 &HelpOptions {
287 scope: HelpScope::Recursive,
288 format: HelpFormat::Markdown,
289 },
290 )
291}
292
293#[cfg(feature = "cli-help")]
309pub fn cli_handle_help_or_continue(
310 raw_args: &[String],
311 cmd: &clap::Command,
312 config: &HelpConfig,
313) -> Result<Option<String>, Value> {
314 let parsed = parse_help_request(raw_args, cmd, config);
315 if !parsed.help_requested {
316 return Ok(None);
317 }
318 if let Some(error) = parsed.output_error {
319 let event = build_cli_error(
320 &error,
321 Some("valid help output formats: plain, markdown, json, yaml"),
322 );
323 let mut event_value: Value = event.into();
324 if config.protocol_mode == CliProtocolMode::ProtocolV1
325 && let Some(obj) = event_value.as_object_mut()
326 {
327 obj.insert("trace".to_string(), serde_json::json!({}));
328 }
329 return Err(event_value);
330 }
331
332 let (scope, format) = resolve_help_options(&parsed, config);
333 let path: Vec<&str> = parsed.subcommand_path.iter().map(String::as_str).collect();
334 let options = HelpOptions { scope, format };
335 if config.protocol_mode == CliProtocolMode::ProtocolV1
336 && matches!(format, HelpFormat::Json | HelpFormat::Yaml)
337 {
338 #[allow(clippy::expect_used)]
339 let event = crate::protocol::json_result(serde_json::json!({
340 "code": "help",
341 "help": build_help_schema(cmd, &path, scope),
342 }))
343 .trace(serde_json::json!({}))
344 .build()
345 .expect("help builder failed");
346 let rendered = match format {
347 HelpFormat::Json => serialize_json_output(&event),
348 HelpFormat::Yaml => output_yaml_with_options(
349 &event,
350 &OutputOptions {
351 redaction: Redactor::new().policy(RedactionPolicy::RedactionNone),
352 style: OutputStyle::Raw,
353 },
354 ),
355 HelpFormat::Plain | HelpFormat::Markdown => unreachable!(),
356 };
357 return Ok(Some(format!("{rendered}\n")));
358 }
359 Ok(Some(cli_render_help_with_options(cmd, &path, &options)))
360}
361
362#[cfg(feature = "cli-help")]
363fn resolve_help_options(
364 parsed: &ParsedHelpRequest,
365 config: &HelpConfig,
366) -> (HelpScope, HelpFormat) {
367 let scope = if parsed.recursive_requested {
371 HelpScope::Recursive
372 } else {
373 config.default_scope
374 };
375 let format = if config.allow_output_format {
376 parsed.output_format.unwrap_or(config.default_format)
377 } else {
378 config.default_format
379 };
380 (scope, format)
381}
382
383#[cfg(feature = "cli-help")]
384fn walk_to_subcommand<'a>(cmd: &'a clap::Command, path: &[&str]) -> &'a clap::Command {
385 let mut current = cmd;
386 for name in path {
387 current = current.find_subcommand(name).unwrap_or(current);
388 }
389 current
390}
391
392#[cfg(feature = "cli-help")]
393fn walk_to_subcommand_with_names<'a>(
394 cmd: &'a clap::Command,
395 path: &[&str],
396) -> (&'a clap::Command, Vec<String>) {
397 let mut current = cmd;
398 let mut names = vec![cmd.get_name().to_string()];
399 for name in path {
400 if let Some(next) = current.find_subcommand(name) {
401 current = next;
402 names.push(next.get_name().to_string());
403 } else {
404 break;
405 }
406 }
407 (current, names)
408}
409
410#[cfg(feature = "cli-help")]
411fn render_help_one_level_plain(cmd: &clap::Command) -> String {
412 enriched_help_command(cmd).render_long_help().to_string()
413}
414
415#[cfg(feature = "cli-help")]
416fn redact_secret_help_defaults(mut cmd: clap::Command) -> clap::Command {
417 let context = RedactionContext::default();
418 let ids: Vec<String> = cmd
419 .get_arguments()
420 .filter(|arg| !arg.get_default_values().is_empty())
421 .filter(|arg| help_arg_is_secret(arg, &context))
422 .map(|arg| arg.get_id().to_string())
423 .collect();
424 for id in ids {
425 cmd = cmd.mut_arg(id, |arg| arg.default_value("***"));
426 }
427 cmd
428}
429
430#[cfg(feature = "cli-help")]
431fn help_arg_is_secret(arg: &clap::Arg, context: &RedactionContext) -> bool {
432 is_secret_flag_name(arg.get_id().as_ref(), context)
433 || arg
434 .get_long()
435 .is_some_and(|long| is_secret_flag_name(long, context))
436}
437
438#[cfg(feature = "cli-help")]
449fn enriched_help_command(cmd: &clap::Command) -> clap::Command {
450 let cmd = redact_secret_help_defaults(cmd.clone());
451 let description = if visible_subcommands(&cmd).next().is_some() {
452 HELP_FLAG_WITH_SUBCOMMANDS
453 } else {
454 HELP_FLAG_LEAF
455 };
456 cmd.disable_help_flag(true).arg(
461 clap::Arg::new("help")
462 .short('h')
463 .long("help")
464 .help(description)
465 .long_help(description)
466 .action(clap::ArgAction::Help),
467 )
468}
469
470#[cfg(feature = "cli-help")]
472const HELP_FLAG_WITH_SUBCOMMANDS: &str = "Print help. Add --recursive to expand every nested subcommand; \
473 add --output json|yaml|markdown to render this help in another format.";
474
475#[cfg(feature = "cli-help")]
477const HELP_FLAG_LEAF: &str =
478 "Print help. Add --output json|yaml|markdown to render this help in another format.";
479
480#[cfg(feature = "cli-help")]
481fn render_help_recursive_plain(cmd: &clap::Command, parent_path: &[&str], buf: &mut String) {
482 use std::fmt::Write;
483
484 let mut cmd_path = parent_path.to_vec();
486 cmd_path.push(cmd.get_name());
487 let path_str = cmd_path.join(" ");
488
489 if !buf.is_empty() {
491 let _ = writeln!(buf);
492 let _ = writeln!(buf, "{}", "═".repeat(60));
493 }
494
495 if let Some(about) = cmd.get_about() {
497 let _ = writeln!(buf, "{path_str} — {about}");
498 } else {
499 let _ = writeln!(buf, "{path_str}");
500 }
501 let _ = writeln!(buf);
502
503 let is_target = parent_path.is_empty();
507 let styled = if is_target {
508 enriched_help_command(cmd).render_long_help()
509 } else {
510 redact_secret_help_defaults(cmd.clone()).render_long_help()
511 };
512 let help_text = styled.to_string();
513 let _ = write!(buf, "{help_text}");
514
515 for sub in cmd.get_subcommands() {
517 if sub.get_name() == "help" || sub.is_hide_set() {
518 continue; }
520 render_help_recursive_plain(sub, &cmd_path, buf);
521 }
522}
523
524#[cfg(feature = "cli-help")]
525fn render_help_markdown(cmd: &clap::Command, subcommand_path: &[&str], scope: HelpScope) -> String {
526 let (target, names) = walk_to_subcommand_with_names(cmd, subcommand_path);
527 let mut buf = String::new();
528 render_markdown_command(target, &names, &mut buf, 1, true);
529 if matches!(scope, HelpScope::Recursive) {
530 render_markdown_descendants(target, &names, &mut buf, 2);
531 }
532 buf
533}
534
535#[cfg(feature = "cli-help")]
536fn render_markdown_descendants(
537 cmd: &clap::Command,
538 parent_names: &[String],
539 buf: &mut String,
540 level: usize,
541) {
542 for sub in cmd.get_subcommands() {
543 if sub.get_name() == "help" || sub.is_hide_set() {
544 continue;
545 }
546 let mut names = parent_names.to_vec();
547 names.push(sub.get_name().to_string());
548 render_markdown_command(sub, &names, buf, level, false);
549 render_markdown_descendants(sub, &names, buf, level.saturating_add(1));
550 }
551}
552
553#[cfg(feature = "cli-help")]
554fn render_markdown_command(
555 cmd: &clap::Command,
556 names: &[String],
557 buf: &mut String,
558 level: usize,
559 enrich: bool,
560) {
561 use std::fmt::Write;
562
563 if !buf.is_empty() {
564 let _ = writeln!(buf);
565 }
566 let heading_level = "#".repeat(level.max(1));
567 let path = names.join(" ");
568 if let Some(about) = cmd.get_about() {
569 let _ = writeln!(buf, "{heading_level} {path} - {about}");
570 } else {
571 let _ = writeln!(buf, "{heading_level} {path}");
572 }
573 if let Some(long_about) = markdown_long_about(cmd) {
574 let _ = writeln!(buf);
575 write_trimmed_help(buf, &long_about);
576 }
577 let _ = writeln!(buf);
578 let _ = writeln!(buf, "```text");
579 let help = markdown_help_block_command(cmd, enrich).render_long_help();
580 write_trimmed_help(buf, &help.to_string());
581 if !buf.ends_with('\n') {
582 let _ = writeln!(buf);
583 }
584 let _ = writeln!(buf, "```");
585}
586
587#[cfg(feature = "cli-help")]
588fn markdown_long_about(cmd: &clap::Command) -> Option<String> {
589 let long_about = cmd.get_long_about()?.to_string();
590 let rendered = match cmd.get_about() {
591 Some(about) => {
592 let about_str = about.to_string();
593 if long_about.trim() == format!("{} - {}", cmd.get_name(), about_str) {
594 return None;
595 }
596 strip_leading_about_paragraph(&long_about, &about_str)
597 }
598 None => long_about.as_str(),
599 };
600 let rendered = rendered.trim_matches(['\r', '\n']);
601 if rendered.is_empty() {
602 None
603 } else {
604 Some(rendered.to_string())
605 }
606}
607
608#[cfg(feature = "cli-help")]
609fn strip_leading_about_paragraph<'a>(long_about: &'a str, about: &str) -> &'a str {
610 let long_about = long_about.trim_start_matches(['\r', '\n']);
611 let Some(rest) = long_about.strip_prefix(about) else {
612 return long_about;
613 };
614 if rest.is_empty() {
615 return "";
616 }
617 rest.strip_prefix("\r\n\r\n")
618 .or_else(|| rest.strip_prefix("\n\n"))
619 .unwrap_or(long_about)
620}
621
622#[cfg(feature = "cli-help")]
623fn markdown_help_block_command(cmd: &clap::Command, enrich: bool) -> clap::Command {
624 let cmd = if enrich {
625 enriched_help_command(cmd)
626 } else {
627 redact_secret_help_defaults(cmd.clone())
628 };
629 cmd.about(None::<&str>).long_about(None::<&str>)
630}
631
632#[cfg(feature = "cli-help")]
633fn write_trimmed_help(buf: &mut String, help: &str) {
634 use std::fmt::Write;
635
636 for line in help.lines() {
637 let _ = writeln!(buf, "{}", line.trim_end());
638 }
639}
640
641#[cfg(feature = "cli-help")]
642struct ParsedHelpRequest {
643 help_requested: bool,
644 recursive_requested: bool,
645 output_format: Option<HelpFormat>,
646 output_error: Option<String>,
647 subcommand_path: Vec<String>,
648}
649
650#[cfg(feature = "cli-help")]
651fn parse_help_request(
652 raw_args: &[String],
653 cmd: &clap::Command,
654 config: &HelpConfig,
655) -> ParsedHelpRequest {
656 let args = match raw_args.first() {
657 Some(first) if first.starts_with('-') || cmd.find_subcommand(first).is_some() => raw_args,
658 _ => raw_args.get(1..).unwrap_or(&[]),
659 };
660 let mut help_requested = false;
661 let mut recursive_requested = false;
662 let mut output_format = None;
663 let mut output_error = None;
664 let mut subcommand_path = Vec::new();
665 let mut current = cmd;
666 let output_flag = config.output_flag.map(normalize_long_flag);
667 let recursive_flag = config.recursive_flag.map(normalize_long_flag);
668
669 let mut i = 0usize;
670 while i < args.len() {
671 let arg = args[i].as_str();
672 if arg == "--" {
673 break;
674 }
675
676 let (flag_name, inline_value) = split_flag(arg);
677 if matches!(arg, "--help" | "-h") {
678 help_requested = true;
679 i += 1;
680 continue;
681 }
682 if arg == "--recursive"
687 || flag_name
688 .zip(recursive_flag)
689 .is_some_and(|(seen, expected)| seen == expected)
690 {
691 recursive_requested = true;
692 i += 1;
693 continue;
694 }
695 if config.allow_output_format && arg == "--json" {
696 set_help_output_format(
697 &mut output_format,
698 HelpFormat::Json,
699 "--json",
700 &mut output_error,
701 );
702 i += 1;
703 continue;
704 }
705 if config.allow_output_format
706 && flag_name
707 .zip(output_flag)
708 .is_some_and(|(seen, expected)| seen == expected)
709 {
710 let value = inline_value.or_else(|| {
711 args.get(i + 1)
712 .map(String::as_str)
713 .filter(|next| !next.starts_with('-'))
714 });
715 if let Some(value) = value {
716 match HelpFormat::parse(value) {
717 Some(format) => set_help_output_format(
718 &mut output_format,
719 format,
720 &format!("--{} {value}", output_flag.unwrap_or("output")),
721 &mut output_error,
722 ),
723 None => {
724 output_error = Some(format!(
725 "invalid --{} format '{}': expected plain, json, yaml, or markdown",
726 output_flag.unwrap_or("output"),
727 value
728 ));
729 }
730 }
731 } else {
732 output_error = Some(format!(
733 "missing value for --{}: expected plain, json, yaml, or markdown",
734 output_flag.unwrap_or("output")
735 ));
736 }
737 i += if inline_value.is_some() || value.is_none() {
738 1
739 } else {
740 2
741 };
742 continue;
743 }
744 if arg.starts_with('-') {
745 i += if inline_value.is_none() && flag_takes_value(current, arg) {
746 2
747 } else {
748 1
749 };
750 continue;
751 }
752 if let Some(sub) = current.find_subcommand(arg)
753 && sub.get_name() != "help"
754 && !sub.is_hide_set()
755 {
756 subcommand_path.push(sub.get_name().to_string());
757 current = sub;
758 }
759 i += 1;
760 }
761
762 ParsedHelpRequest {
763 help_requested,
764 recursive_requested,
765 output_format,
766 output_error,
767 subcommand_path,
768 }
769}
770
771#[cfg(feature = "cli-help")]
772fn set_help_output_format(
773 current: &mut Option<HelpFormat>,
774 next: HelpFormat,
775 source: &str,
776 output_error: &mut Option<String>,
777) {
778 if let Some(existing) = current
779 && *existing != next
780 {
781 *output_error = Some(format!(
782 "conflicting output formats: {source} conflicts with previous output format"
783 ));
784 return;
785 }
786 *current = Some(next);
787}
788
789fn normalize_long_flag(flag: &str) -> &str {
790 flag.trim_start_matches('-')
791}
792
793fn split_flag(arg: &str) -> (Option<&str>, Option<&str>) {
794 if let Some(stripped) = arg.strip_prefix("--") {
795 if let Some((name, value)) = stripped.split_once('=') {
796 (Some(name), Some(value))
797 } else {
798 (Some(stripped), None)
799 }
800 } else if let Some(stripped) = arg.strip_prefix('-') {
801 (Some(stripped), None)
802 } else {
803 (None, None)
804 }
805}
806
807#[cfg(feature = "cli-help")]
808fn flag_takes_value(cmd: &clap::Command, raw_flag: &str) -> bool {
809 let Some(flag) = raw_flag.strip_prefix('-') else {
810 return false;
811 };
812 let name = flag.trim_start_matches('-');
813 cmd.get_arguments().any(|arg| {
814 let long_matches = arg.get_long().is_some_and(|long| long == name);
815 let short_matches =
816 name.len() == 1 && arg.get_short().is_some_and(|short| name.starts_with(short));
817 (long_matches || short_matches)
818 && matches!(
819 arg.get_action(),
820 clap::ArgAction::Set | clap::ArgAction::Append
821 )
822 })
823}
824
825#[cfg(feature = "cli-help")]
826fn build_help_schema(cmd: &clap::Command, subcommand_path: &[&str], scope: HelpScope) -> Value {
827 let (target, names) = walk_to_subcommand_with_names(cmd, subcommand_path);
828 let mut schema = command_schema(target, &names, matches!(scope, HelpScope::Recursive), true);
829 if let Value::Object(map) = &mut schema {
830 map.insert("code".to_string(), Value::String("help".to_string()));
831 map.insert(
832 "scope".to_string(),
833 Value::String(help_scope_tag(scope).to_string()),
834 );
835 map.insert("versions".to_string(), afdata_versions_value());
836 }
837 schema
838}
839
840#[cfg(feature = "cli-help")]
841fn help_scope_tag(scope: HelpScope) -> &'static str {
842 match scope {
843 HelpScope::OneLevel => "one_level",
844 HelpScope::Recursive => "recursive",
845 }
846}
847
848#[cfg(feature = "cli-help")]
849fn command_schema(cmd: &clap::Command, names: &[String], recursive: bool, enrich: bool) -> Value {
850 let subcommands: Vec<Value> = visible_subcommands(cmd)
851 .map(|sub| {
852 let mut child_names = names.to_vec();
853 child_names.push(sub.get_name().to_string());
854 if recursive {
855 command_schema(sub, &child_names, true, false)
857 } else {
858 command_summary_schema(sub, &child_names)
859 }
860 })
861 .collect();
862
863 serde_json::json!({
864 "name": cmd.get_name(),
865 "command_path": names.join(" "),
866 "path": names,
867 "about": styled_to_value(cmd.get_about()),
868 "long_about": styled_to_value(cmd.get_long_about()),
869 "usage": cmd.clone().render_usage().to_string(),
870 "arguments": command_arguments_schema(cmd, enrich),
871 "subcommands": subcommands,
872 })
873}
874
875#[cfg(feature = "cli-help")]
876fn command_summary_schema(cmd: &clap::Command, names: &[String]) -> Value {
877 serde_json::json!({
878 "name": cmd.get_name(),
879 "command_path": names.join(" "),
880 "path": names,
881 "about": styled_to_value(cmd.get_about()),
882 "long_about": styled_to_value(cmd.get_long_about()),
883 "usage": Value::Null,
884 "arguments": [],
885 "subcommands": [],
886 })
887}
888
889#[cfg(feature = "cli-help")]
890fn visible_subcommands(cmd: &clap::Command) -> impl Iterator<Item = &clap::Command> {
891 cmd.get_subcommands()
892 .filter(|sub| sub.get_name() != "help" && !sub.is_hide_set())
893}
894
895#[cfg(feature = "cli-help")]
896fn command_arguments_schema(cmd: &clap::Command, enrich: bool) -> Vec<Value> {
897 let owned = enrich.then(|| enriched_help_command(cmd));
903 let source = owned.as_ref().unwrap_or(cmd);
904 source
905 .get_arguments()
906 .filter(|arg| !arg.is_hide_set())
907 .map(argument_schema)
908 .collect()
909}
910
911#[cfg(feature = "cli-help")]
912fn argument_schema(arg: &clap::Arg) -> Value {
913 let value_names: Vec<String> = arg
914 .get_value_names()
915 .map(|names| names.iter().map(ToString::to_string).collect())
916 .unwrap_or_default();
917 let default_values: Vec<String> = arg
918 .get_default_values()
919 .iter()
920 .map(|value| {
921 if help_arg_is_secret(arg, &RedactionContext::default()) {
922 "***".to_string()
923 } else {
924 value.to_string_lossy().to_string()
925 }
926 })
927 .collect();
928 serde_json::json!({
929 "id": arg.get_id().to_string(),
930 "kind": if arg.get_long().is_some() || arg.get_short().is_some() { "option" } else { "argument" },
931 "long": arg.get_long(),
932 "short": arg.get_short().map(|c| c.to_string()),
933 "help": styled_to_value(arg.get_help()),
934 "long_help": styled_to_value(arg.get_long_help()),
935 "required": arg.is_required_set(),
936 "action": format!("{:?}", arg.get_action()),
937 "value_names": value_names,
938 "default_values": default_values,
939 })
940}
941
942#[cfg(feature = "cli-help")]
943fn styled_to_value(value: Option<&clap::builder::StyledStr>) -> Value {
944 value.map_or(Value::Null, |s| Value::String(s.to_string()))
945}