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, packages_dir};
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, Clone, Copy)]
28enum ResolvedSourceKind {
29 Installed,
31 Linked,
33 LocalPath,
35 Bundled,
37}
38
39impl ResolvedSourceKind {
40 fn as_str(self) -> &'static str {
41 match self {
42 ResolvedSourceKind::Installed => "installed",
43 ResolvedSourceKind::Linked => "linked",
44 ResolvedSourceKind::LocalPath => "local_path",
45 ResolvedSourceKind::Bundled => "bundled",
46 }
47 }
48}
49
50#[derive(Debug)]
54struct PackageListEntry {
55 name: String,
56 scope: Scope,
57 source_type: Option<String>,
59 path: Option<String>,
61 source: Option<String>,
63 active: bool,
64 version: Option<String>,
66 installed_at: Option<String>,
67 updated_at: Option<String>,
68 install_source: Option<String>,
70 overrides: Option<Vec<String>>,
71 meta: serde_json::Value,
72 error: Option<String>,
73 linked: Option<bool>,
75 link_target: Option<String>,
77 broken: Option<bool>,
79 resolved_source_path: Option<String>,
82 resolved_source_kind: Option<ResolvedSourceKind>,
85 override_paths: Option<Vec<String>>,
88}
89
90impl PackageListEntry {
91 fn into_json(self) -> serde_json::Value {
92 let scope_str = match self.scope {
93 Scope::Project => "project",
94 Scope::Global => "global",
95 };
96
97 let mut map = serde_json::Map::new();
98 map.insert("name".to_string(), serde_json::Value::String(self.name));
99 map.insert(
100 "scope".to_string(),
101 serde_json::Value::String(scope_str.to_string()),
102 );
103
104 if let Some(st) = self.source_type {
106 map.insert("source_type".to_string(), serde_json::Value::String(st));
107 }
108
109 if let Some(p) = self.path {
110 map.insert("path".to_string(), serde_json::Value::String(p));
111 }
112 if let Some(s) = self.source {
113 map.insert("source".to_string(), serde_json::Value::String(s));
114 }
115
116 map.insert("active".to_string(), serde_json::Value::Bool(self.active));
117
118 if let Some(v) = self.version {
119 map.insert("version".to_string(), serde_json::Value::String(v));
120 }
121 if let Some(ia) = self.installed_at {
122 map.insert("installed_at".to_string(), serde_json::Value::String(ia));
123 }
124 if let Some(ua) = self.updated_at {
125 map.insert("updated_at".to_string(), serde_json::Value::String(ua));
126 }
127 if let Some(is) = self.install_source {
128 map.insert("install_source".to_string(), serde_json::Value::String(is));
129 }
130 if let Some(ov) = self.overrides {
131 map.insert("overrides".to_string(), serde_json::json!(ov));
132 }
133 if let Some(rsp) = self.resolved_source_path {
134 map.insert(
135 "resolved_source_path".to_string(),
136 serde_json::Value::String(rsp),
137 );
138 }
139 if let Some(rsk) = self.resolved_source_kind {
140 map.insert(
141 "resolved_source_kind".to_string(),
142 serde_json::Value::String(rsk.as_str().to_string()),
143 );
144 }
145 if let Some(op) = self.override_paths {
146 map.insert("override_paths".to_string(), serde_json::json!(op));
147 }
148
149 if let Some(err) = self.error {
154 map.insert("error".to_string(), serde_json::Value::String(err));
155 }
156 if let Some(linked) = self.linked {
157 map.insert("linked".to_string(), serde_json::Value::Bool(linked));
158 }
159 if let Some(target) = self.link_target {
160 map.insert("link_target".to_string(), serde_json::Value::String(target));
161 }
162 if let Some(broken) = self.broken {
163 map.insert("broken".to_string(), serde_json::Value::Bool(broken));
164 }
165
166 if let serde_json::Value::Object(meta_map) = self.meta {
168 for (k, v) in meta_map {
169 map.entry(k).or_insert(v);
171 }
172 }
173
174 serde_json::Value::Object(map)
175 }
176}
177
178impl AppService {
179 pub async fn pkg_list(&self, project_root: Option<String>) -> Result<String, String> {
187 let manifest_data = manifest::load_manifest().unwrap_or_default();
189
190 let resolved_root = resolve_project_root(project_root.as_deref());
192
193 let mut project_names: std::collections::HashSet<String> = std::collections::HashSet::new();
194 let mut entries: Vec<PackageListEntry> = Vec::new();
195 let mut project_root_str: Option<String> = None;
196 let mut lockfile_path_str: Option<String> = None;
197
198 if let Some(ref root) = resolved_root {
199 project_root_str = Some(root.display().to_string());
200 lockfile_path_str = Some(lockfile_path(root).display().to_string());
201
202 let lock_map: HashMap<String, (Option<String>, PackageSource)> =
204 match load_lockfile(root) {
205 Ok(Some(lock)) => lock
206 .packages
207 .into_iter()
208 .map(|p| (p.name, (p.version, p.source)))
209 .collect(),
210 Ok(None) => HashMap::new(),
211 Err(e) => {
212 tracing::warn!("failed to load alc.lock: {e}");
213 HashMap::new()
214 }
215 };
216
217 match load_alc_toml(root) {
219 Ok(Some(alc_toml)) => {
220 for (name, dep) in &alc_toml.packages {
221 let (version, source_type, abs_path) =
222 resolve_project_pkg_info(name, dep, &lock_map, root);
223 project_names.insert(name.clone());
224
225 let (rsp, rsk, resolve_err): (
230 Option<String>,
231 Option<ResolvedSourceKind>,
232 Option<String>,
233 ) = match source_type.as_deref() {
234 Some("path") => {
235 let rsp = abs_path
236 .as_ref()
237 .and_then(|p| resolve_source_path(std::path::Path::new(p)));
238 (rsp, Some(ResolvedSourceKind::LocalPath), None)
239 }
240 Some(st) => {
241 let kind = if st == "bundled" {
242 ResolvedSourceKind::Bundled
243 } else {
244 ResolvedSourceKind::Installed
245 };
246 match packages_dir() {
247 Ok(dir) => {
248 (resolve_source_path(&dir.join(name)), Some(kind), None)
249 }
250 Err(e) => (
251 None,
252 Some(kind),
253 Some(format!("cannot resolve packages_dir: {e}")),
254 ),
255 }
256 }
257 None => (None, None, None),
258 };
259
260 entries.push(make_project_entry(
261 name.clone(),
262 version,
263 source_type,
264 abs_path,
265 rsp,
266 rsk,
267 resolve_err,
268 ));
269 }
270 }
271 Ok(None) => {
272 collect_path_entries_from_lock(
274 &lock_map,
275 root,
276 &mut project_names,
277 &mut entries,
278 );
279 }
280 Err(e) => {
281 tracing::warn!("failed to load alc.toml: {e}");
282 }
283 }
284 }
285
286 let mut seen: HashMap<String, Vec<(usize, String)>> = HashMap::new();
289 let global_start_idx = entries.len();
291
292 for (idx, sp) in self.search_paths.iter().enumerate() {
293 if !sp.path.is_dir() {
294 continue;
295 }
296 let read_entries = match std::fs::read_dir(&sp.path) {
297 Ok(e) => e,
298 Err(_) => continue,
299 };
300
301 for dir_entry in read_entries.flatten() {
302 let path = dir_entry.path();
303
304 let is_symlink = path
307 .symlink_metadata()
308 .map(|m| m.file_type().is_symlink())
309 .unwrap_or(false);
310
311 let link_target = if is_symlink {
312 path.read_link().ok().map(|t| t.display().to_string())
313 } else {
314 None
315 };
316
317 let broken = if is_symlink {
326 Some(!path.try_exists().unwrap_or(false))
327 } else {
328 None
329 };
330
331 if !is_symlink && !path.is_dir() {
335 continue;
336 }
337
338 if broken != Some(true) && !path.join("init.lua").exists() {
340 continue;
341 }
342
343 let name = dir_entry.file_name().to_string_lossy().to_string();
344 if is_system_package(&name) {
345 continue;
346 }
347
348 let source_display = sp.path.display().to_string();
349 seen.entry(name.clone())
350 .or_default()
351 .push((idx, source_display.clone()));
352
353 let global_active = seen[&name].len() == 1 && !project_names.contains(&name);
356
357 let (meta, eval_error) = if is_safe_pkg_name(&name) {
359 let code = format!(
360 r#"package.loaded["{name}"] = nil
361local pkg = require("{name}")
362return pkg.meta or {{ name = "{name}" }}"#
363 );
364 match self.executor.eval_simple(code).await {
365 Ok(v) => (v, None),
366 Err(_) => (
367 serde_json::Value::Object(serde_json::Map::new()),
368 Some("failed to load meta".to_string()),
369 ),
370 }
371 } else {
372 (
373 serde_json::Value::Object(serde_json::Map::new()),
374 Some("invalid package name".to_string()),
375 )
376 };
377
378 let (source_type, installed_at, updated_at, install_source) =
380 if let Some(entry) = manifest_data.packages.get(&name) {
381 let inferred = infer_from_legacy_source_string(&entry.source);
382 let st = match &inferred {
383 PackageSource::Git { .. } => "git".to_string(),
384 PackageSource::Installed => {
385 format!("installed (from: {})", entry.source)
387 }
388 PackageSource::Path { .. } => "path".to_string(),
389 PackageSource::Bundled { .. } => "bundled".to_string(),
390 };
391 (
392 Some(st),
393 Some(entry.installed_at.clone()),
394 Some(entry.updated_at.clone()),
395 Some(entry.source.clone()),
396 )
397 } else {
398 (None, None, None, None)
400 };
401
402 let (resolved_source_path, resolved_source_kind): (
404 Option<String>,
405 Option<ResolvedSourceKind>,
406 ) = if is_symlink {
407 let kind = Some(ResolvedSourceKind::Linked);
408 if broken == Some(true) {
409 (None, kind)
411 } else {
412 let candidate = path.read_link().ok().map(|target| {
414 if target.is_absolute() {
415 target
416 } else {
417 sp.path.join(target)
418 }
419 });
420 let rsp = candidate.as_deref().and_then(resolve_source_path);
421 (rsp, kind)
422 }
423 } else {
424 let candidate = sp.path.join(&name);
426 let rsp = resolve_source_path(&candidate);
427 let kind = match source_type.as_deref() {
428 Some("bundled") => ResolvedSourceKind::Bundled,
429 _ => ResolvedSourceKind::Installed,
430 };
431 (rsp, Some(kind))
432 };
433
434 entries.push(PackageListEntry {
435 name,
436 scope: Scope::Global,
437 source_type,
438 path: None,
439 source: Some(source_display),
440 active: global_active,
441 version: None,
442 installed_at,
443 updated_at,
444 install_source,
445 overrides: None,
446 meta,
447 error: eval_error,
448 linked: if is_symlink { Some(true) } else { None },
449 link_target,
450 broken,
451 resolved_source_path,
452 resolved_source_kind,
453 override_paths: None,
454 });
455 }
456 }
457
458 for entry in entries[global_start_idx..].iter_mut() {
464 if !entry.active {
465 continue;
466 }
467 if let Some(occurrences) = seen.get(&entry.name) {
468 if occurrences.len() > 1 {
469 entry.overrides =
470 Some(occurrences.iter().skip(1).map(|(_, s)| s.clone()).collect());
471
472 let override_ps: Vec<String> = occurrences
474 .iter()
475 .skip(1)
476 .filter_map(|(idx, _)| {
477 let candidate = self.search_paths[*idx].path.join(&entry.name);
478 resolve_source_path(&candidate)
479 })
480 .collect();
481 if !override_ps.is_empty() {
482 entry.override_paths = Some(override_ps);
483 }
484 }
485 }
486 }
487
488 for entry in entries[..global_start_idx].iter_mut() {
498 let self_path = entry.resolved_source_path.as_deref();
499 if let Some(occurrences) = seen.get(&entry.name) {
500 let ps: Vec<String> = occurrences
501 .iter()
502 .filter_map(|(idx, _)| {
503 let candidate = self.search_paths[*idx].path.join(&entry.name);
504 resolve_source_path(&candidate)
505 })
506 .filter(|p| Some(p.as_str()) != self_path)
507 .collect();
508 if !ps.is_empty() {
509 entry.override_paths = Some(ps);
510 }
511 }
512 }
513
514 let all_packages: Vec<serde_json::Value> =
516 entries.into_iter().map(|e| e.into_json()).collect();
517
518 let search_paths_json: Vec<serde_json::Value> = self
519 .search_paths
520 .iter()
521 .map(|sp| {
522 serde_json::json!({
523 "path": sp.path.display().to_string(),
524 "source": sp.source.to_string(),
525 })
526 })
527 .collect();
528
529 let mut result = serde_json::json!({
530 "packages": all_packages,
531 "search_paths": search_paths_json,
532 });
533
534 if let Some(root_str) = project_root_str {
535 result["project_root"] = serde_json::Value::String(root_str);
536 }
537 if let Some(lp) = lockfile_path_str {
538 result["lockfile_path"] = serde_json::Value::String(lp);
539 }
540
541 Ok(result.to_string())
542 }
543}
544
545fn resolve_project_pkg_info(
550 name: &str,
551 dep: &alc_toml::PackageDep,
552 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
553 root: &Path,
554) -> (Option<String>, Option<String>, Option<String>) {
555 if let Some((ver, source)) = lock_map.get(name) {
556 match source {
557 PackageSource::Path { path: raw_path } => {
558 let p = Path::new(raw_path);
559 let abs = if p.is_absolute() {
560 p.to_path_buf()
561 } else {
562 root.join(p)
563 };
564 (
565 ver.clone(),
566 Some("path".to_string()),
567 Some(abs.display().to_string()),
568 )
569 }
570 PackageSource::Installed => (ver.clone(), Some("installed".to_string()), None),
571 PackageSource::Git { .. } => (ver.clone(), Some("git".to_string()), None),
572 PackageSource::Bundled { .. } => (ver.clone(), Some("bundled".to_string()), None),
573 }
574 } else {
575 let st = match dep {
576 alc_toml::PackageDep::Version(_) => Some("installed".to_string()),
577 alc_toml::PackageDep::Path { .. } => Some("path".to_string()),
578 alc_toml::PackageDep::Git { .. } => Some("git".to_string()),
579 };
580 (None, st, None)
581 }
582}
583
584fn make_project_entry(
586 name: String,
587 version: Option<String>,
588 source_type: Option<String>,
589 abs_path: Option<String>,
590 resolved_source_path: Option<String>,
591 resolved_source_kind: Option<ResolvedSourceKind>,
592 error: Option<String>,
593) -> PackageListEntry {
594 PackageListEntry {
595 name,
596 scope: Scope::Project,
597 source_type,
598 path: abs_path,
599 source: None,
600 active: true,
601 version,
602 installed_at: None,
603 updated_at: None,
604 install_source: None,
605 overrides: None,
606 meta: serde_json::Value::Object(serde_json::Map::new()),
607 error,
608 linked: None,
609 link_target: None,
610 broken: None,
611 resolved_source_path,
612 resolved_source_kind,
613 override_paths: None,
614 }
615}
616
617fn collect_path_entries_from_lock(
619 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
620 root: &Path,
621 project_names: &mut std::collections::HashSet<String>,
622 entries: &mut Vec<PackageListEntry>,
623) {
624 for (name, (version, source)) in lock_map {
625 if let PackageSource::Path { path: raw_path } = source {
626 let p = Path::new(raw_path);
627 let abs = if p.is_absolute() {
628 p.to_path_buf()
629 } else {
630 root.join(p)
631 };
632 project_names.insert(name.clone());
633 let rsp = resolve_source_path(&abs);
634 entries.push(make_project_entry(
635 name.clone(),
636 version.clone(),
637 Some("path".to_string()),
638 Some(abs.display().to_string()),
639 rsp,
640 Some(ResolvedSourceKind::LocalPath),
641 None,
642 ));
643 }
644 }
645}
646
647fn resolve_source_path(candidate: &std::path::Path) -> Option<String> {
654 std::fs::canonicalize(candidate)
655 .ok()
656 .map(|p| p.display().to_string())
657}
658
659fn is_safe_pkg_name(name: &str) -> bool {
665 !name.is_empty()
666 && name
667 .bytes()
668 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
669}