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