algocline_app/service/pkg/
list.rs1use std::collections::HashMap;
4use std::path::Path;
5
6use super::super::lockfile::{load_lockfile, lockfile_path};
7use super::super::manifest;
8use super::super::project::resolve_project_root;
9use super::super::resolve::is_system_package;
10use super::super::source::{infer_from_legacy_source_string, PackageSource};
11use super::super::AppService;
12
13#[derive(Debug)]
16enum Scope {
17 Project,
18 Global,
19}
20
21#[derive(Debug)]
25struct PackageListEntry {
26 name: String,
27 scope: Scope,
28 source_type: Option<String>,
30 path: Option<String>,
32 source: Option<String>,
34 active: bool,
35 linked_at: Option<String>,
36 installed_at: Option<String>,
37 updated_at: Option<String>,
38 install_source: Option<String>,
40 overrides: Option<Vec<String>>,
41 meta: serde_json::Value,
42 error: Option<String>,
43}
44
45impl PackageListEntry {
46 fn into_json(self) -> serde_json::Value {
47 let scope_str = match self.scope {
48 Scope::Project => "project",
49 Scope::Global => "global",
50 };
51
52 let mut map = serde_json::Map::new();
53 map.insert("name".to_string(), serde_json::Value::String(self.name));
54 map.insert(
55 "scope".to_string(),
56 serde_json::Value::String(scope_str.to_string()),
57 );
58
59 if let Some(st) = self.source_type {
61 map.insert("source_type".to_string(), serde_json::Value::String(st));
62 }
63
64 if let Some(p) = self.path {
65 map.insert("path".to_string(), serde_json::Value::String(p));
66 }
67 if let Some(s) = self.source {
68 map.insert("source".to_string(), serde_json::Value::String(s));
69 }
70
71 map.insert("active".to_string(), serde_json::Value::Bool(self.active));
72
73 if let Some(la) = self.linked_at {
74 map.insert("linked_at".to_string(), serde_json::Value::String(la));
75 }
76 if let Some(ia) = self.installed_at {
77 map.insert("installed_at".to_string(), serde_json::Value::String(ia));
78 }
79 if let Some(ua) = self.updated_at {
80 map.insert("updated_at".to_string(), serde_json::Value::String(ua));
81 }
82 if let Some(is) = self.install_source {
83 map.insert("install_source".to_string(), serde_json::Value::String(is));
84 }
85 if let Some(ov) = self.overrides {
86 map.insert("overrides".to_string(), serde_json::json!(ov));
87 }
88
89 if let serde_json::Value::Object(meta_map) = self.meta {
91 for (k, v) in meta_map {
92 map.entry(k).or_insert(v);
94 }
95 }
96
97 if let Some(err) = self.error {
98 map.insert("error".to_string(), serde_json::Value::String(err));
99 }
100
101 serde_json::Value::Object(map)
102 }
103}
104
105impl AppService {
106 pub async fn pkg_list(&self, project_root: Option<String>) -> Result<String, String> {
114 let manifest_data = manifest::load_manifest().unwrap_or_default();
116
117 let resolved_root = resolve_project_root(project_root.as_deref());
119
120 let mut project_names: std::collections::HashSet<String> = std::collections::HashSet::new();
121 let mut entries: Vec<PackageListEntry> = Vec::new();
122 let mut project_root_str: Option<String> = None;
123 let mut lockfile_path_str: Option<String> = None;
124
125 if let Some(ref root) = resolved_root {
126 project_root_str = Some(root.display().to_string());
127 lockfile_path_str = Some(lockfile_path(root).display().to_string());
128
129 match load_lockfile(root) {
130 Ok(Some(lock)) => {
131 for pkg in &lock.packages {
132 let PackageSource::LocalDir { path: ref raw_path } = pkg.source else {
133 continue;
134 };
135 let abs_path = {
136 let p = Path::new(raw_path);
137 if p.is_absolute() {
138 p.to_path_buf()
139 } else {
140 root.join(p)
141 }
142 };
143
144 project_names.insert(pkg.name.clone());
145 entries.push(PackageListEntry {
146 name: pkg.name.clone(),
147 scope: Scope::Project,
148 source_type: Some("local_dir".to_string()),
149 path: Some(abs_path.display().to_string()),
150 source: None,
151 active: true,
152 linked_at: Some(pkg.linked_at.clone()),
153 installed_at: None,
154 updated_at: None,
155 install_source: None,
156 overrides: None,
157 meta: serde_json::Value::Object(serde_json::Map::new()),
158 error: None,
159 });
160 }
161 }
162 Ok(None) => {}
163 Err(e) => {
164 tracing::warn!("failed to load alc.lock: {e}");
165 }
166 }
167 }
168
169 let mut seen: HashMap<String, Vec<(usize, String)>> = HashMap::new();
172 let global_start_idx = entries.len();
174
175 for (idx, sp) in self.search_paths.iter().enumerate() {
176 if !sp.path.is_dir() {
177 continue;
178 }
179 let read_entries = match std::fs::read_dir(&sp.path) {
180 Ok(e) => e,
181 Err(_) => continue,
182 };
183
184 for dir_entry in read_entries.flatten() {
185 let path = dir_entry.path();
186 if !path.is_dir() {
187 continue;
188 }
189 if !path.join("init.lua").exists() {
190 continue;
191 }
192 let name = dir_entry.file_name().to_string_lossy().to_string();
193 if is_system_package(&name) {
194 continue;
195 }
196
197 let source_display = sp.path.display().to_string();
198 seen.entry(name.clone())
199 .or_default()
200 .push((idx, source_display.clone()));
201
202 let global_active = seen[&name].len() == 1 && !project_names.contains(&name);
205
206 let (meta, eval_error) = if is_safe_pkg_name(&name) {
213 let code = format!(
214 r#"package.loaded["{name}"] = nil
215local pkg = require("{name}")
216return pkg.meta or {{ name = "{name}" }}"#
217 );
218 match self.executor.eval_simple(code).await {
219 Ok(v) => (v, None),
220 Err(_) => (
221 serde_json::Value::Object(serde_json::Map::new()),
222 Some("failed to load meta".to_string()),
223 ),
224 }
225 } else {
226 (
227 serde_json::Value::Object(serde_json::Map::new()),
228 Some("invalid package name".to_string()),
229 )
230 };
231
232 let (source_type, installed_at, updated_at, install_source) =
234 if let Some(entry) = manifest_data.packages.get(&name) {
235 let st = match infer_from_legacy_source_string(&entry.source) {
236 PackageSource::Git { .. } => "git",
237 PackageSource::LocalCopy { .. } => "local_copy",
238 PackageSource::LocalDir { .. } => "local_dir",
239 PackageSource::Bundled { .. } => "bundled",
240 };
241 (
242 Some(st.to_string()),
243 Some(entry.installed_at.clone()),
244 Some(entry.updated_at.clone()),
245 Some(entry.source.clone()),
246 )
247 } else {
248 (None, None, None, None)
250 };
251
252 entries.push(PackageListEntry {
253 name,
254 scope: Scope::Global,
255 source_type,
256 path: None,
257 source: Some(source_display),
258 active: global_active,
259 linked_at: None,
260 installed_at,
261 updated_at,
262 install_source,
263 overrides: None,
264 meta,
265 error: eval_error,
266 });
267 }
268 }
269
270 for entry in entries[global_start_idx..].iter_mut() {
274 if !entry.active {
275 continue;
276 }
277 if let Some(occurrences) = seen.get(&entry.name) {
278 if occurrences.len() > 1 {
279 entry.overrides =
280 Some(occurrences.iter().skip(1).map(|(_, s)| s.clone()).collect());
281 }
282 }
283 }
284
285 let all_packages: Vec<serde_json::Value> =
287 entries.into_iter().map(|e| e.into_json()).collect();
288
289 let search_paths_json: Vec<serde_json::Value> = self
290 .search_paths
291 .iter()
292 .map(|sp| {
293 serde_json::json!({
294 "path": sp.path.display().to_string(),
295 "source": sp.source.to_string(),
296 })
297 })
298 .collect();
299
300 let mut result = serde_json::json!({
301 "packages": all_packages,
302 "search_paths": search_paths_json,
303 });
304
305 if let Some(root_str) = project_root_str {
306 result["project_root"] = serde_json::Value::String(root_str);
307 }
308 if let Some(lp) = lockfile_path_str {
309 result["lockfile_path"] = serde_json::Value::String(lp);
310 }
311
312 Ok(result.to_string())
313 }
314}
315
316fn is_safe_pkg_name(name: &str) -> bool {
324 !name.is_empty()
325 && name
326 .bytes()
327 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
328}