asimov_cli/commands/module/
install.rs1use 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}