Skip to main content

wtg_cli/
lib.rs

1use std::{env, ffi::OsString};
2
3use clap::Parser;
4
5use std::sync::Arc;
6
7use crate::backend::resolve_backend_with_notices;
8use crate::cli::Cli;
9use crate::error::{WtgError, WtgResult};
10use crate::release_filter::ReleaseFilter;
11use crate::resolution::resolve;
12
13pub mod backend;
14pub mod changelog;
15pub mod cli;
16pub mod constants;
17pub mod error;
18pub mod git;
19pub mod github;
20pub mod help;
21pub mod notice;
22pub mod output;
23pub mod parse_input;
24pub mod release_filter;
25pub mod remote;
26pub mod resolution;
27pub mod semver;
28
29/// Run the CLI using the process arguments.
30pub fn run() -> WtgResult<()> {
31    run_with_args(env::args())
32}
33
34/// Run the CLI using a custom iterator of arguments.
35pub fn run_with_args<I, T>(args: I) -> WtgResult<()>
36where
37    I: IntoIterator<Item = T>,
38    T: Into<OsString> + Clone,
39{
40    // Initialize logging - respects RUST_LOG env var
41    // Uses try_init to avoid panic if called multiple times (e.g., in tests)
42    let _ = env_logger::try_init();
43
44    let cli = match Cli::try_parse_from(args) {
45        Ok(cli) => cli,
46        Err(err) => {
47            // If the error is DisplayHelp, show our custom help
48            if err.kind() == clap::error::ErrorKind::DisplayHelp {
49                help::display_help();
50                return Ok(());
51            }
52            // Otherwise, propagate the error
53            return Err(WtgError::Cli {
54                message: err.to_string(),
55                code: err.exit_code(),
56            });
57        }
58    };
59    run_with_cli(cli)
60}
61
62fn run_with_cli(cli: Cli) -> WtgResult<()> {
63    // If no input provided, show custom help
64    if cli.input.is_none() {
65        help::display_help();
66        return Ok(());
67    }
68
69    let runtime = tokio::runtime::Builder::new_current_thread()
70        .enable_all()
71        .build()?;
72
73    runtime.block_on(run_async(cli))
74}
75
76async fn run_async(cli: Cli) -> WtgResult<()> {
77    // Parse the input to determine if it's a remote repo or local
78    let parsed_input = cli.parse_input()?;
79    log::debug!("Parsed input: {parsed_input:?}");
80
81    // Create notice callback - all notices (capability warnings and operational info)
82    // are delivered via callback and printed by output::print_notice
83    let notice_cb = Arc::new(output::print_notice);
84
85    // Create the backend based on available resources
86    log::debug!("Resolving backend (fetch={})", cli.fetch);
87    let backend = resolve_backend_with_notices(&parsed_input, cli.fetch, notice_cb)?;
88    log::debug!("Backend resolved");
89
90    // Build the release filter from CLI args
91    let filter = if let Some(ref release) = cli.release {
92        ReleaseFilter::Specific(release.clone())
93    } else if cli.skip_prereleases {
94        ReleaseFilter::SkipPrereleases
95    } else {
96        ReleaseFilter::Unrestricted
97    };
98
99    // Resolve the query using the backend
100    log::debug!("Disambiguating query: {:?}", parsed_input.query());
101    let query = backend.disambiguate_query(parsed_input.query()).await?;
102    log::debug!("Disambiguated to: {query:?}");
103
104    log::debug!("Resolving query");
105    let result = resolve(backend.as_ref(), &query, &filter).await?;
106    log::debug!("Resolution complete");
107
108    // Display the result
109    output::display(result, &filter)?;
110
111    Ok(())
112}