Skip to main content

monorail/api/
cli.rs

1use crate::app;
2use crate::core::{self, error::MonorailError};
3
4use clap::builder::ArgPredicate;
5use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
6use once_cell::sync::OnceCell;
7use serde::Serialize;
8use std::io::Write;
9use std::result::Result;
10use std::str::FromStr;
11use std::{env, path};
12use tokio::runtime::Runtime;
13
14pub const CMD_MONORAIL: &str = "monorail";
15pub const CMD_CONFIG: &str = "config";
16pub const CMD_CHECKPOINT: &str = "checkpoint";
17pub const CMD_DELETE: &str = "delete";
18pub const CMD_UPDATE: &str = "update";
19pub const CMD_SHOW: &str = "show";
20pub const CMD_TARGET: &str = "target";
21pub const CMD_RUN: &str = "run";
22pub const CMD_ANALYZE: &str = "analyze";
23pub const CMD_RESULT: &str = "result";
24pub const CMD_LOG: &str = "log";
25pub const CMD_TAIL: &str = "tail";
26pub const CMD_OUT: &str = "out";
27pub const CMD_RENDER: &str = "render";
28pub const CMD_GENERATE: &str = "generate";
29
30pub const ARG_GIT_PATH: &str = "git-path";
31pub const ARG_BEGIN: &str = "begin";
32pub const ARG_END: &str = "end";
33pub const ARG_CONFIG_FILE: &str = "config-file";
34pub const ARG_OUTPUT_FORMAT: &str = "output-format";
35pub const ARG_PENDING: &str = "pending";
36pub const ARG_COMMANDS: &str = "commands";
37pub const ARG_ARGS: &str = "args";
38pub const ARG_ARGMAPS: &str = "argmaps";
39pub const ARG_TARGETS: &str = "targets";
40pub const ARG_CHANGES: &str = "changes";
41pub const ARG_CHANGE_TARGETS: &str = "change-targets";
42pub const ARG_TARGET_GROUPS: &str = "target-groups";
43pub const ARG_SEQUENCES: &str = "sequences";
44pub const ARG_ALL: &str = "all";
45pub const ARG_VERBOSE: &str = "verbose";
46pub const ARG_STDERR: &str = "stderr";
47pub const ARG_STDOUT: &str = "stdout";
48pub const ARG_ID: &str = "id";
49pub const ARG_DEPS: &str = "deps";
50pub const ARG_FAIL_ON_UNDEFINED: &str = "fail-on-undefined";
51pub const ARG_NO_BASE_ARGMAPS: &str = "no-base-argmaps";
52pub const ARG_FORMAT: &str = "format";
53pub const ARG_TYPE: &str = "type";
54pub const ARG_OUTPUT_FILE: &str = "output-file";
55
56pub const VAL_JSON: &str = "json";
57
58// clap requires fields to be static, so dynamic fields like this need to be provided as static references
59static DEFAULT_CONFIG_PATH: OnceCell<String> = OnceCell::new();
60
61fn default_config_path() -> &'static str {
62    DEFAULT_CONFIG_PATH.get_or_init(|| {
63        env::current_dir()
64            .unwrap()
65            .join("Monorail.json")
66            .display()
67            .to_string()
68    })
69}
70
71pub fn build() -> clap::Command {
72    let arg_git_path = Arg::new(ARG_GIT_PATH)
73        .long(ARG_GIT_PATH)
74        .help("Absolute path to a `git` binary to use for certain operations. If unspecified, default value uses PATH to resolve.")
75        .num_args(1)
76        .default_value("git");
77    let arg_begin = Arg::new(ARG_BEGIN)
78        .short('b')
79        .long(ARG_BEGIN)
80        .help("Start of the interval to consider for changes; if not provided, the checkpoint (or start of history, if no checkpoint exists) is used")
81        .num_args(1)
82        .required(false);
83    let arg_end = Arg::new(ARG_END)
84        .short('e')
85        .long(ARG_END)
86        .help(
87            "End of the interval to consider for changes; if not provided, end of history is used",
88        )
89        .num_args(1)
90        .required(false);
91    let arg_log_id = Arg::new(ARG_ID)
92        .long(ARG_ID)
93        .short('i')
94        .required(false)
95        .value_parser(clap::value_parser!(usize))
96        .num_args(1)
97        .help("Result id to query; if not provided, the most recent will be used");
98    let arg_log_command = Arg::new(ARG_COMMANDS)
99        .short('c')
100        .long(ARG_COMMANDS)
101        .required(false)
102        .num_args(1..)
103        .value_delimiter(' ')
104        .action(ArgAction::Append)
105        .help("A list commands for which to include logs");
106    let arg_log_target = Arg::new(ARG_TARGETS)
107        .short('t')
108        .long(ARG_TARGETS)
109        .num_args(1..)
110        .value_delimiter(' ')
111        .required(false)
112        .action(ArgAction::Append)
113        .help("A list of targets for which to include logs");
114
115    let arg_log_stderr = Arg::new(ARG_STDERR)
116        .long(ARG_STDERR)
117        .help("Include stderr logs")
118        .action(ArgAction::SetTrue);
119
120    let arg_log_stdout = Arg::new(ARG_STDOUT)
121        .long(ARG_STDOUT)
122        .help("Include stdout logs")
123        .action(ArgAction::SetTrue);
124
125    Command::new(CMD_MONORAIL)
126    .version(env!("CARGO_PKG_VERSION"))
127    .author("Patrick Nordahl <plnordahl@gmail.com>")
128    .about("A tool for effective polyglot, multi-project monorepo development.")
129    .arg_required_else_help(true)
130    .arg(
131        Arg::new(ARG_CONFIG_FILE)
132            .short('f')
133            .long(ARG_CONFIG_FILE)
134            .help("Absolute path to a configuration file")
135            .num_args(1)
136            .default_value(default_config_path()),
137    )
138    .arg(
139        Arg::new(ARG_OUTPUT_FORMAT)
140            .short('o')
141            .long(ARG_OUTPUT_FORMAT)
142            .help("Format to use for program output")
143            .value_parser([VAL_JSON])
144            .default_value(VAL_JSON)
145            .num_args(1),
146    )
147    .arg(
148        Arg::new(ARG_VERBOSE)
149            .short('v')
150            .long(ARG_VERBOSE)
151            .help("Emit additional workflow, result, and error logging. Specifying this flag multiple times (up to 3) increases the verbosity.")
152            .action(ArgAction::Count),
153    )
154    .subcommand(
155        Command::new(CMD_CONFIG)
156            .arg_required_else_help(true)
157            .about("Parse and display configuration")
158            .subcommand(
159                Command::new(CMD_SHOW)
160                    .about("Show configuration, including runtime default values"))
161            .subcommand(
162                Command::new(CMD_GENERATE)
163                    .about("Generate a configuration file from, and linked to, a source file")
164                    .after_help("This command accepts a valid JSON configuration file over stdin that includes a top-level 'source' object, validating and synchronizing the source file and generated files.")))
165    .subcommand(Command::new(CMD_CHECKPOINT)
166        .about("Show, update, or delete the tracking checkpoint")
167        .subcommand(
168        Command::new(CMD_UPDATE)
169            .about("Update the tracking checkpoint")
170            .after_help(r#"This command updates the tracking checkpoint with data appropriate for the configured vcs."#)
171            .arg(arg_git_path.clone())
172            .arg(
173                Arg::new(ARG_PENDING)
174                    .short('p')
175                    .long(ARG_PENDING)
176                    .help("Add pending changes to the checkpoint. E.g. for git, this means the paths and content checksums of uncommitted and unstaged changes since the last commit.")
177                    .action(ArgAction::SetTrue),
178            )
179            .arg(
180                Arg::new(ARG_ID)
181                    .long(ARG_ID)
182                    .short('i')
183                    .required(false)
184                    .num_args(1)
185                    .help("ID to use for the updated checkpoint. If not provided, this will default to the beginning of history for the current change provider."),
186            )
187        )
188        .subcommand(
189        Command::new(CMD_DELETE)
190            .about("Remove the tracking checkpoint")
191            .after_help(r#"This command removes the tracking checkpoint."#)
192        )
193        .subcommand(
194        Command::new(CMD_SHOW)
195            .about("Show the tracking checkpoint")
196            .after_help(r#"This command displays the tracking checkpoint."#)
197        )
198    )
199    .subcommand(Command::new(CMD_TARGET)
200        .about("Display targets and target groups")
201        .arg_required_else_help(true)
202        .subcommand(
203            Command::new(CMD_RENDER)
204                .about("Output a visualization of the target graph.")
205                .after_help("This command parses the target graph and emits it in a visual format for use in other tools, such as .dot.")
206                .arg(
207                    Arg::new(ARG_TYPE)
208                        .short('t')
209                        .long(ARG_TYPE)
210                        .help("File type to render")
211                        .required(false)
212                        .num_args(1)
213                )
214                .arg(
215                    Arg::new(ARG_OUTPUT_FILE)
216                        .short('f')
217                        .long(ARG_OUTPUT_FILE)
218                        .help("Filesystem location to render to")
219                        .required(false)
220                        .num_args(1)
221                )
222        )
223        .subcommand(
224        Command::new(CMD_SHOW)
225            .about("List targets and their properties.")
226            .after_help(r#"This command reads targets from configuration data, and displays their properties."#)
227            .arg(
228                Arg::new(ARG_TARGET_GROUPS)
229                    .short('g')
230                    .long(ARG_TARGET_GROUPS)
231                    .help("Display a representation of the 'depends on' relationship of targets. The array represents a topological ordering of the graph, with each element of the array being a set of targets that do not depend on each other at that position of the ordering.")
232                    .action(ArgAction::SetTrue),
233            )
234            .arg(
235                Arg::new(ARG_COMMANDS)
236                    .short('c')
237                    .long(ARG_COMMANDS)
238                    .help("Display information for the commands that are defined for this target.")
239                    .action(ArgAction::SetTrue),
240            )
241            .arg(
242                Arg::new(ARG_ARGMAPS)
243                    .short('m')
244                    .long(ARG_ARGMAPS)
245                    .help("Display information for the argmaps that are defined for this target.")
246                    .action(ArgAction::SetTrue),
247            )
248    ))
249
250    .subcommand(Command::new(CMD_RUN)
251        .about("Run target commands.")
252        .after_help(r#"
253When --argmaps, and/or --args are provided, keys that appear multiple times will have their respective arrays concatenated in the following order:
254
255    1. All mappings in the `base` argmap, unless disabled with --no-base-argmaps
256    2. All mappings in the files specified by --argmaps, in the order provided
257    3. Each element of the --args array, in the order provided
258
259Refer to --help for more information on these options.
260"#)
261        .arg_required_else_help(true)
262        .arg(
263            Arg::new(ARG_COMMANDS)
264                .short('c')
265                .long(ARG_COMMANDS)
266                .required(false)
267                .num_args(1..)
268                .value_delimiter(' ')
269                .action(ArgAction::Append)
270                .help("A list of commands that will be executed, in the order provided. If one or more sequences are provided, they will be expanded and executed first.")
271        )
272        .arg(
273            Arg::new(ARG_SEQUENCES)
274                .short('s')
275                .long(ARG_SEQUENCES)
276                .required(false)
277                .num_args(1..)
278                .value_delimiter(' ')
279                .action(ArgAction::Append)
280                .help("A list of command sequences that will be expanded and executed in the order provided. If one or more --commands are provided, they will be executed after all sequences.")
281        )
282        .arg(
283            Arg::new(ARG_TARGETS)
284                .short('t')
285                .long(ARG_TARGETS)
286                .num_args(1..)
287                .value_delimiter(' ')
288                .required(false)
289                .action(ArgAction::Append)
290                .help("A list of targets for which commands will be executed. If not provided, target groups will be inferred from the target graph.")
291        )
292        .arg(
293            Arg::new(ARG_ARGS)
294                .short('a')
295                .long(ARG_ARGS)
296                .num_args(1..)
297                .required(false)
298                .action(ArgAction::Append)
299                .help("A list of runtime arguments to be provided when executing one command for a single target.")
300                .long_help("This is a shorthand form of the more expressive '--argmaps', designed for testing and single command + single target use. Providing this flag without specifying exactly one command and one target will result in an error.")
301        )
302        .arg(
303            Arg::new(ARG_ARGMAPS)
304                .long(ARG_ARGMAPS)
305                .short('m')
306                .num_args(1..)
307                .required(false)
308                .action(ArgAction::Append)
309                .help("A list of argmap names to load when executing commands.")
310                .long_help(r#"Each name provided is resolved to a respective {{name}}.json file in the target's argmap directory. Each object is merged from left to right, and keys that appear in multiple mappings have their arrays of arguments concatenated according to the order of the lists provided.
311
312An argmap has the following form:
313
314{
315    "<command>": [
316        "arg1",
317        "arg2",
318        ...
319        "argN"
320    ]
321}
322
323See `monorail run -h` for information on how this interacts with other arg-related options.
324
325"#),
326        )
327        .arg(
328            Arg::new(ARG_DEPS)
329                .long(ARG_DEPS)
330                .help("Include target dependencies")
331                .action(ArgAction::SetTrue),
332        )
333        .arg(
334            Arg::new(ARG_FAIL_ON_UNDEFINED)
335                .long(ARG_FAIL_ON_UNDEFINED)
336                .long_help("Fail commands that are undefined by targets.")
337                .action(ArgAction::SetTrue),
338        )
339        .arg(
340            Arg::new(ARG_NO_BASE_ARGMAPS)
341                .long(ARG_NO_BASE_ARGMAPS)
342                .long_help("Disable loading base argmaps for run targets. By default, all base argmaps are loaded.")
343                .action(ArgAction::SetTrue),
344        )
345        .arg(arg_git_path.clone())
346        .arg(arg_begin.clone())
347        .arg(arg_end.clone())
348        .group(
349            ArgGroup::new("commands_and_or_sequences")
350                .args([ARG_COMMANDS, ARG_SEQUENCES])
351                .required(true)
352                .multiple(true),
353        )
354    )
355    .subcommand(Command::new(CMD_RESULT)
356        .about("Show historical results from runs")
357        .arg_required_else_help(true)
358        .subcommand(Command::new(CMD_SHOW)
359            .about("Show results from `run` invocations")))
360
361    // TODO: monorail log delete [--all] --result [r1 r2 r3 ... rN]
362    .subcommand(
363        Command::new(CMD_LOG)
364        .about("Show historical or tail real-time logs")
365        .arg_required_else_help(true)
366        .subcommand(
367            Command::new(CMD_SHOW).about("Display run logs")
368                .after_help(r#"This command shows logs for current or historical run invocations."#)
369                .arg(arg_log_id.clone())
370                .arg(arg_log_command.clone())
371                .arg(arg_log_target.clone())
372                .arg(arg_log_stderr.clone())
373                .arg(arg_log_stdout.clone()))
374        .subcommand(
375            Command::new(CMD_TAIL).about("Receive a stream of logs from executed commands")
376                .arg(arg_log_id)
377                .arg(arg_log_command)
378                .arg(arg_log_target)
379                .arg(arg_log_stderr)
380                .arg(arg_log_stdout))
381    )
382    .subcommand(
383        Command::new(CMD_ANALYZE).about("Analyze repository changes and targets")
384            .after_help(r#"This command performs an analysis of historical changes from the checkpoint to end of history in a change provider, as well as pending changes on the filesystem. By default, this command only outputs a list of affected targets."#)
385            .arg(arg_git_path.clone())
386            .arg(arg_begin.clone())
387            .arg(arg_end.clone())
388            .arg(
389                Arg::new(ARG_CHANGES)
390                    .long(ARG_CHANGES)
391                    .help("Displays detailed information about changes. If no checkpoint is available to guide change detection, this flag is ignored.")
392                    .action(ArgAction::SetTrue)
393                    .default_value_if(ARG_CHANGE_TARGETS, ArgPredicate::IsPresent, Some("true"))
394                    .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
395            )
396            .arg(
397                Arg::new(ARG_CHANGE_TARGETS)
398                    .long(ARG_CHANGE_TARGETS)
399                    .help("For each change, display the targets affected. If no checkpoint is available to guide change detection, this flag is ignored.")
400                    .action(ArgAction::SetTrue)
401                    .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
402            )
403            .arg(
404                Arg::new(ARG_TARGET_GROUPS)
405                    .long(ARG_TARGET_GROUPS)
406                    .help("Display targets grouped according to the dependency graph. If no checkpoint is available to guide change detection, all targets are returned.")
407                    .action(ArgAction::SetTrue)
408                    .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
409            )
410            .arg(
411                Arg::new(ARG_ALL)
412                    .long(ARG_ALL)
413                    .help("Display changes, change targets, and targets")
414                    .action(ArgAction::SetTrue),
415            ))
416    .subcommand(
417        Command::new(CMD_OUT)
418        .about("Manipulate data in the monorail output directory")
419        .arg_required_else_help(true)
420        .subcommand(
421            Command::new(CMD_DELETE).about("Delete files from the output dir")
422                .after_help(r#""#)
423                .arg(
424                Arg::new(ARG_ALL)
425                    .long(ARG_ALL)
426                    .help("Delete all subdirectories")
427                    .action(ArgAction::SetTrue),
428            )
429    ))
430}
431
432pub const HANDLE_OK: i32 = 0;
433pub const HANDLE_ERR: i32 = 1;
434pub const HANDLE_FATAL: i32 = 2;
435
436#[inline(always)]
437fn get_code(is_err: bool) -> i32 {
438    if is_err {
439        return HANDLE_ERR;
440    }
441    HANDLE_OK
442}
443
444#[derive(Debug)]
445pub struct OutputOptions<'a> {
446    pub format: &'a str,
447}
448
449#[tracing::instrument]
450pub fn handle<'a>(
451    matches: &ArgMatches,
452    output_options: &OutputOptions<'a>,
453) -> Result<i32, MonorailError> {
454    let verbosity = matches.get_one::<u8>(ARG_VERBOSE).unwrap_or(&0);
455    app::setup_tracing(output_options.format, *verbosity)?;
456
457    match matches.get_one::<String>(ARG_CONFIG_FILE) {
458        Some(config_file) => {
459            let config_file_path = path::Path::new(&config_file);
460            if !config_file_path.is_absolute() {
461                return Err(MonorailError::Generic(format!(
462                    "Configuration file path '{}' is not absolute",
463                    &config_file
464                )));
465            }
466            // Config generation does not use or require an existing config file, but will use the path from `-f`
467            if let Some(config_matches) = matches.subcommand_matches(CMD_CONFIG) {
468                if config_matches.subcommand_matches(CMD_GENERATE).is_some() {
469                    return handle_config_generate(config_file_path, output_options);
470                }
471            }
472            let work_path =
473                path::Path::new(config_file_path)
474                    .parent()
475                    .ok_or(MonorailError::Generic(format!(
476                        "Config file {} has no parent directory",
477                        config_file_path.display()
478                    )))?;
479            let mut config = core::Config::new(config_file_path)?;
480            config.check(config_file_path, work_path)?;
481            if let Some(config_matches) = matches.subcommand_matches(CMD_CONFIG) {
482                if config_matches.subcommand_matches(CMD_SHOW).is_some() {
483                    // fill all runtime derived values in prior to serializing
484                    config.fill();
485                    write_result(&Ok(config), output_options)?;
486                    return Ok(HANDLE_OK);
487                }
488            }
489            if let Some(checkpoint_matches) = matches.subcommand_matches(CMD_CHECKPOINT) {
490                if let Some(update_matches) = checkpoint_matches.subcommand_matches(CMD_UPDATE) {
491                    return handle_checkpoint_update(
492                        &config,
493                        update_matches,
494                        output_options,
495                        work_path,
496                    );
497                }
498                if checkpoint_matches.subcommand_matches(CMD_DELETE).is_some() {
499                    return handle_checkpoint_delete(&config, output_options, work_path);
500                }
501                if checkpoint_matches.subcommand_matches(CMD_SHOW).is_some() {
502                    return handle_checkpoint_show(&config, output_options, work_path);
503                }
504            }
505            if let Some(result_matches) = matches.subcommand_matches(CMD_RESULT) {
506                if result_matches.subcommand_matches(CMD_SHOW).is_some() {
507                    return handle_result_show(&config, result_matches, output_options, work_path);
508                }
509            }
510            if let Some(target_matches) = matches.subcommand_matches(CMD_TARGET) {
511                if let Some(show_matches) = target_matches.subcommand_matches(CMD_SHOW) {
512                    return handle_target_show(
513                        &mut config,
514                        show_matches,
515                        output_options,
516                        work_path,
517                    );
518                }
519                if let Some(render_matches) = target_matches.subcommand_matches(CMD_RENDER) {
520                    return handle_target_render(
521                        &config,
522                        render_matches,
523                        output_options,
524                        work_path,
525                    );
526                }
527            }
528            if let Some(analyze_matches) = matches.subcommand_matches(CMD_ANALYZE) {
529                return handle_analyze(&config, analyze_matches, output_options, work_path);
530            }
531            if let Some(run_matches) = matches.subcommand_matches(CMD_RUN) {
532                return handle_run(&config, run_matches, output_options, work_path);
533            }
534            if let Some(log_matches) = matches.subcommand_matches(CMD_LOG) {
535                if let Some(tail_matches) = log_matches.subcommand_matches(CMD_TAIL) {
536                    return handle_log_tail(&config, tail_matches, output_options);
537                }
538                if let Some(show_matches) = log_matches.subcommand_matches(CMD_SHOW) {
539                    return handle_log_show(&config, show_matches, output_options, work_path);
540                }
541            }
542            if let Some(out_matches) = matches.subcommand_matches(CMD_OUT) {
543                if let Some(delete_matches) = out_matches.subcommand_matches(CMD_DELETE) {
544                    return handle_out_delete(&config, delete_matches, output_options);
545                }
546            }
547            Err(MonorailError::from("Command not recognized"))
548        }
549        None => Err(MonorailError::from("No configuration specified")),
550    }
551}
552
553fn handle_out_delete<'a>(
554    config: &'a core::Config,
555    matches: &'a ArgMatches,
556    output_options: &OutputOptions<'a>,
557) -> Result<i32, MonorailError> {
558    let rt = Runtime::new()?;
559    let _guard =
560        rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
561    let i = app::out::OutDeleteInput::try_from(matches)?;
562    let res = app::out::out_delete(&config.out_dir, &i);
563    write_result(&res, output_options)?;
564    Ok(get_code(res.is_err()))
565}
566
567fn handle_analyze<'a>(
568    config: &'a core::Config,
569    matches: &'a ArgMatches,
570    output_options: &OutputOptions<'a>,
571    work_path: &'a path::Path,
572) -> Result<i32, MonorailError> {
573    let rt = Runtime::new()?;
574    let i = app::analyze::HandleAnalyzeInput::from(matches);
575    let res = rt.block_on(app::analyze::handle_analyze(config, &i, work_path));
576    write_result(&res, output_options)?;
577    Ok(get_code(res.is_err()))
578}
579
580fn handle_run<'a>(
581    config: &'a core::Config,
582    matches: &'a ArgMatches,
583    output_options: &OutputOptions<'a>,
584    work_path: &'a path::Path,
585) -> Result<i32, MonorailError> {
586    let rt = Runtime::new()?;
587    let _guard =
588        rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
589    let i = app::run::HandleRunInput::try_from(matches).unwrap();
590    let invocation = env::args().skip(1).collect::<Vec<_>>().join(" ");
591    let o = rt.block_on(app::run::handle_run(config, &i, &invocation, work_path))?;
592    let mut code = HANDLE_OK;
593    if o.failed {
594        code = HANDLE_ERR;
595    }
596    write_result(&Ok(o), output_options)?;
597    Ok(code)
598}
599
600fn handle_config_generate(
601    output_file_path: &path::Path,
602    output_options: &OutputOptions<'_>,
603) -> Result<i32, MonorailError> {
604    let res = app::config::config_generate(app::config::ConfigGenerateInput { output_file_path });
605    write_result(&res, output_options)?;
606    Ok(get_code(res.is_err()))
607}
608
609fn handle_checkpoint_update<'a>(
610    config: &'a core::Config,
611    matches: &'a ArgMatches,
612    output_options: &OutputOptions<'a>,
613    work_path: &'a path::Path,
614) -> Result<i32, MonorailError> {
615    let rt = Runtime::new()?;
616    let _guard =
617        rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
618    let i = app::checkpoint::CheckpointUpdateInput::try_from(matches)?;
619    let res = rt.block_on(app::checkpoint::handle_checkpoint_update(
620        config, &i, work_path,
621    ));
622    write_result(&res, output_options)?;
623    Ok(get_code(res.is_err()))
624}
625
626fn handle_checkpoint_show<'a>(
627    config: &'a core::Config,
628    output_options: &OutputOptions<'a>,
629    work_path: &'a path::Path,
630) -> Result<i32, MonorailError> {
631    let rt = Runtime::new()?;
632    let res = rt.block_on(app::checkpoint::handle_checkpoint_show(config, work_path));
633    write_result(&res, output_options)?;
634    Ok(get_code(res.is_err()))
635}
636
637fn handle_checkpoint_delete<'a>(
638    config: &'a core::Config,
639    output_options: &OutputOptions<'a>,
640    work_path: &'a path::Path,
641) -> Result<i32, MonorailError> {
642    let rt = Runtime::new()?;
643    let _guard =
644        rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
645    let res = rt.block_on(app::checkpoint::handle_checkpoint_delete(config, work_path));
646    write_result(&res, output_options)?;
647    Ok(get_code(res.is_err()))
648}
649
650fn handle_log_tail<'a>(
651    config: &'a core::Config,
652    matches: &'a ArgMatches,
653    output_options: &OutputOptions<'a>,
654) -> Result<i32, MonorailError> {
655    let rt = Runtime::new()?;
656    let i = app::log::LogTailInput::try_from(matches)?;
657    match rt.block_on(app::log::log_tail(config, &i)) {
658        Ok(_) => Ok(HANDLE_OK),
659        Err(e) => {
660            write_result(&Err::<(), MonorailError>(e), output_options)?;
661            Ok(HANDLE_ERR)
662        }
663    }
664}
665
666fn handle_log_show<'a>(
667    config: &'a core::Config,
668    matches: &'a ArgMatches,
669    output_options: &OutputOptions<'a>,
670    work_path: &'a path::Path,
671) -> Result<i32, MonorailError> {
672    let i = app::log::LogShowInput::try_from(matches)?;
673    match app::log::log_show(config, &i, work_path) {
674        Ok(_) => Ok(HANDLE_OK),
675        Err(e) => {
676            write_result(&Err::<(), MonorailError>(e), output_options)?;
677            Ok(HANDLE_ERR)
678        }
679    }
680}
681fn handle_target_render<'a>(
682    config: &'a core::Config,
683    matches: &'a ArgMatches,
684    output_options: &OutputOptions<'a>,
685    work_path: &'a path::Path,
686) -> Result<i32, MonorailError> {
687    let output_file = match matches.get_one::<String>(ARG_OUTPUT_FILE) {
688        Some(v) => {
689            let vp = path::Path::new(v);
690            if vp.is_absolute() {
691                vp.to_path_buf()
692            } else {
693                work_path.join(vp)
694            }
695        }
696        None => work_path.join("target.dot"),
697    };
698    let render_type = match matches.get_one::<String>(ARG_TYPE) {
699        Some(v) => app::target::TargetRenderType::from_str(v)?,
700        None => app::target::TargetRenderType::Dot,
701    };
702    let res = app::target::target_render(
703        config,
704        app::target::TargetRenderInput {
705            render_type,
706            output_file,
707        },
708        work_path,
709    );
710    write_result(&res, output_options)?;
711    Ok(get_code(res.is_err()))
712}
713fn handle_target_show<'a>(
714    config: &'a mut core::Config,
715    matches: &'a ArgMatches,
716    output_options: &OutputOptions<'a>,
717    work_path: &'a path::Path,
718) -> Result<i32, MonorailError> {
719    let res = app::target::target_show(
720        config,
721        app::target::TargetShowInput {
722            show_target_groups: matches.get_flag(ARG_TARGET_GROUPS),
723            show_commands: matches.get_flag(ARG_COMMANDS),
724            show_argmaps: matches.get_flag(ARG_ARGMAPS),
725        },
726        work_path,
727    );
728    write_result(&res, output_options)?;
729    Ok(get_code(res.is_err()))
730}
731fn handle_result_show<'a>(
732    config: &'a core::Config,
733    matches: &'a ArgMatches,
734    output_options: &OutputOptions<'a>,
735    work_path: &'a path::Path,
736) -> Result<i32, MonorailError> {
737    let i = app::result::ResultShowInput::try_from(matches)?;
738    let res = app::result::result_show(config, work_path, &i);
739    write_result(&res, output_options)?;
740    Ok(get_code(res.is_err()))
741}
742
743#[derive(Serialize)]
744struct TimestampWrapper<T: Serialize> {
745    timestamp: String,
746    #[serde(flatten)]
747    data: T,
748}
749impl<T: Serialize> TimestampWrapper<T> {
750    fn new(data: T) -> Self {
751        Self {
752            timestamp: chrono::Utc::now().to_rfc3339(),
753            data,
754        }
755    }
756}
757
758pub fn write_result<T>(
759    value: &Result<T, MonorailError>,
760    opts: &OutputOptions<'_>,
761) -> Result<(), MonorailError>
762where
763    T: Serialize,
764{
765    match opts.format {
766        "json" => {
767            match value {
768                Ok(t) => {
769                    let mut writer = std::io::stdout();
770                    serde_json::to_writer(&mut writer, &TimestampWrapper::new(t))?;
771                    writeln!(writer)?;
772                }
773                Err(e) => {
774                    let mut writer = std::io::stderr();
775                    serde_json::to_writer(&mut writer, &TimestampWrapper::new(e))?;
776                    writeln!(writer)?;
777                }
778            }
779            Ok(())
780        }
781        _ => Err(MonorailError::Generic(format!(
782            "Unrecognized output format {}",
783            opts.format
784        ))),
785    }
786}
787
788impl TryFrom<&clap::ArgMatches> for app::result::ResultShowInput {
789    type Error = MonorailError;
790    fn try_from(_cmd: &clap::ArgMatches) -> Result<Self, Self::Error> {
791        Ok(Self {})
792    }
793}
794
795impl<'a> TryFrom<&'a clap::ArgMatches> for app::checkpoint::CheckpointUpdateInput<'a> {
796    type Error = MonorailError;
797    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
798        Ok(Self {
799            id: cmd.get_one::<String>(ARG_ID).map(|x: &String| x.as_str()),
800            git_opts: core::git::GitOptions {
801                begin: None,
802                end: None,
803                git_path: cmd
804                    .get_one::<String>(ARG_GIT_PATH)
805                    .ok_or(MonorailError::MissingArg(ARG_GIT_PATH.into()))?,
806            },
807            pending: cmd.get_flag(ARG_PENDING),
808        })
809    }
810}
811impl<'a> TryFrom<&'a clap::ArgMatches> for app::run::HandleRunInput<'a> {
812    type Error = MonorailError;
813    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
814        Ok(Self {
815            git_opts: core::git::GitOptions {
816                begin: cmd
817                    .get_one::<String>(ARG_BEGIN)
818                    .map(|x: &String| x.as_str()),
819                end: cmd.get_one::<String>(ARG_END).map(|x: &String| x.as_str()),
820                git_path: cmd
821                    .get_one::<String>(ARG_GIT_PATH)
822                    .ok_or(MonorailError::MissingArg(ARG_GIT_PATH.into()))?,
823            },
824            commands: cmd
825                .get_many::<String>(ARG_COMMANDS)
826                .ok_or(MonorailError::MissingArg(ARG_COMMANDS.into()))
827                .into_iter()
828                .flatten()
829                .collect(),
830            targets: cmd
831                .get_many::<String>(ARG_TARGETS)
832                .into_iter()
833                .flatten()
834                .collect(),
835            sequences: cmd
836                .get_many::<String>(ARG_SEQUENCES)
837                .into_iter()
838                .flatten()
839                .collect(),
840            args: cmd
841                .get_many::<String>(ARG_ARGS)
842                .into_iter()
843                .flatten()
844                .collect(),
845            argmaps: cmd
846                .get_many::<String>(ARG_ARGMAPS)
847                .into_iter()
848                .flatten()
849                .collect(),
850            include_deps: cmd.get_flag(ARG_DEPS),
851            fail_on_undefined: cmd.get_flag(ARG_FAIL_ON_UNDEFINED),
852            use_base_argmaps: !cmd.get_flag(ARG_NO_BASE_ARGMAPS),
853        })
854    }
855}
856
857impl<'a> From<&'a clap::ArgMatches> for app::analyze::HandleAnalyzeInput<'a> {
858    fn from(cmd: &'a clap::ArgMatches) -> Self {
859        Self {
860            git_opts: core::git::GitOptions {
861                begin: cmd
862                    .get_one::<String>(ARG_BEGIN)
863                    .map(|x: &String| x.as_str()),
864                end: cmd.get_one::<String>(ARG_END).map(|x: &String| x.as_str()),
865                git_path: cmd.get_one::<String>(ARG_GIT_PATH).unwrap(),
866            },
867            analyze_input: app::analyze::AnalyzeInput {
868                show_changes: cmd.get_flag(ARG_CHANGES),
869                show_change_targets: cmd.get_flag(ARG_CHANGE_TARGETS),
870                show_target_groups: cmd.get_flag(ARG_TARGET_GROUPS),
871            },
872        }
873    }
874}
875
876impl<'a> TryFrom<&'a clap::ArgMatches> for app::log::LogTailInput {
877    type Error = MonorailError;
878    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
879        Ok(Self {
880            filter_input: core::server::LogFilterInput::try_from(cmd)?,
881        })
882    }
883}
884
885impl<'a> TryFrom<&'a clap::ArgMatches> for core::server::LogFilterInput {
886    type Error = MonorailError;
887    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
888        Ok(Self {
889            include_stdout: cmd.get_flag(ARG_STDOUT),
890            include_stderr: cmd.get_flag(ARG_STDERR),
891            commands: cmd
892                .get_many::<String>(ARG_COMMANDS)
893                .into_iter()
894                .flatten()
895                .map(|x| x.to_owned())
896                .collect(),
897            targets: cmd
898                .get_many::<String>(ARG_TARGETS)
899                .into_iter()
900                .flatten()
901                .map(|x| x.to_owned())
902                .collect(),
903        })
904    }
905}
906
907impl<'a> TryFrom<&'a clap::ArgMatches> for app::log::LogShowInput<'a> {
908    type Error = MonorailError;
909    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
910        Ok(Self {
911            filter_input: core::server::LogFilterInput::try_from(cmd)?,
912            id: cmd.get_one::<usize>(ARG_ID),
913        })
914    }
915}
916
917impl<'a> TryFrom<&'a clap::ArgMatches> for app::out::OutDeleteInput {
918    type Error = MonorailError;
919    fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
920        Ok(Self {
921            all: cmd.get_flag(ARG_ALL),
922        })
923    }
924}