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
use color_eyre::eyre::{eyre, Result};
use url::Url;

use crate::cli::command::Command;
use crate::config::Config;
use crate::output::Output;
use crate::plugins::{unalias_plugin, ExternalPlugin, Plugin, PluginName};
use crate::tool::Tool;
use crate::toolset::ToolsetBuilder;
use crate::ui::multi_progress_report::MultiProgressReport;

/// Install a plugin
///
/// note that rtx automatically can install plugins when you install a tool
/// e.g.: `rtx install node@20` will autoinstall the node plugin
///
/// This behavior can be modified in ~/.config/rtx/config.toml
#[derive(Debug, clap::Args)]
#[clap(visible_aliases = ["i", "a"], alias = "add", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct PluginsInstall {
    /// The name of the plugin to install
    /// e.g.: node, ruby
    /// Can specify multiple plugins: `rtx plugins install node ruby python`
    #[clap(required_unless_present = "all", verbatim_doc_comment)]
    name: Option<String>,

    /// The git url of the plugin
    /// e.g.: https://github.com/asdf-vm/asdf-node.git
    #[clap(help = "The git url of the plugin", value_hint = clap::ValueHint::Url, verbatim_doc_comment)]
    git_url: Option<String>,

    /// Reinstall even if plugin exists
    #[clap(short, long, verbatim_doc_comment)]
    force: bool,

    /// Install all missing plugins
    /// This will only install plugins that have matching shorthands.
    /// i.e.: they don't need the full git repo url
    #[clap(short, long, conflicts_with_all = ["name", "force"], verbatim_doc_comment)]
    all: bool,

    /// Show installation output
    #[clap(long, short, action = clap::ArgAction::Count, verbatim_doc_comment)]
    verbose: u8,

    #[clap(hide = true)]
    rest: Vec<String>,
}

impl Command for PluginsInstall {
    fn run(self, mut config: Config, _out: &mut Output) -> Result<()> {
        let mpr = MultiProgressReport::new(config.show_progress_bars());
        if self.all {
            return self.install_all_missing_plugins(config, mpr);
        }
        let (name, git_url) = get_name_and_url(&self.name.clone().unwrap(), &self.git_url)?;
        if git_url.is_some() {
            self.install_one(&mut config, name, git_url, &mpr)?;
        } else {
            let mut plugins: Vec<PluginName> = vec![name];
            if let Some(second) = self.git_url.clone() {
                plugins.push(second);
            };
            plugins.extend(self.rest.clone());
            self.install_many(config, plugins, mpr)?;
        }

        Ok(())
    }
}

impl PluginsInstall {
    fn install_all_missing_plugins(
        &self,
        mut config: Config,
        mpr: MultiProgressReport,
    ) -> Result<()> {
        let ts = ToolsetBuilder::new().build(&mut config)?;
        let missing_plugins = ts.list_missing_plugins(&mut config);
        if missing_plugins.is_empty() {
            warn!("all plugins already installed");
        }
        self.install_many(config, missing_plugins, mpr)?;
        Ok(())
    }

    fn install_many(
        &self,
        mut config: Config,
        plugins: Vec<PluginName>,
        mpr: MultiProgressReport,
    ) -> Result<()> {
        for plugin in plugins {
            self.install_one(&mut config, plugin, None, &mpr)?;
        }
        Ok(())
        // TODO: run in parallel
        // ThreadPoolBuilder::new()
        //     .num_threads(config.settings.jobs)
        //     .build()?
        //     .install(|| -> Result<()> {
        //         plugins
        //             .into_par_iter()
        //             .map(|plugin| self.install_one(&mut config, plugin, None, &mpr))
        //             .collect::<Result<Vec<_>>>()?;
        //         Ok(())
        //     })
    }

    fn install_one(
        &self,
        config: &mut Config,
        name: PluginName,
        git_url: Option<String>,
        mpr: &MultiProgressReport,
    ) -> Result<()> {
        let mut plugin = ExternalPlugin::new(name.clone());
        plugin.repo_url = git_url;
        if !self.force && plugin.is_installed() {
            mpr.warn(format!("Plugin {} already installed", name));
            mpr.warn("Use --force to install anyway".to_string());
        } else {
            let tool = Tool::new(plugin.name.clone(), Box::new(plugin));
            tool.ensure_installed(config, Some(mpr), true)?;
        }
        Ok(())
    }
}

fn get_name_and_url(name: &str, git_url: &Option<String>) -> Result<(String, Option<String>)> {
    let name = unalias_plugin(name);
    Ok(match git_url {
        Some(url) => match url.contains("://") {
            true => (name.to_string(), Some(url.clone())),
            false => (name.to_string(), None),
        },
        None => match name.contains("://") {
            true => (get_name_from_url(name)?, Some(name.to_string())),
            false => (name.to_string(), None),
        },
    })
}

fn get_name_from_url(url: &str) -> Result<String> {
    if let Ok(url) = Url::parse(url) {
        if let Some(segments) = url.path_segments() {
            let last = segments.last().unwrap_or_default();
            let name = last.strip_prefix("asdf-").unwrap_or(last);
            let name = name.strip_prefix("rtx-").unwrap_or(name);
            let name = name.strip_suffix(".git").unwrap_or(name);
            return Ok(unalias_plugin(name).to_string());
        }
    }
    Err(eyre!("could not infer plugin name from url: {}", url))
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>
  # install the node via shorthand
  $ <bold>rtx plugins install node</bold>

  # install the node plugin using a specific git url
  $ <bold>rtx plugins install node https://github.com/rtx-plugins/rtx-nodejs.git</bold>

  # install the node plugin using the git url only
  # (node is inferred from the url)
  $ <bold>rtx plugins install https://github.com/rtx-plugins/rtx-nodejs.git</bold>

  # install the node plugin using a specific ref
  $ <bold>rtx plugins install node https://github.com/rtx-plugins/rtx-nodejs.git#v1.0.0</bold>
"#
);

#[cfg(test)]
mod tests {
    use insta::assert_display_snapshot;

    use crate::cli::tests::cli_run;

    #[test]
    fn test_plugin_install_invalid_url() {
        let args = ["rtx", "plugin", "add", "tiny:"].map(String::from).into();
        let err = cli_run(&args).unwrap_err();
        assert_display_snapshot!(err);
    }
}