cargo_hoist/
cli.rs

1//! CLI Logic
2
3use crate::registry::HoistRegistry;
4use anyhow::Result;
5use clap::{ArgAction, Parser, Subcommand};
6
7#[derive(Debug, Parser)]
8#[clap(name = "cargo-hoist", author, bin_name = "cargo", version)]
9enum Cargo {
10    #[clap(alias = "h")]
11    Hoist(Args),
12}
13
14/// Command line arguments
15#[derive(Parser, Debug)]
16#[command(author, version, about, long_about = None)]
17pub struct Args {
18    /// Global options
19    #[clap(flatten)]
20    pub globals: GlobalOpts,
21
22    /// The cargo-hoist subcommand
23    #[clap(subcommand)]
24    pub command: Option<Command>,
25}
26
27/// Global Config Options
28#[derive(Debug, clap::Args)]
29pub struct GlobalOpts {
30    /// Verbosity level (0-4). Default: 0 (ERROR).
31    #[arg(long, short, action = ArgAction::Count, default_value = "0")]
32    pub verbosity: u8,
33
34    /// Suppresses standard output.
35    #[arg(long, short)]
36    pub quiet: bool,
37}
38
39/// Subcommands
40#[derive(Subcommand, Debug)]
41pub enum Command {
42    /// Hoist dependencies
43    Hoist {
44        /// An optional list of binaries to bring into scope from the hoist toml registry
45        bins: Option<Vec<String>>,
46
47        /// Binary list flag. Merged ad de-duplicated with any binaries provided in the inline
48        /// argument.
49        #[clap(short, long)]
50        binaries: Option<Vec<String>>,
51    },
52    /// List registered dependencies.
53    List,
54    /// Search for a binary in the hoist toml registry.
55    #[clap(alias = "find")]
56    Search {
57        /// The binary to search for in the hoist toml registry.
58        binary: String,
59    },
60    /// Nuke wipes the hoist toml registry.
61    Nuke,
62    /// Registers a binary in the global hoist toml registry
63    #[clap(alias = "install")]
64    Register {
65        /// An optional list of binaries to install in the hoist toml registry
66        bins: Option<Vec<String>>,
67
68        /// Binary list flag. Merged ad de-duplicated with any binaries provided in the inline
69        /// argument.
70        #[clap(short, long)]
71        binaries: Option<Vec<String>>,
72    },
73}
74
75/// Run the main hoist command
76pub fn run() -> Result<()> {
77    let Cargo::Hoist(arg) = Cargo::parse();
78
79    crate::telemetry::init_tracing_subscriber(arg.globals.verbosity)?;
80
81    HoistRegistry::create_pre_hook(true, false)?;
82
83    let res = match arg.command {
84        None => HoistRegistry::install(None, Vec::new(), arg.globals.quiet),
85        Some(c) => match c {
86            Command::Hoist { binaries, bins } => HoistRegistry::hoist(
87                crate::utils::merge_and_dedup_vecs(binaries, bins),
88                arg.globals.quiet,
89            ),
90            Command::Search { binary } => HoistRegistry::find(binary),
91            Command::List => HoistRegistry::list(false),
92            Command::Register { binaries, bins } => HoistRegistry::install(
93                None,
94                crate::utils::merge_and_dedup_vecs(binaries, bins),
95                arg.globals.quiet,
96            ),
97            Command::Nuke => HoistRegistry::nuke(false),
98        },
99    };
100    if let Err(e) = res {
101        if !arg.globals.quiet {
102            eprintln!("Error: {e:?}");
103        }
104        std::process::exit(1);
105    }
106    Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111    use assert_cmd::Command;
112    use rand::{distributions::Alphanumeric, Rng};
113    use serial_test::serial;
114    use std::path::PathBuf;
115    use tempfile::TempDir;
116
117    const HOIST_BIN: &str = "cargo-hoist";
118
119    #[test]
120    #[serial]
121    fn test_cli_no_args() {
122        let tempdir = tempfile::tempdir().unwrap();
123        let _ = setup_test_dir(&tempdir);
124        let mut cmd = Command::cargo_bin(HOIST_BIN).unwrap();
125        let assert = cmd.arg("hoist").assert();
126        assert.success().stdout("");
127    }
128
129    #[test]
130    #[serial]
131    fn test_cli_nuke() {
132        let tempdir = tempfile::tempdir().unwrap();
133        let _ = setup_test_dir(&tempdir);
134        let mut cmd = Command::cargo_bin(HOIST_BIN).unwrap();
135        cmd.arg("hoist").arg("nuke").assert().success().stdout("");
136    }
137
138    #[test]
139    #[serial]
140    fn test_cli_install() {
141        let tempdir = tempfile::tempdir().unwrap();
142        let _ = setup_test_dir(&tempdir);
143        let mut cmd = Command::cargo_bin(HOIST_BIN).unwrap();
144        cmd.arg("hoist")
145            .arg("install")
146            .assert()
147            .success()
148            .stdout("");
149    }
150
151    #[test]
152    #[serial]
153    fn test_cli_list() {
154        let tempdir = tempfile::tempdir().unwrap();
155        let _ = setup_test_dir(&tempdir);
156        let mut cmd = Command::cargo_bin(HOIST_BIN).unwrap();
157        cmd.arg("hoist").arg("list").assert().success();
158    }
159
160    #[test]
161    #[serial]
162    fn test_cli_unrecognized_subcommand() {
163        let tempdir = tempfile::tempdir().unwrap();
164        let _ = setup_test_dir(&tempdir);
165        let mut cmd = Command::cargo_bin(HOIST_BIN).unwrap();
166        let assert = cmd.arg("hoist").arg("foobar").assert();
167        assert.failure().code(2).stderr(
168            r#"error: unrecognized subcommand 'foobar'
169
170Usage: cargo hoist [OPTIONS] [COMMAND]
171
172For more information, try '--help'.
173"#,
174        );
175    }
176
177    /// Helper function to setup a batteries included [TempDir].
178    fn setup_test_dir(tempdir: &TempDir) -> PathBuf {
179        // Create the test tempdir
180        let s: String = rand::thread_rng()
181            .sample_iter(&Alphanumeric)
182            .take(7)
183            .map(char::from)
184            .collect();
185        let test_tempdir = tempdir.path().join(s);
186        std::fs::create_dir(&test_tempdir).unwrap();
187
188        // Try to copy the cargo-hoist binary from the target/debug/
189        // directory, falling back to a manual install if not present.
190        let backup = match std::env::current_dir() {
191            Ok(d) => {
192                if d.join("target/debug/cargo-hoist").exists() {
193                    std::fs::copy(
194                        d.join("target/debug/cargo-hoist"),
195                        test_tempdir.join("cargo-hoist"),
196                    )
197                    .unwrap();
198                    false
199                } else {
200                    true
201                }
202            }
203            Err(_) => true,
204        };
205        if backup {
206            let _ = std::process::Command::new("cargo")
207                .args(["install", "--path", "."])
208                .current_dir(&test_tempdir)
209                .output()
210                .unwrap();
211        }
212
213        // Set the current directory to the test tempdir and return the
214        // test tempdir and the tempdir.
215        std::env::set_current_dir(&test_tempdir).unwrap();
216
217        test_tempdir
218    }
219}