-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmain.rs
348 lines (301 loc) · 11.6 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! The Zoo command line tool.
#![deny(missing_docs)]
// Always export the cmd_* modules as public so that it tells us when we are
// missing docs.
mod cmd;
/// The alias command.
pub mod cmd_alias;
/// The api command.
pub mod cmd_api;
/// The api call command.
pub mod cmd_api_call;
/// The app command.
pub mod cmd_app;
/// The auth command.
pub mod cmd_auth;
/// The completion command.
pub mod cmd_completion;
/// The config command.
pub mod cmd_config;
/// The drake command.
pub mod cmd_drake;
/// The file command.
pub mod cmd_file;
/// The generate command.
pub mod cmd_generate;
/// The kcl command.
pub mod cmd_kcl;
/// The ml command.
pub mod cmd_ml;
/// The open command.
pub mod cmd_open;
/// The say command.
pub mod cmd_say;
/// The start-session command.
pub mod cmd_start_session;
/// The update command.
pub mod cmd_update;
/// The user command.
pub mod cmd_user;
/// The version command.
pub mod cmd_version;
/// Formatting for `kcl` errors.
pub mod kcl_error_fmt;
// Use of a mod or pub mod is not actually necessary.
mod built_info {
// The file has been placed there by the build script.
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}
mod colors;
mod config;
mod config_alias;
mod config_file;
mod config_from_env;
mod config_from_file;
mod config_map;
mod context;
mod docs_markdown;
mod iostreams;
mod types;
#[cfg(test)]
mod tests;
mod update;
use std::io::{Read, Write};
use anyhow::Result;
use clap::Parser;
use slog::Drain;
/// The default host for the Zoo API.
pub const DEFAULT_HOST: &str = "https://api.zoo.dev";
/// Work seamlessly with Zoo from the command line.
///
/// You've never CAD it so good.
///
/// Environment variables that can be used with `zoo`.
///
/// ZOO_TOKEN: an authentication token for Zoo API requests. Setting this
/// avoids being prompted to authenticate and takes precedence over previously
/// stored credentials.
///
/// ZOO_HOST: specify the Zoo hostname for commands that would otherwise assume
/// the "api.zoo.dev" host.
///
/// ZOO_BROWSER, BROWSER (in order of precedence): the web browser to use for opening
/// links.
///
/// DEBUG: set to any value to enable verbose output to standard error.
///
/// ZOO_PAGER, PAGER (in order of precedence): a terminal paging program to send
/// standard output to, e.g. "less".
///
/// NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output.
///
/// CLICOLOR: set to "0" to disable printing ANSI colors in output.
///
/// CLICOLOR_FORCE: set to a value other than "0" to keep ANSI colors in output
/// even when the output is piped.
///
/// ZOO_FORCE_TTY: set to any value to force terminal-style output even when the
/// output is redirected. When the value is a number, it is interpreted as the number of
/// columns available in the viewport. When the value is a percentage, it will be applied
/// against the number of columns available in the current viewport.
///
/// ZOO_NO_UPDATE_NOTIFIER: set to any value to disable update notifications. By
/// default, `zoo` checks for new releases once every 24 hours and displays an upgrade
/// notice on standard error if a newer version was found.
///
/// ZOO_CONFIG_DIR: the directory where `zoo` will store configuration files.
/// Default: `$XDG_CONFIG_HOME/zoo` or `$HOME/.config/zoo`.
#[derive(Parser, Debug, Clone)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!("\n"))]
struct Opts {
/// Print debug info
#[clap(short, long, global = true, env)]
debug: bool,
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Parser, Debug, Clone)]
enum SubCommand {
#[clap(alias = "aliases")]
Alias(cmd_alias::CmdAlias),
Api(cmd_api::CmdApi),
ApiCall(cmd_api_call::CmdApiCall),
App(cmd_app::CmdApp),
Auth(cmd_auth::CmdAuth),
Completion(cmd_completion::CmdCompletion),
Config(cmd_config::CmdConfig),
Drake(cmd_drake::CmdDrake),
File(cmd_file::CmdFile),
Generate(cmd_generate::CmdGenerate),
Kcl(cmd_kcl::CmdKcl),
Ml(cmd_ml::CmdMl),
Say(cmd_say::CmdSay),
// Hide until <https://github.com/KittyCAD/cli/issues/983> is done.
#[clap(hide = true)]
StartSession(cmd_start_session::CmdStartSession),
Open(cmd_open::CmdOpen),
Update(cmd_update::CmdUpdate),
User(cmd_user::CmdUser),
Version(cmd_version::CmdVersion),
}
#[tokio::main]
async fn main() -> Result<(), ()> {
miette::set_hook(Box::new(|_| {
Box::new(miette::MietteHandlerOpts::new().show_related_errors_as_nested().build())
}))
.map_err(|err| {
eprintln!("Failed to set miette hook: {err}");
})?;
let build_version = clap::crate_version!();
// Check for updates to the cli.
// We don't await here since we don't want to block the main thread.
// We'll check again before we exit.
let update = crate::update::check_for_update(build_version, false);
// Let's get our configuration.
let mut c = crate::config_file::parse_default_config().unwrap();
let mut config = crate::config_from_env::EnvConfig::inherit_env(&mut c);
let mut ctx = crate::context::Context::new(&mut config);
// Let's grab all our args.
let args: Vec<String> = std::env::args().collect();
let result = do_main(args, &mut ctx).await;
// If we have an update, let's print it.
handle_update(&mut ctx, update.await.unwrap_or_default(), build_version).unwrap();
if let Err(err) = result {
eprintln!("{err}");
std::process::exit(1);
}
std::process::exit(result.unwrap_or(0));
}
async fn do_main(mut args: Vec<String>, ctx: &mut crate::context::Context<'_>) -> Result<i32> {
let original_args = args.clone();
// Remove the first argument, which is the program name, and can change depending on how
// they are calling it.
args.remove(0);
let args_str = shlex::try_join(args.iter().map(|s| s.as_str()).collect::<Vec<&str>>())?;
// Check if the user is passing in an alias.
if !crate::cmd_alias::valid_command(&args_str) {
// Let's validate if it is an alias.
// It is okay to check the error here because we will not error out if the
// alias does not exist. We will just return the expanded args.
let (mut expanded_args, is_shell) = ctx.config.expand_alias(original_args)?;
if is_shell {
// Remove the first argument, since thats our `sh`.
expanded_args.remove(0);
let mut external_cmd = std::process::Command::new("sh")
.args(expanded_args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let ecode = external_cmd.wait()?;
// Pipe the output to the terminal.
if let Some(stdout_rd) = external_cmd.stdout.as_mut() {
let mut stdout = Vec::new();
stdout_rd.read_to_end(&mut stdout)?;
ctx.io.out.write_all(&stdout)?;
}
if let Some(mut stderr_rd) = external_cmd.stderr {
let mut stderr = Vec::new();
stderr_rd.read_to_end(&mut stderr)?;
ctx.io.err_out.write_all(&stderr)?;
}
return Ok(ecode.code().unwrap_or(0));
}
// So we handled if the alias was a shell.
// We can now parse our options from the extended args.
args = expanded_args;
} else {
args = original_args;
}
// Parse the command line arguments.
let opts: Opts = Opts::parse_from(args);
// Set our debug flag.
ctx.debug = opts.debug;
// Setup our logger. This is mainly for debug purposes.
// And getting debug logs from other libraries we consume, like even Zoo.
if ctx.debug {
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
let logger = slog::Logger::root(drain, slog::o!());
let scope_guard = slog_scope::set_global_logger(logger);
scope_guard.cancel_reset();
slog_stdlog::init_with_level(log::Level::Debug).unwrap();
}
match opts.subcmd {
SubCommand::Alias(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Api(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::ApiCall(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::App(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Auth(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Completion(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Config(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Drake(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::File(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Generate(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Kcl(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Ml(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Say(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::StartSession(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Open(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Update(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::User(cmd) => run_cmd(&cmd, ctx).await,
SubCommand::Version(cmd) => run_cmd(&cmd, ctx).await,
}
}
async fn run_cmd(cmd: &impl crate::cmd::Command, ctx: &mut context::Context<'_>) -> Result<i32> {
let cs = ctx.io.color_scheme();
if let Err(err) = cmd.run(ctx).await {
// If the error was from the API, let's handle it better for each type of error.
match err.downcast::<kittycad::types::error::Error>() {
Ok(err) => {
if err.status() == Some(http::StatusCode::FORBIDDEN) {
writeln!(
ctx.io.err_out,
"{} You are not authorized to perform this action",
cs.failure_icon(),
)?;
} else if err.status() == Some(http::StatusCode::UNAUTHORIZED) {
writeln!(ctx.io.err_out, "{} You are not authenticated.", cs.failure_icon())?;
writeln!(ctx.io.err_out, "Try authenticating with: `zoo auth login`")?;
} else if let kittycad::types::error::Error::UnexpectedResponse(resp) = err {
let body = resp.text().await?;
writeln!(ctx.io.err_out, "zoo.dev api error: {}", body)?;
} else {
writeln!(ctx.io.err_out, "{err}")?;
}
}
Err(err) => {
writeln!(ctx.io.err_out, "{err}")?;
}
}
return Ok(1);
}
Ok(0)
}
fn handle_update(
ctx: &mut crate::context::Context,
update: Option<crate::update::ReleaseInfo>,
build_version: &str,
) -> Result<()> {
if let Some(latest_release) = update {
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
let is_homebrew = crate::update::is_under_homebrew()?;
if !(is_homebrew && crate::update::is_recent_release(latest_release.published_at)) {
let cs = ctx.io.color_scheme();
writeln!(
ctx.io.err_out,
"\n\n{} {} → {}\n",
cs.yellow("A new release of zoo is available:"),
cs.cyan(build_version),
cs.purple(&latest_release.version)
)?;
if is_homebrew {
writeln!(ctx.io.err_out, "To upgrade, run: `brew update && brew upgrade zoo`")?;
} else {
writeln!(ctx.io.err_out, "To upgrade, run: `zoo update`")?;
}
writeln!(ctx.io.err_out, "{}\n\n", cs.yellow(&latest_release.url))?;
}
}
Ok(())
}