1use crate::Result;
4use asimov_env::paths::asimov_root;
5use asimov_module::{models::ModuleManifest, resolve::Resolver};
6use clientele::{Subcommand, SubcommandsProvider, SysexitsError::*};
7use miette::{IntoDiagnostic, miette};
8
9pub(crate) fn build_resolver(pattern: &str) -> miette::Result<Resolver> {
10 let mut resolver = Resolver::new();
11
12 let module_dir_path = asimov_root().join("modules");
13 let module_dir = std::fs::read_dir(&module_dir_path)
14 .map_err(|e| miette!("Failed to read module manifest directory: {e}"))?
15 .filter_map(Result::ok);
16
17 for entry in module_dir {
18 let filename = entry.file_name();
19 let filename = filename.to_string_lossy();
20 if !filename.ends_with(".yml") && !filename.ends_with(".yaml") {
21 continue;
22 }
23 let file = std::fs::File::open(entry.path()).into_diagnostic()?;
24
25 let manifest: ModuleManifest = serde_yml::from_reader(file).map_err(|e| {
26 miette!(
27 "Invalid module manifest at `{}`: {}",
28 entry.path().display(),
29 e
30 )
31 })?;
32
33 if !manifest
34 .provides
35 .programs
36 .iter()
37 .any(|program| program.split('-').next_back().is_some_and(|p| p == pattern))
38 {
39 continue;
40 }
41
42 resolver
43 .insert_manifest(&manifest)
44 .map_err(|e| miette!("{e}"))?;
45 }
46
47 Ok(resolver)
48}
49
50pub fn locate_subcommand(name: &str) -> Result<Subcommand> {
52 let libexec = asimov_root().join("libexec");
53 if libexec.exists() {
54 let file = std::fs::read_dir(libexec)?
55 .filter_map(Result::ok)
56 .find(|entry| {
57 entry.file_name().to_str().is_some_and(|filename| {
58 filename.starts_with("asimov-") && filename.ends_with(name)
59 })
60 });
61 if let Some(entry) = file {
62 return Ok(Subcommand {
63 name: format!("asimov-{}", name),
64 path: entry.path(),
65 });
66 }
67 }
68
69 match SubcommandsProvider::find("asimov-", name) {
70 Some(cmd) => Ok(cmd),
71 None => {
72 eprintln!("{}: command not found: {}{}", "asimov", "asimov-", name);
73 Err(EX_UNAVAILABLE)
74 }
75 }
76}
77
78pub fn normalize_url(url: &str) -> String {
79 if let Ok(url) = <url::Url>::parse(url) {
81 return url.to_string();
82 };
83
84 let url = url
88 .strip_prefix("~/")
89 .map(|path| {
90 std::env::home_dir()
91 .expect("unable to determine home directory")
92 .join(path)
93 })
94 .unwrap_or_else(|| std::path::PathBuf::from(url));
95
96 if let Ok(path) = std::path::Path::new(&url)
102 .canonicalize()
103 .map_err(|_| ())
104 .and_then(url::Url::from_file_path)
105 {
106 return path.to_string();
107 };
108
109 if let Ok(path) = std::path::absolute(&url)
112 .map_err(|_| ())
113 .and_then(url::Url::from_file_path)
114 {
115 return path.to_string();
116 }
117
118 if let Ok(path) = url::Url::from_file_path(std::path::Path::new(&url)) {
127 return path.to_string();
128 }
129
130 format!("file://{}", url.display())
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn url_normalization() {
141 let cases = [
142 ("https://example.org/", "https://example.org/"),
143 ("near://testnet/123456789", "near://testnet/123456789"),
144 ("/file with spaces.txt", "file:///file%20with%20spaces.txt"),
145 ("/file+with+pluses.txt", "file:///file+with+pluses.txt"),
146 ];
147
148 for case in cases {
149 assert_eq!(normalize_url(case.0), case.1, "input: {:?}", case.0);
150 }
151
152 #[cfg(unix)]
153 {
154 unsafe { std::env::set_var("HOME", "/home/user") };
155
156 let input = "~/path/to/file.txt";
157 let want = "file:///home/user/path/to/file.txt";
158
159 assert_eq!(
160 normalize_url(input),
161 want,
162 "home directory should be expanded, input: {:?}",
163 input
164 );
165 }
166
167 let cur_dir = std::env::current_dir().unwrap().display().to_string();
168
169 let input = "path/to/file.txt";
170 let want = "file://".to_string() + &cur_dir + "/path/to/file.txt";
171 assert_eq!(
172 normalize_url(input),
173 want,
174 "relative path should be get added after current directory, input: {:?}",
175 input
176 );
177
178 let input = "../path/./file.txt";
179 let want = "file://".to_string() + &cur_dir + "/../path/file.txt";
180 assert_eq!(
181 normalize_url(input),
182 want,
183 "relative path should be get added after current directory, input: {:?}",
184 input
185 );
186
187 let input = "another-type-of-a-string";
188 let want = "file://".to_string() + &cur_dir + "/another-type-of-a-string";
189 assert_eq!(
190 normalize_url(input),
191 want,
192 "non-path-looking input should be treated as a file in current directory, input: {:?}",
193 input
194 );
195
196 let input = "hello\\ world!";
197 let want = "file://".to_string() + &cur_dir + "/hello%5C%20world!";
198 assert_eq!(
199 normalize_url(input),
200 want,
201 "output should be url encoded, input: {:?}",
202 input
203 );
204 }
205}