1use std::collections::HashMap;
4use std::path::Path;
5
6use super::super::alc_toml::{self, load_alc_toml};
7use super::super::lockfile::{load_lockfile, lockfile_path};
8use super::super::manifest;
9use super::super::project::resolve_project_root;
10use super::super::resolve::is_system_package;
11use super::super::source::{infer_from_legacy_source_string, PackageSource};
12use super::super::AppService;
13
14#[derive(Debug)]
17enum Scope {
18 Project,
19 Global,
20}
21
22#[derive(Debug)]
26struct PackageListEntry {
27 name: String,
28 scope: Scope,
29 source_type: Option<String>,
31 path: Option<String>,
33 source: Option<String>,
35 active: bool,
36 version: Option<String>,
38 installed_at: Option<String>,
39 updated_at: Option<String>,
40 install_source: Option<String>,
42 overrides: Option<Vec<String>>,
43 meta: serde_json::Value,
44 error: Option<String>,
45 linked: Option<bool>,
47 link_target: Option<String>,
49 broken: Option<bool>,
51}
52
53impl PackageListEntry {
54 fn into_json(self) -> serde_json::Value {
55 let scope_str = match self.scope {
56 Scope::Project => "project",
57 Scope::Global => "global",
58 };
59
60 let mut map = serde_json::Map::new();
61 map.insert("name".to_string(), serde_json::Value::String(self.name));
62 map.insert(
63 "scope".to_string(),
64 serde_json::Value::String(scope_str.to_string()),
65 );
66
67 if let Some(st) = self.source_type {
69 map.insert("source_type".to_string(), serde_json::Value::String(st));
70 }
71
72 if let Some(p) = self.path {
73 map.insert("path".to_string(), serde_json::Value::String(p));
74 }
75 if let Some(s) = self.source {
76 map.insert("source".to_string(), serde_json::Value::String(s));
77 }
78
79 map.insert("active".to_string(), serde_json::Value::Bool(self.active));
80
81 if let Some(v) = self.version {
82 map.insert("version".to_string(), serde_json::Value::String(v));
83 }
84 if let Some(ia) = self.installed_at {
85 map.insert("installed_at".to_string(), serde_json::Value::String(ia));
86 }
87 if let Some(ua) = self.updated_at {
88 map.insert("updated_at".to_string(), serde_json::Value::String(ua));
89 }
90 if let Some(is) = self.install_source {
91 map.insert("install_source".to_string(), serde_json::Value::String(is));
92 }
93 if let Some(ov) = self.overrides {
94 map.insert("overrides".to_string(), serde_json::json!(ov));
95 }
96
97 if let serde_json::Value::Object(meta_map) = self.meta {
99 for (k, v) in meta_map {
100 map.entry(k).or_insert(v);
102 }
103 }
104
105 if let Some(err) = self.error {
106 map.insert("error".to_string(), serde_json::Value::String(err));
107 }
108
109 if let Some(linked) = self.linked {
110 map.insert("linked".to_string(), serde_json::Value::Bool(linked));
111 }
112 if let Some(target) = self.link_target {
113 map.insert("link_target".to_string(), serde_json::Value::String(target));
114 }
115 if let Some(broken) = self.broken {
116 map.insert("broken".to_string(), serde_json::Value::Bool(broken));
117 }
118
119 serde_json::Value::Object(map)
120 }
121}
122
123impl AppService {
124 pub async fn pkg_list(&self, project_root: Option<String>) -> Result<String, String> {
132 let manifest_data = manifest::load_manifest().unwrap_or_default();
134
135 let resolved_root = resolve_project_root(project_root.as_deref());
137
138 let mut project_names: std::collections::HashSet<String> = std::collections::HashSet::new();
139 let mut entries: Vec<PackageListEntry> = Vec::new();
140 let mut project_root_str: Option<String> = None;
141 let mut lockfile_path_str: Option<String> = None;
142
143 if let Some(ref root) = resolved_root {
144 project_root_str = Some(root.display().to_string());
145 lockfile_path_str = Some(lockfile_path(root).display().to_string());
146
147 let lock_map: HashMap<String, (Option<String>, PackageSource)> =
149 match load_lockfile(root) {
150 Ok(Some(lock)) => lock
151 .packages
152 .into_iter()
153 .map(|p| (p.name, (p.version, p.source)))
154 .collect(),
155 Ok(None) => HashMap::new(),
156 Err(e) => {
157 tracing::warn!("failed to load alc.lock: {e}");
158 HashMap::new()
159 }
160 };
161
162 match load_alc_toml(root) {
164 Ok(Some(alc_toml)) => {
165 for (name, dep) in &alc_toml.packages {
166 let (version, source_type, abs_path) =
167 resolve_project_pkg_info(name, dep, &lock_map, root);
168 project_names.insert(name.clone());
169 entries.push(make_project_entry(
170 name.clone(),
171 version,
172 source_type,
173 abs_path,
174 ));
175 }
176 }
177 Ok(None) => {
178 collect_path_entries_from_lock(
180 &lock_map,
181 root,
182 &mut project_names,
183 &mut entries,
184 );
185 }
186 Err(e) => {
187 tracing::warn!("failed to load alc.toml: {e}");
188 }
189 }
190 }
191
192 let mut seen: HashMap<String, Vec<(usize, String)>> = HashMap::new();
195 let global_start_idx = entries.len();
197
198 for (idx, sp) in self.search_paths.iter().enumerate() {
199 if !sp.path.is_dir() {
200 continue;
201 }
202 let read_entries = match std::fs::read_dir(&sp.path) {
203 Ok(e) => e,
204 Err(_) => continue,
205 };
206
207 for dir_entry in read_entries.flatten() {
208 let path = dir_entry.path();
209
210 let is_symlink = path
213 .symlink_metadata()
214 .map(|m| m.file_type().is_symlink())
215 .unwrap_or(false);
216
217 let link_target = if is_symlink {
218 path.read_link().ok().map(|t| t.display().to_string())
219 } else {
220 None
221 };
222
223 let broken = if is_symlink {
225 Some(!path.exists())
226 } else {
227 None
228 };
229
230 if !is_symlink && !path.is_dir() {
234 continue;
235 }
236
237 if broken != Some(true) && !path.join("init.lua").exists() {
239 continue;
240 }
241
242 let name = dir_entry.file_name().to_string_lossy().to_string();
243 if is_system_package(&name) {
244 continue;
245 }
246
247 let source_display = sp.path.display().to_string();
248 seen.entry(name.clone())
249 .or_default()
250 .push((idx, source_display.clone()));
251
252 let global_active = seen[&name].len() == 1 && !project_names.contains(&name);
255
256 let (meta, eval_error) = if is_safe_pkg_name(&name) {
258 let code = format!(
259 r#"package.loaded["{name}"] = nil
260local pkg = require("{name}")
261return pkg.meta or {{ name = "{name}" }}"#
262 );
263 match self.executor.eval_simple(code).await {
264 Ok(v) => (v, None),
265 Err(_) => (
266 serde_json::Value::Object(serde_json::Map::new()),
267 Some("failed to load meta".to_string()),
268 ),
269 }
270 } else {
271 (
272 serde_json::Value::Object(serde_json::Map::new()),
273 Some("invalid package name".to_string()),
274 )
275 };
276
277 let (source_type, installed_at, updated_at, install_source) =
279 if let Some(entry) = manifest_data.packages.get(&name) {
280 let inferred = infer_from_legacy_source_string(&entry.source);
281 let st = match &inferred {
282 PackageSource::Git { .. } => "git".to_string(),
283 PackageSource::Installed => {
284 format!("installed (from: {})", entry.source)
286 }
287 PackageSource::Path { .. } => "path".to_string(),
288 PackageSource::Bundled { .. } => "bundled".to_string(),
289 };
290 (
291 Some(st),
292 Some(entry.installed_at.clone()),
293 Some(entry.updated_at.clone()),
294 Some(entry.source.clone()),
295 )
296 } else {
297 (None, None, None, None)
299 };
300
301 entries.push(PackageListEntry {
302 name,
303 scope: Scope::Global,
304 source_type,
305 path: None,
306 source: Some(source_display),
307 active: global_active,
308 version: None,
309 installed_at,
310 updated_at,
311 install_source,
312 overrides: None,
313 meta,
314 error: eval_error,
315 linked: if is_symlink { Some(true) } else { None },
316 link_target,
317 broken,
318 });
319 }
320 }
321
322 for entry in entries[global_start_idx..].iter_mut() {
326 if !entry.active {
327 continue;
328 }
329 if let Some(occurrences) = seen.get(&entry.name) {
330 if occurrences.len() > 1 {
331 entry.overrides =
332 Some(occurrences.iter().skip(1).map(|(_, s)| s.clone()).collect());
333 }
334 }
335 }
336
337 let all_packages: Vec<serde_json::Value> =
339 entries.into_iter().map(|e| e.into_json()).collect();
340
341 let search_paths_json: Vec<serde_json::Value> = self
342 .search_paths
343 .iter()
344 .map(|sp| {
345 serde_json::json!({
346 "path": sp.path.display().to_string(),
347 "source": sp.source.to_string(),
348 })
349 })
350 .collect();
351
352 let mut result = serde_json::json!({
353 "packages": all_packages,
354 "search_paths": search_paths_json,
355 });
356
357 if let Some(root_str) = project_root_str {
358 result["project_root"] = serde_json::Value::String(root_str);
359 }
360 if let Some(lp) = lockfile_path_str {
361 result["lockfile_path"] = serde_json::Value::String(lp);
362 }
363
364 Ok(result.to_string())
365 }
366}
367
368fn resolve_project_pkg_info(
373 name: &str,
374 dep: &alc_toml::PackageDep,
375 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
376 root: &Path,
377) -> (Option<String>, Option<String>, Option<String>) {
378 if let Some((ver, source)) = lock_map.get(name) {
379 match source {
380 PackageSource::Path { path: raw_path } => {
381 let p = Path::new(raw_path);
382 let abs = if p.is_absolute() {
383 p.to_path_buf()
384 } else {
385 root.join(p)
386 };
387 (
388 ver.clone(),
389 Some("path".to_string()),
390 Some(abs.display().to_string()),
391 )
392 }
393 PackageSource::Installed => (ver.clone(), Some("installed".to_string()), None),
394 PackageSource::Git { .. } => (ver.clone(), Some("git".to_string()), None),
395 PackageSource::Bundled { .. } => (ver.clone(), Some("bundled".to_string()), None),
396 }
397 } else {
398 let st = match dep {
399 alc_toml::PackageDep::Version(_) => Some("installed".to_string()),
400 alc_toml::PackageDep::Path { .. } => Some("path".to_string()),
401 alc_toml::PackageDep::Git { .. } => Some("git".to_string()),
402 };
403 (None, st, None)
404 }
405}
406
407fn make_project_entry(
409 name: String,
410 version: Option<String>,
411 source_type: Option<String>,
412 abs_path: Option<String>,
413) -> PackageListEntry {
414 PackageListEntry {
415 name,
416 scope: Scope::Project,
417 source_type,
418 path: abs_path,
419 source: None,
420 active: true,
421 version,
422 installed_at: None,
423 updated_at: None,
424 install_source: None,
425 overrides: None,
426 meta: serde_json::Value::Object(serde_json::Map::new()),
427 error: None,
428 linked: None,
429 link_target: None,
430 broken: None,
431 }
432}
433
434fn collect_path_entries_from_lock(
436 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
437 root: &Path,
438 project_names: &mut std::collections::HashSet<String>,
439 entries: &mut Vec<PackageListEntry>,
440) {
441 for (name, (version, source)) in lock_map {
442 if let PackageSource::Path { path: raw_path } = source {
443 let p = Path::new(raw_path);
444 let abs = if p.is_absolute() {
445 p.to_path_buf()
446 } else {
447 root.join(p)
448 };
449 project_names.insert(name.clone());
450 entries.push(make_project_entry(
451 name.clone(),
452 version.clone(),
453 Some("path".to_string()),
454 Some(abs.display().to_string()),
455 ));
456 }
457 }
458}
459
460fn is_safe_pkg_name(name: &str) -> bool {
466 !name.is_empty()
467 && name
468 .bytes()
469 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
470}