1use std::fmt::Write as _;
10
11use clap::{Arg, Args, Command as ClapCommand, CommandFactory};
12
13use super::Cli;
14
15#[derive(Debug, Args)]
16pub struct CommandsArgs {
17 #[arg(num_args = 0.., value_name = "NAME")]
20 pub name: Vec<String>,
21
22 #[arg(long)]
24 pub examples: bool,
25
26 #[arg(long)]
30 pub json: bool,
31}
32
33pub fn run(args: CommandsArgs) -> zad::error::Result<()> {
34 let root = Cli::command();
35
36 if args.json {
37 let dump = json_dump(&root);
38 println!("{dump}");
39 return Ok(());
40 }
41
42 let mut out = String::new();
43 if args.name.is_empty() {
44 if args.examples {
45 render_all_examples(&root, &mut out);
46 } else {
47 render_index(&root, &mut out);
48 }
49 } else {
50 let path: Vec<&str> = args.name.iter().map(String::as_str).collect();
51 match find(&root, &path) {
52 Some(cmd) => {
53 if args.examples {
54 render_command_examples(cmd, &path, &mut out);
55 } else {
56 render_command_detail(cmd, &path, &mut out);
57 }
58 }
59 None => {
60 return Err(zad::error::ZadError::Invalid(format!(
61 "no such command: `{}`. Run `zad commands` to list available commands.",
62 args.name.join(" ")
63 )));
64 }
65 }
66 }
67
68 print!("{out}");
69 Ok(())
70}
71
72fn render_index(root: &ClapCommand, out: &mut String) {
73 let binary = root.get_name().to_string();
74 let _ = writeln!(out, "{binary} — available commands");
75 out.push('\n');
76 walk(root, &mut Vec::new(), &mut |path, cmd| {
77 if path.is_empty() {
78 return;
79 }
80 let desc = cmd.get_about().map(|s| s.to_string()).unwrap_or_default();
81 let joined = path.join(" ");
82 let _ = writeln!(out, " {binary} {joined:<28} {desc}");
83 });
84}
85
86fn render_command_detail(cmd: &ClapCommand, path: &[&str], out: &mut String) {
87 let _ = writeln!(out, "zad {}", path.join(" "));
88 if let Some(about) = cmd.get_about() {
89 let _ = writeln!(out, " {about}");
90 }
91 out.push('\n');
92
93 let flags: Vec<&Arg> = cmd
94 .get_arguments()
95 .filter(|a| !a.is_positional() && !a.is_hide_set())
96 .collect();
97 if !flags.is_empty() {
98 out.push_str("Flags:\n");
99 for arg in flags {
100 let render = format_flag(arg);
101 let help = arg.get_help().map(|s| s.to_string()).unwrap_or_default();
102 let _ = writeln!(out, " {render:<32} {help}");
103 }
104 out.push('\n');
105 }
106
107 let positionals: Vec<&Arg> = cmd.get_arguments().filter(|a| a.is_positional()).collect();
108 if !positionals.is_empty() {
109 out.push_str("Arguments:\n");
110 for arg in positionals {
111 let name = arg.get_id().as_str();
112 let help = arg.get_help().map(|s| s.to_string()).unwrap_or_default();
113 let _ = writeln!(out, " {name:<32} {help}");
114 }
115 out.push('\n');
116 }
117
118 let subs: Vec<&ClapCommand> = cmd.get_subcommands().filter(|s| !s.is_hide_set()).collect();
119 if !subs.is_empty() {
120 out.push_str("Subcommands:\n");
121 for sub in subs {
122 let name = sub.get_name();
123 let desc = sub.get_about().map(|s| s.to_string()).unwrap_or_default();
124 let _ = writeln!(out, " {name:<20} {desc}");
125 }
126 out.push('\n');
127 }
128
129 out.push_str("Exit codes: 0 success; 1 on any error.\n");
130 out.push('\n');
131 out.push_str("See `zad man ");
132 out.push_str(path.first().copied().unwrap_or("main"));
133 out.push_str("` for the full reference.\n");
134}
135
136fn render_all_examples(root: &ClapCommand, out: &mut String) {
137 let binary = root.get_name();
138 let _ = writeln!(out, "{binary} — realistic example invocations");
139 out.push('\n');
140 walk(root, &mut Vec::new(), &mut |path, _cmd| {
141 if path.is_empty() {
142 return;
143 }
144 if let Some(ex) = example_for(path) {
145 let _ = writeln!(out, "# {}", path.join(" "));
146 let _ = writeln!(out, "{ex}");
147 out.push('\n');
148 }
149 });
150}
151
152fn render_command_examples(_cmd: &ClapCommand, path: &[&str], out: &mut String) {
153 match example_for(path) {
154 Some(ex) => {
155 let _ = writeln!(out, "# {}", path.join(" "));
156 let _ = writeln!(out, "{ex}");
157 }
158 None => {
159 let _ = writeln!(
160 out,
161 "no example registered for `{}` — see `zad man {}`.",
162 path.join(" "),
163 path.first().copied().unwrap_or("main")
164 );
165 }
166 }
167}
168
169fn format_flag(arg: &Arg) -> String {
170 let mut s = String::new();
171 if let Some(short) = arg.get_short() {
172 let _ = write!(s, "-{short}");
173 }
174 if let Some(long) = arg.get_long() {
175 if !s.is_empty() {
176 s.push_str(", ");
177 }
178 let _ = write!(s, "--{long}");
179 }
180 if arg.get_action().takes_values() {
181 let _ = write!(s, " <{}>", arg.get_id().as_str().to_uppercase());
182 }
183 s
184}
185
186fn walk<'a>(
187 cmd: &'a ClapCommand,
188 path: &mut Vec<&'a str>,
189 f: &mut dyn FnMut(&[&str], &ClapCommand),
190) {
191 f(path, cmd);
192 for sub in cmd.get_subcommands() {
193 if sub.is_hide_set() {
194 continue;
195 }
196 path.push(sub.get_name());
197 walk(sub, path, f);
198 path.pop();
199 }
200}
201
202fn find<'a>(root: &'a ClapCommand, path: &[&str]) -> Option<&'a ClapCommand> {
203 let mut cur = root;
204 for seg in path {
205 cur = cur.get_subcommands().find(|s| s.get_name() == *seg)?;
206 }
207 Some(cur)
208}
209
210fn json_dump(root: &ClapCommand) -> String {
211 let binary = root.get_name().to_string();
212 let version = zad::version();
213 let mut commands = Vec::new();
214 walk(root, &mut Vec::new(), &mut |path, cmd| {
215 if path.is_empty() {
216 return;
217 }
218 let flags: Vec<serde_json::Value> = cmd
219 .get_arguments()
220 .filter(|a| !a.is_positional() && !a.is_hide_set())
221 .map(|a| {
222 serde_json::json!({
223 "name": a.get_id().as_str(),
224 "long": a.get_long(),
225 "short": a.get_short().map(|c| c.to_string()),
226 "help": a.get_help().map(|s| s.to_string()),
227 "takes_value": a.get_action().takes_values(),
228 })
229 })
230 .collect();
231 let positionals: Vec<serde_json::Value> = cmd
232 .get_arguments()
233 .filter(|a| a.is_positional())
234 .map(|a| {
235 serde_json::json!({
236 "name": a.get_id().as_str(),
237 "help": a.get_help().map(|s| s.to_string()),
238 })
239 })
240 .collect();
241 commands.push(serde_json::json!({
242 "path": path,
243 "description": cmd.get_about().map(|s| s.to_string()),
244 "flags": flags,
245 "positionals": positionals,
246 "example": example_for(path),
247 }));
248 });
249 let dump = serde_json::json!({
250 "binary": binary,
251 "version": version,
252 "commands": commands,
253 });
254 serde_json::to_string_pretty(&dump).unwrap()
255}
256
257fn example_for(path: &[&str]) -> Option<&'static str> {
260 match path {
261 ["service"] => Some("zad service list"),
262 ["service", "list"] => Some("zad service list"),
263 ["service", "discord"] => Some("zad service discord add"),
264 ["discord"] => Some("zad discord channels"),
265 ["discord", "send"] => Some("zad discord send --channel general --body 'deploy complete'"),
266 ["discord", "read"] => Some("zad discord read --channel general --limit 20"),
267 ["discord", "channels"] => Some("zad discord channels"),
268 ["discord", "join"] => Some("zad discord join --channel release-notes"),
269 ["discord", "leave"] => Some("zad discord leave --channel release-notes"),
270 ["discord", "permissions"] => Some("zad discord permissions show"),
271 ["telegram"] => Some("zad telegram chats"),
272 ["telegram", "send"] => Some("zad telegram send --chat team-room 'deploy complete'"),
273 ["telegram", "read"] => Some("zad telegram read --chat team-room --limit 20"),
274 ["telegram", "chats"] => Some("zad telegram chats"),
275 ["telegram", "discover"] => Some("zad telegram discover"),
276 ["telegram", "directory"] => Some("zad telegram directory"),
277 ["telegram", "permissions"] => Some("zad telegram permissions show"),
278 ["commands"] => Some("zad commands discord"),
279 ["docs"] => Some("zad docs architecture"),
280 ["man"] => Some("zad man discord"),
281 _ => None,
282 }
283}