1use crate::app::Command;
5use crate::args::GLOBAL;
6
7mod message;
8mod ops;
9mod profile;
10mod token;
11mod topic;
12mod tui;
13
14pub fn version_text() -> String {
17 let text = format!(
18 "taba-cli {} (API contract {})",
19 env!("CARGO_PKG_VERSION"),
20 taba_client::CONTRACT_VERSION
21 );
22 match option_env!("TABA_GIT_SHA") {
23 Some(sha) => format!("{text} commit {sha}"),
24 None => text,
25 }
26}
27
28pub fn table() -> Vec<Command> {
29 let mut all = Vec::new();
30 all.extend(topic::commands());
31 all.extend(message::commands());
32 all.extend(token::commands());
33 all.extend(ops::commands());
34 all.extend(profile::commands());
35 all.extend(tui::commands());
36 all
37}
38
39pub enum Invocation<'a> {
40 Help(String),
42 Version,
43 Bare,
45 Run(&'a Command, Vec<String>),
47}
48
49pub fn resolve<'a>(table: &'a [Command], args: &[String]) -> Result<Invocation<'a>, String> {
52 let mut words: Vec<(usize, &str)> = Vec::new();
53 let mut i = 0;
54 while i < args.len() && words.len() < 2 {
55 let arg = args[i].as_str();
56 if arg == "--" {
57 break;
58 }
59 if let Some(rest) = arg.strip_prefix('-').filter(|r| !r.is_empty()) {
60 let long = rest.strip_prefix('-');
61 let takes = GLOBAL.iter().any(|o| {
62 o.takes_value
63 && match long {
64 Some(name) => o.name == name,
65 None => rest.len() == 1 && o.short.is_some_and(|s| rest.starts_with(s)),
66 }
67 });
68 i += if takes { 2 } else { 1 };
69 continue;
70 }
71 words.push((i, arg));
72 i += 1;
73 }
74 let wants_help = args.iter().any(|a| a == "--help");
75 if words.is_empty() {
76 if args.iter().any(|a| a == "--version") {
77 return Ok(Invocation::Version);
78 }
79 return if wants_help {
80 Ok(Invocation::Help(String::new()))
81 } else {
82 Ok(Invocation::Bare)
83 };
84 }
85 if words[0].1 == "help" {
86 let topic: Vec<&str> = args
87 .iter()
88 .map(String::as_str)
89 .filter(|a| !a.starts_with('-'))
90 .skip(1)
91 .collect();
92 return Ok(Invocation::Help(topic.join(" ")));
93 }
94 let find = |path: &[&str]| table.iter().find(|c| c.path == path);
95 let known_group = table
96 .iter()
97 .any(|c| c.path.len() == 2 && c.path[0] == words[0].1);
98 let (command, used): (&Command, Vec<usize>) =
99 if let Some(second) = words.get(1).filter(|_| known_group) {
100 match find(&[words[0].1, second.1]) {
101 Some(c) => (c, vec![words[0].0, second.0]),
102 None => {
103 let subs: Vec<&str> = table
104 .iter()
105 .filter(|c| c.path.len() == 2 && c.path[0] == words[0].1)
106 .map(|c| c.path[1])
107 .collect();
108 return Err(format!(
109 "`{} {}` is not a command ({} {})",
110 words[0].1,
111 second.1,
112 words[0].1,
113 subs.join(" | ")
114 ));
115 }
116 }
117 } else if let Some(c) = find(&[words[0].1]) {
118 (c, vec![words[0].0])
119 } else if known_group {
120 if wants_help {
121 return Ok(Invocation::Help(words[0].1.to_owned()));
122 }
123 let subs: Vec<&str> = table
124 .iter()
125 .filter(|c| c.path.len() == 2 && c.path[0] == words[0].1)
126 .map(|c| c.path[1])
127 .collect();
128 return Err(format!(
129 "missing the subcommand of `{}` ({})",
130 words[0].1,
131 subs.join(" | ")
132 ));
133 } else {
134 return Err(format!("`{}` is not a command", words[0].1));
135 };
136 let rest: Vec<String> = args
137 .iter()
138 .enumerate()
139 .filter(|(n, _)| !used.contains(n))
140 .map(|(_, a)| a.clone())
141 .collect();
142 Ok(Invocation::Run(command, rest))
143}
144
145pub fn usage_line(c: &Command) -> String {
146 let mut line = format!("usage: taba-cli {}", c.path.join(" "));
147 if !c.args.is_empty() {
148 line.push(' ');
149 line.push_str(c.args);
150 }
151 if !c.opts.is_empty() {
152 line.push_str(" [options]");
153 }
154 line
155}
156
157pub fn help_text(table: &[Command], topic: &str) -> String {
158 let words: Vec<&str> = topic.split_whitespace().collect();
159 let matching: Vec<&Command> = table
160 .iter()
161 .filter(|c| {
162 !words.is_empty() && c.path.len() >= words.len() && c.path[..words.len()] == words[..]
163 })
164 .collect();
165 let mut out = String::new();
166 if words.is_empty() {
167 out.push_str(&format!(
168 "{}\n\nusage: taba-cli <command> [arguments] [options]\n\nCommands:\n",
169 version_text()
170 ));
171 for c in table {
172 out.push_str(&format!(
173 " {:<28} {}\n",
174 format!("{} {}", c.path.join(" "), c.args).trim_end(),
175 c.summary
176 ));
177 }
178 out.push_str(" help [command] Shows the help of a command.\n version Shows the program version and the contract's.\n");
179 out.push_str("\nOptions of every command:\n --profile <name> Connection profile (default: the file's default profile)\n --lang <code> Interface language: en-US or pt-BR (taba-cli tui)\n -o, --output <fmt> json, table or raw\n -q, --quiet Prints nothing on success\n --request-id <id> Goes in X-Request-Id\n --help, --version\n");
180 return out;
181 }
182 if matching.is_empty() {
183 return format!("There is no help for `{topic}`. Use `taba-cli help`.\n");
184 }
185 for c in matching {
186 out.push_str(&format!("{}\n {}\n", usage_line(c), c.summary));
187 let mut opts: Vec<String> = c
188 .opts
189 .iter()
190 .map(|o| {
191 if o.takes_value {
192 format!("--{} <value>", o.name)
193 } else {
194 format!("--{}", o.name)
195 }
196 })
197 .collect();
198 opts.extend([
199 "--profile <name>".into(),
200 "-o <formato>".into(),
201 "-q".into(),
202 "--request-id <id>".into(),
203 ]);
204 out.push_str(&format!(" options: {}\n\n", opts.join(", ")));
205 }
206 out
207}
208
209use crate::app::{CliError, Ctx};
214use crate::args::Parsed;
215use crate::output;
216use taba_client::json::{self, Fields};
217use taba_client::{Call, Client, Error as ApiError};
218
219pub(crate) fn call(ctx: &Ctx<'_>, method: &'static str, path: impl Into<String>) -> Call {
221 let mut call = Call::new(method, path);
222 call.request_id = ctx.globals.request_id.clone();
223 call
224}
225
226pub(crate) fn emit(ctx: &mut Ctx<'_>, text: &str) -> Result<(), CliError> {
228 if ctx.globals.quiet {
229 return Ok(());
230 }
231 let format = ctx.format()?;
232 let rendered = output::render(text, format);
233 ctx.say(&rendered)
234}
235
236pub(crate) fn number(p: &Parsed, name: &str) -> Result<Option<u64>, CliError> {
237 p.number(name).map_err(CliError::Usage)
238}
239
240pub(crate) fn pairs(p: &Parsed, name: &str) -> Result<Vec<(String, String)>, CliError> {
242 p.values(name)
243 .into_iter()
244 .map(|raw| match raw.split_once('=') {
245 Some((k, v)) if !k.is_empty() => Ok((k.to_owned(), v.to_owned())),
246 _ => Err(CliError::Usage(format!(
247 "--{name}: `{raw}` must have the form key=value"
248 ))),
249 })
250 .collect()
251}
252
253pub(crate) fn one_of(p: &Parsed, name: &str, allowed: &[&str]) -> Result<Option<String>, CliError> {
254 match p.value(name) {
255 None => Ok(None),
256 Some(v) if allowed.contains(&v) => Ok(Some(v.to_owned())),
257 Some(v) => Err(CliError::Usage(format!(
258 "--{name}: `{v}` is not valid (use {})",
259 allowed.join(" or ")
260 ))),
261 }
262}
263
264pub(crate) fn paged(
267 ctx: &Ctx<'_>,
268 client: &Client,
269 path: &str,
270 query: Vec<(String, String)>,
271 all: bool,
272 first_cursor: Option<String>,
273) -> Result<String, CliError> {
274 let mut cursor = first_cursor;
275 let mut collected: Vec<String> = Vec::new();
276 loop {
277 let mut c = call(ctx, "GET", path);
278 c.query = query.clone();
279 if let Some(cursor) = &cursor {
280 c.query.retain(|(k, _)| k != "cursor");
281 c.query.push(("cursor".to_owned(), cursor.clone()));
282 }
283 let text = client.call(&c)?.text();
284 if !all {
285 return Ok(text);
286 }
287 let invalid =
288 |m: String| CliError::Api(ApiError::from_message("client.invalid-response", m));
289 let fields = Fields::parse(&text).map_err(|e| invalid(format!("invalid page: {e}")))?;
290 let items = json::array_items(fields.raw("items").unwrap_or("[]"))
291 .map_err(|e| invalid(format!("invalid items: {e}")))?;
292 collected.extend(items.into_iter().map(str::to_owned));
293 match fields.string("nextCursor") {
294 Some(next) => cursor = Some(next),
295 None => return Ok(format!("{{\"items\":[{}]}}", collected.join(","))),
296 }
297 }
298}