algocline_app/service/pkg/
install.rs1use std::path::Path;
4
5use super::super::manifest;
6use super::super::path::{copy_dir, ContainedPath};
7use super::super::resolve::{
8 install_scenarios_from_dir, packages_dir, scenarios_dir, DirEntryFailures, AUTO_INSTALL_SOURCES,
9};
10use super::super::AppService;
11
12impl AppService {
13 pub async fn pkg_install(&self, url: String, name: Option<String>) -> Result<String, String> {
15 let pkg_dir = packages_dir()?;
16 let _ = std::fs::create_dir_all(&pkg_dir);
17
18 let local_path = Path::new(&url);
20 if local_path.is_absolute() && local_path.is_dir() {
21 return self.install_from_local_path(local_path, &pkg_dir, name);
22 }
23
24 let git_url = if url.starts_with("http://")
26 || url.starts_with("https://")
27 || url.starts_with("file://")
28 || url.starts_with("git@")
29 {
30 url.clone()
31 } else {
32 format!("https://{url}")
33 };
34
35 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
37
38 let output = tokio::process::Command::new("git")
39 .args([
40 "clone",
41 "--depth",
42 "1",
43 &git_url,
44 &staging.path().to_string_lossy(),
45 ])
46 .output()
47 .await
48 .map_err(|e| format!("Failed to run git: {e}"))?;
49
50 if !output.status.success() {
51 let stderr = String::from_utf8_lossy(&output.stderr);
52 return Err(format!("git clone failed: {stderr}"));
53 }
54
55 let _ = std::fs::remove_dir_all(staging.path().join(".git"));
57
58 if staging.path().join("init.lua").exists() {
60 let name = name.unwrap_or_else(|| {
62 url.trim_end_matches('/')
63 .rsplit('/')
64 .next()
65 .unwrap_or("unknown")
66 .trim_end_matches(".git")
67 .to_string()
68 });
69
70 let dest = ContainedPath::child(&pkg_dir, &name)?;
71 if dest.as_ref().exists() {
72 return Err(format!(
73 "Package '{name}' already exists at {}. Remove it first.",
74 dest.as_ref().display()
75 ));
76 }
77
78 copy_dir(staging.path(), dest.as_ref())
79 .map_err(|e| format!("Failed to copy package: {e}"))?;
80
81 let _ = manifest::record_install(&name, None, &url);
83
84 let mut response = serde_json::json!({
85 "installed": [name],
86 "mode": "single",
87 });
88 if let Some(tp) = super::super::resolve::types_stub_path() {
89 response["types_path"] = serde_json::Value::String(tp);
90 }
91 Ok(response.to_string())
92 } else {
93 if name.is_some() {
95 return Err(
97 "The 'name' parameter is only supported for single-package repos (init.lua at root). \
98 This repository is a collection (subdirs with init.lua)."
99 .to_string(),
100 );
101 }
102
103 let mut installed = Vec::new();
104 let mut skipped = Vec::new();
105
106 let entries = std::fs::read_dir(staging.path())
107 .map_err(|e| format!("Failed to read staging dir: {e}"))?;
108
109 for entry in entries {
110 let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
111 let path = entry.path();
112 if !path.is_dir() {
113 continue;
114 }
115 if !path.join("init.lua").exists() {
116 continue;
117 }
118 let pkg_name = entry.file_name().to_string_lossy().to_string();
119 let dest = pkg_dir.join(&pkg_name);
120 if dest.exists() {
121 skipped.push(pkg_name);
122 continue;
123 }
124 copy_dir(&path, &dest)
125 .map_err(|e| format!("Failed to copy package '{pkg_name}': {e}"))?;
126 installed.push(pkg_name);
127 }
128
129 let scenarios_subdir = staging.path().join("scenarios");
133 let mut scenarios_installed: Vec<String> = Vec::new();
134 let mut scenarios_failures: DirEntryFailures = Vec::new();
135 if scenarios_subdir.is_dir() {
136 if let Ok(sc_dir) = scenarios_dir() {
137 std::fs::create_dir_all(&sc_dir)
138 .map_err(|e| format!("Failed to create scenarios dir: {e}"))?;
139 if let Ok(result) = install_scenarios_from_dir(&scenarios_subdir, &sc_dir) {
140 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&result) {
141 if let Some(arr) = parsed.get("installed").and_then(|v| v.as_array()) {
142 scenarios_installed = arr
143 .iter()
144 .filter_map(|v| v.as_str().map(String::from))
145 .collect();
146 }
147 if let Some(arr) = parsed.get("failures").and_then(|v| v.as_array()) {
148 scenarios_failures = arr
149 .iter()
150 .filter_map(|v| v.as_str().map(String::from))
151 .collect();
152 }
153 }
154 }
155 }
156 }
157
158 if installed.is_empty() && skipped.is_empty() {
159 return Err(
160 "No packages found. Expected init.lua at root (single) or */init.lua (collection)."
161 .to_string(),
162 );
163 }
164
165 let _ = manifest::record_install_batch(&installed, &url);
167
168 let mut response = serde_json::json!({
169 "installed": installed,
170 "skipped": skipped,
171 "scenarios_installed": scenarios_installed,
172 "scenarios_failures": scenarios_failures,
173 "mode": "collection",
174 });
175 if let Some(tp) = super::super::resolve::types_stub_path() {
176 response["types_path"] = serde_json::Value::String(tp);
177 }
178 Ok(response.to_string())
179 }
180 }
181
182 fn install_from_local_path(
184 &self,
185 source: &Path,
186 pkg_dir: &Path,
187 name: Option<String>,
188 ) -> Result<String, String> {
189 if source.join("init.lua").exists() {
190 let name = name.unwrap_or_else(|| {
192 source
193 .file_name()
194 .map(|n| n.to_string_lossy().to_string())
195 .unwrap_or_else(|| "unknown".to_string())
196 });
197
198 let dest = ContainedPath::child(pkg_dir, &name)?;
199 if dest.as_ref().exists() {
200 let _ = std::fs::remove_dir_all(&dest);
202 }
203
204 copy_dir(source, dest.as_ref()).map_err(|e| format!("Failed to copy package: {e}"))?;
205 let _ = std::fs::remove_dir_all(dest.as_ref().join(".git"));
207
208 let _ = manifest::record_install(&name, None, &source.display().to_string());
210
211 let mut response = serde_json::json!({
212 "installed": [name],
213 "mode": "local_single",
214 });
215 if let Some(tp) = super::super::resolve::types_stub_path() {
216 response["types_path"] = serde_json::Value::String(tp);
217 }
218 Ok(response.to_string())
219 } else {
220 if name.is_some() {
222 return Err(
223 "The 'name' parameter is only supported for single-package dirs (init.lua at root)."
224 .to_string(),
225 );
226 }
227
228 let mut installed = Vec::new();
229 let mut updated = Vec::new();
230
231 let entries =
232 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
233
234 for entry in entries {
235 let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
236 let path = entry.path();
237 if !path.is_dir() || !path.join("init.lua").exists() {
238 continue;
239 }
240 let pkg_name = entry.file_name().to_string_lossy().to_string();
241 let dest = pkg_dir.join(&pkg_name);
242 let existed = dest.exists();
243 if existed {
244 let _ = std::fs::remove_dir_all(&dest);
245 }
246 copy_dir(&path, &dest)
247 .map_err(|e| format!("Failed to copy package '{pkg_name}': {e}"))?;
248 let _ = std::fs::remove_dir_all(dest.join(".git"));
249 if existed {
250 updated.push(pkg_name);
251 } else {
252 installed.push(pkg_name);
253 }
254 }
255
256 if installed.is_empty() && updated.is_empty() {
257 return Err(
258 "No packages found. Expected init.lua at root (single) or */init.lua (collection)."
259 .to_string(),
260 );
261 }
262
263 let source_str = source.display().to_string();
265 let all_names: Vec<String> = installed.iter().chain(updated.iter()).cloned().collect();
266 let _ = manifest::record_install_batch(&all_names, &source_str);
267
268 let mut response = serde_json::json!({
269 "installed": installed,
270 "updated": updated,
271 "mode": "local_collection",
272 });
273 if let Some(tp) = super::super::resolve::types_stub_path() {
274 response["types_path"] = serde_json::Value::String(tp);
275 }
276 Ok(response.to_string())
277 }
278 }
279
280 pub(in crate::service) async fn auto_install_bundled_packages(&self) -> Result<(), String> {
282 let mut errors: Vec<String> = Vec::new();
283 for url in AUTO_INSTALL_SOURCES {
284 tracing::info!("auto-installing from {url}");
285 if let Err(e) = self.pkg_install(url.to_string(), None).await {
286 tracing::warn!("failed to auto-install from {url}: {e}");
287 errors.push(format!("{url}: {e}"));
288 }
289 }
290 if errors.len() == AUTO_INSTALL_SOURCES.len() {
292 return Err(format!(
293 "Failed to auto-install bundled packages: {}",
294 errors.join("; ")
295 ));
296 }
297 Ok(())
298 }
299}