Skip to main content

asimov_cli/commands/module/
install.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{BoxError, StandardOptions, SysexitsError::*};
4use asimov_installer::InstallOptions;
5use asimov_module::{ModuleManifest, ModuleName, ReadVarError};
6use color_print::{ceprintln, cprintln};
7
8pub async fn install(
9    module_names: Vec<ModuleName>,
10    version: Option<String>,
11    model_size: Option<String>,
12    flags: &StandardOptions,
13) -> Result<(), BoxError> {
14    let registry = asimov_registry::Registry::default();
15    let installer = asimov_installer::Installer::default();
16
17    let install_options = InstallOptions::builder()
18        .maybe_version(version.clone())
19        .maybe_model_size(model_size.clone())
20        .build();
21
22    let module_names = if module_names.len() == 1 && module_names[0].as_str() == "all" {
23        fetch_all_module_names()
24            .await
25            .map_err(|e| {
26                tracing::error!("unable to fetch list of all modules: {e}");
27                EX_UNAVAILABLE
28            })?
29            .into_iter()
30            .map(ModuleName::try_from)
31            .collect::<Result<Vec<_>, _>>()?
32    } else {
33        module_names.to_vec()
34    };
35
36    for module_name in module_names {
37        if !registry
38            .is_module_installed(&module_name)
39            .await
40            .unwrap_or(false)
41        {
42            let target_version = if let Some(ref want) = version {
43                want.clone()
44            } else {
45                installer
46                    .fetch_latest_release(&module_name)
47                    .await
48                    .map_err(|e| {
49                        tracing::error!(
50                            "unable to find latest release for module `{module_name}`: {e}"
51                        );
52                        EX_UNAVAILABLE
53                    })?
54            };
55
56            if flags.verbose > 0 {
57                cprintln!(
58                    "<s,g>โœ“</> Found version <s>{target_version}</> for module <s>{module_name}</>."
59                );
60            }
61
62            if flags.verbose > 1 {
63                cprintln!("<s,c>ยป</> Installing module <s>{module_name}</>...");
64            }
65
66            installer
67                .install_module(&module_name, &install_options)
68                .await
69                .map_err(|e| {
70                    tracing::error!("failed to install module `{module_name}`: {e}");
71                    EX_UNAVAILABLE
72                })?;
73
74            if flags.verbose > 0 {
75                cprintln!("<s,g>โœ“</> Installed module <s>{module_name}</>.");
76            }
77        } else if flags.verbose > 0 {
78            cprintln!("<s,g>โœ“</> Module <s>{module_name}</> is already installed.");
79        }
80
81        if registry
82            .is_module_enabled(&module_name)
83            .await
84            .unwrap_or(false)
85        {
86            continue;
87        }
88
89        let manifest = registry.read_manifest(&module_name).await.map_err(|e| {
90            tracing::error!("failed to read module manifest for `{module_name}`: {e}");
91            EX_UNAVAILABLE
92        })?;
93
94        let variables = manifest
95            .manifest
96            .config
97            .iter()
98            .flat_map(|conf| conf.variables.iter());
99
100        let mut missing_variables = Vec::new();
101        for var in variables {
102            if !var.is_required() {
103                continue;
104            }
105            match manifest.manifest.variable(&var.name, None) {
106                Ok(_) => (),
107                Err(ReadVarError::UnconfiguredVar(_)) => {
108                    missing_variables.push(var);
109                },
110                Err(e) => {
111                    tracing::error!(
112                        "failed to read configuration variable `{}` for module `{module_name}`: {e}",
113                        var.name
114                    );
115                    return Err(EX_UNAVAILABLE.into());
116                },
117            }
118        }
119
120        if missing_variables.is_empty() {
121            registry.enable_module(&module_name).await.map_err(|e| {
122                tracing::error!("failed to enable installed module `{module_name}`: {e}");
123                EX_UNAVAILABLE
124            })?;
125        } else {
126            ceprintln!(
127                "<s,y>warn:</> Module <s>{module_name}</> wasn't enabled automatically due to missing configuration."
128            );
129            ceprintln!("<s,dim>hint:</> Module <s>{module_name}</> requires configuration:");
130
131            for var in missing_variables {
132                let desc_suffix = if let Some(ref desc) = var.description {
133                    format!(" (Description: \"{desc}\")")
134                } else {
135                    String::new()
136                };
137
138                ceprintln!(
139                    "<s,dim>hint:</>   Missing variable: <s>{}</s>{}",
140                    var.name,
141                    desc_suffix
142                );
143
144                if let Some(ref env) = var.environment {
145                    ceprintln!(
146                        "<s,dim>hint:</>   Alternative: set environment variable: <s>{env}</>"
147                    );
148                }
149            }
150
151            ceprintln!(
152                "<s,dim>hint:</>   To configure: <s>asimov module config setup {module_name}</s>"
153            );
154            ceprintln!("<s,dim>hint:</>   To enable: <s>asimov module enable {module_name}</s>");
155        }
156    }
157
158    Ok(())
159}
160
161pub async fn fetch_all_module_names() -> Result<Vec<String>, Box<dyn core::error::Error>> {
162    let url = "https://github.com/asimov-modules/asimov-modules/raw/master/all/.asimov/module.yaml";
163
164    let client = reqwest::Client::builder()
165        .user_agent("asimov-module-cli")
166        .connect_timeout(std::time::Duration::from_secs(10))
167        .read_timeout(std::time::Duration::from_secs(30))
168        .build()
169        .expect("Failed to build HTTP client");
170
171    let response = client
172        .get(url)
173        .send()
174        .await
175        .map_err(|e| format!("request failed: {e}"))?;
176
177    if !response.status().is_success() {
178        Err(format!(
179            "HTTP status code was not successful: {0}",
180            response.status()
181        ))?;
182    }
183
184    let content = response
185        .text()
186        .await
187        .inspect_err(|err| tracing::debug!(?err))?;
188
189    let manifest: ModuleManifest = serde_yml::from_str(&content)
190        .inspect_err(|err| tracing::debug!(?err, ?content))
191        .map_err(|e| format!("unable to deserialize GitHub response: {e}"))?;
192
193    Ok(manifest.requires.modules)
194}