1#![recursion_limit = "256"]
11#![allow(
12 clippy::result_large_err,
13 reason = "CLI errors expose all structured backend fields"
14)]
15mod args;
16mod backend;
17mod backend_error;
18mod commands;
19mod config;
20mod error;
21mod payload;
22mod profiles;
23mod progress;
24mod prompt;
25mod render;
26mod resolve;
27mod uploads;
28
29use clap::Parser;
30use std::process::ExitCode;
31
32const USAGE_EXIT_CODE: u8 = 2;
38
39pub async fn main() -> ExitCode {
40 let cli = match args::Cli::try_parse() {
41 Ok(cli) => cli,
42 Err(error) => return render_parse_failure(&error),
43 };
44 if let Err(error) = args::validate_cli(&cli) {
45 return render_parse_failure(&error);
46 }
47 let runtime = args::RuntimeBehavior::detect(&cli);
48
49 let result = Box::pin(commands::run(cli, runtime)).await;
51 match result {
52 Ok(output) => match render::render_success(&output, runtime.json) {
53 Ok(()) if output.data.reports_failures() => ExitCode::FAILURE,
56 Ok(()) => ExitCode::SUCCESS,
57 Err(err) => {
58 let failure = commands::CommandFailure {
59 kind: output.kind,
60 profile: output.profile.clone(),
61 mode: output.mode,
62 error: Box::new(error::CliError::io(err)),
63 };
64 let _ = render::render_error(&failure, runtime.json);
65 ExitCode::FAILURE
66 }
67 },
68 Err(failure) => {
69 let _ = render::render_error(&failure, runtime.json);
70 ExitCode::FAILURE
71 }
72 }
73}
74
75fn render_parse_failure(error: &clap::Error) -> ExitCode {
86 if !error.use_stderr() {
87 error.print().ok();
88 return ExitCode::SUCCESS;
89 }
90 if !json_requested(std::env::args_os()) {
91 error.print().ok();
92 return ExitCode::from(USAGE_EXIT_CODE);
93 }
94 let failure = parse_failure_error(error);
95 let _ = render::render_parse_error(&failure);
96 ExitCode::from(USAGE_EXIT_CODE)
97}
98
99fn parse_failure_error(error: &clap::Error) -> error::CliError {
100 let failure = error::CliError::invalid_usage(error.render().to_string());
101 match parse_error_param(error) {
102 Some(param) => failure.with_param(param),
103 None => failure,
104 }
105}
106
107fn parse_error_param(error: &clap::Error) -> Option<String> {
108 use clap::error::{ContextKind, ContextValue};
109
110 let value = error.get(ContextKind::InvalidArg)?;
111 let rendered = match value {
112 ContextValue::String(value) => value.clone(),
113 ContextValue::Strings(values) => values.first()?.clone(),
114 ContextValue::StyledStr(value) => value.to_string(),
115 ContextValue::StyledStrs(values) => values.first()?.to_string(),
116 _ => return None,
117 };
118 let token = rendered.split_whitespace().next()?;
119 Some(
120 token
121 .trim_matches(|character| matches!(character, '`' | '\'' | '[' | ']'))
122 .to_owned(),
123 )
124}
125
126fn json_requested(arguments: impl IntoIterator<Item = std::ffi::OsString>) -> bool {
130 for argument in arguments {
131 if argument == "--" {
132 return false;
133 }
134 if argument == "--json" {
135 return true;
136 }
137 }
138 false
139}