1use crate::model::{CommandDef, ListDetail};
4use crate::output::{output_result, OutputOptions};
5use crate::usage::{resolve_sort_mode, sort_commands};
6
7pub fn apply_list_options(
8 commands: Vec<CommandDef>,
9 source_hash: &str,
10 sort_mode: Option<&str>,
11 top: Option<usize>,
12) -> Vec<CommandDef> {
13 let mode = resolve_sort_mode(sort_mode, source_hash);
14 let mut commands = sort_commands(commands, &mode, source_hash);
15 if let Some(n) = top {
16 commands.truncate(n);
17 }
18 commands
19}
20
21pub fn filter_by_search(commands: Vec<CommandDef>, pattern: &str) -> Vec<CommandDef> {
22 let p = pattern.to_lowercase();
23 commands
24 .into_iter()
25 .filter(|c| c.name.to_lowercase().contains(&p) || c.description.to_lowercase().contains(&p))
26 .collect()
27}
28
29pub fn list_commands(commands: &[CommandDef], opts: &ListOptions) -> crate::error::Result<()> {
30 if opts.json_output || opts.toon {
31 let detail = if opts.compact {
32 ListDetail::Names
33 } else {
34 opts.detail
35 };
36 let payload = crate::model::commands_to_json(commands, detail);
37 output_result(
38 payload,
39 &OutputOptions {
40 pretty: opts.pretty,
41 json_output: opts.json_output || !opts.toon,
42 toon: opts.toon,
43 max_bytes: opts.max_bytes,
44 inline: opts.inline,
45 ..Default::default()
46 },
47 )?;
48 return Ok(());
49 }
50
51 if opts.compact || opts.detail == ListDetail::Names {
52 let names: Vec<_> = commands.iter().map(|c| c.name.as_str()).collect();
53 println!("{}", names.join(" "));
54 return Ok(());
55 }
56
57 match opts.style {
58 ListStyle::Mcp => {
59 for cmd in commands {
60 let desc = truncate_desc(&cmd.description, if opts.verbose { 10_000 } else { 70 });
61 if desc.is_empty() {
62 println!(" {:<40}", cmd.name);
63 } else {
64 println!(" {:<40} {desc}", cmd.name);
65 }
66 }
67 }
68 ListStyle::OpenApi => {
69 let mut groups: std::collections::BTreeMap<String, Vec<&CommandDef>> =
70 std::collections::BTreeMap::new();
71 for cmd in commands {
72 let prefix = cmd
73 .name
74 .split_once('-')
75 .map(|(a, _)| a.to_string())
76 .unwrap_or_else(|| "other".into());
77 groups.entry(prefix).or_default().push(cmd);
78 }
79 for (group, cmds) in groups {
80 println!("\n{group}:");
81 for cmd in cmds {
82 let method = cmd.method.as_deref().unwrap_or("").to_uppercase();
83 let desc =
84 truncate_desc(&cmd.description, if opts.verbose { 10_000 } else { 60 });
85 if desc.is_empty() {
86 println!(" {:<45} {:<6}", cmd.name, method);
87 } else {
88 println!(" {:<45} {:<6} {desc}", cmd.name, method);
89 }
90 }
91 }
92 }
93 ListStyle::Graphql => {
94 let mut groups: std::collections::BTreeMap<&str, Vec<&CommandDef>> =
95 std::collections::BTreeMap::new();
96 for cmd in commands {
97 let key = cmd.graphql_operation_type.as_deref().unwrap_or("other");
98 groups.entry(key).or_default().push(cmd);
99 }
100 for group in ["query", "mutation"] {
101 let Some(cmds) = groups.get(group) else {
102 continue;
103 };
104 if cmds.is_empty() {
105 continue;
106 }
107 let label = if group == "query" {
108 "queries"
109 } else {
110 "mutations"
111 };
112 println!("\n{label}:");
113 for cmd in cmds {
114 let desc =
115 truncate_desc(&cmd.description, if opts.verbose { 10_000 } else { 60 });
116 if desc.is_empty() {
117 println!(" {:<40}", cmd.name);
118 } else {
119 println!(" {:<40} {desc}", cmd.name);
120 }
121 }
122 }
123 }
124 }
125 Ok(())
126}
127
128#[derive(Debug, Clone, Copy)]
129pub enum ListStyle {
130 Mcp,
131 OpenApi,
132 Graphql,
133}
134
135#[derive(Debug, Clone)]
136pub struct ListOptions {
137 pub verbose: bool,
138 pub compact: bool,
139 pub json_output: bool,
140 pub pretty: bool,
141 pub toon: bool,
142 pub detail: ListDetail,
143 pub max_bytes: Option<usize>,
144 pub inline: bool,
145 pub style: ListStyle,
146}
147
148impl ListOptions {
149 pub fn from_global(pre: &crate::cli::args::GlobalArgs, style: ListStyle) -> Self {
150 Self {
151 verbose: pre.verbose,
152 compact: pre.compact,
153 json_output: pre.json_output,
155 pretty: pre.pretty,
156 toon: pre.toon,
157 detail: pre.list_detail(),
158 max_bytes: pre.max_bytes,
159 inline: pre.inline,
160 style,
161 }
162 }
163}
164
165fn truncate_desc(description: &str, max_len: usize) -> String {
166 crate::model::truncate_at_word(description, max_len)
167}
168
169pub fn emit_command_help(
171 cmd: &CommandDef,
172 pre: &crate::cli::args::GlobalArgs,
173) -> crate::error::Result<()> {
174 if pre.json_output || pre.toon || pre.agent {
175 output_result(crate::model::command_to_json(cmd), &pre.output_options())?;
176 } else {
177 crate::cli::dynamic::print_command_help(cmd);
178 }
179 Ok(())
180}
181
182pub fn maybe_tool_help(
184 commands: &[CommandDef],
185 remaining: &[String],
186 pre: &crate::cli::args::GlobalArgs,
187) -> crate::error::Result<bool> {
188 if remaining.len() >= 2 && (remaining[1] == "--help" || remaining[1] == "-h") {
189 if let Some(cmd) = commands.iter().find(|c| c.name == remaining[0]) {
190 emit_command_help(cmd, pre)?;
191 return Ok(true);
192 }
193 }
194 Ok(false)
195}
196
197pub fn describe_tool(
198 commands: &[CommandDef],
199 name: &str,
200 pre: &crate::cli::args::GlobalArgs,
201) -> crate::error::Result<()> {
202 let cmd = commands
203 .iter()
204 .find(|c| c.name == name || c.tool_name.as_deref() == Some(name))
205 .ok_or_else(|| crate::error::Error::usage(format!("unknown tool: {name}")))?;
206 let mut opts = pre.output_options();
208 opts.json_output = true;
209 output_result(crate::model::command_to_json(cmd), &opts)?;
210 Ok(())
211}