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 Variant,
21 Project,
22 Global,
23}
24
25#[derive(Debug, Clone, Copy)]
31enum ResolvedSourceKind {
32 Installed,
34 Linked,
36 LocalPath,
38 Bundled,
40 Variant,
42}
43
44impl ResolvedSourceKind {
45 fn as_str(self) -> &'static str {
46 match self {
47 ResolvedSourceKind::Installed => "installed",
48 ResolvedSourceKind::Linked => "linked",
49 ResolvedSourceKind::LocalPath => "local_path",
50 ResolvedSourceKind::Bundled => "bundled",
51 ResolvedSourceKind::Variant => "variant",
52 }
53 }
54}
55
56#[derive(Debug)]
60struct PackageListEntry {
61 name: String,
62 scope: Scope,
63 source_type: Option<String>,
65 path: Option<String>,
67 source: Option<String>,
69 active: bool,
70 version: Option<String>,
72 installed_at: Option<String>,
73 updated_at: Option<String>,
74 install_source: Option<String>,
76 overrides: Option<Vec<String>>,
77 meta: serde_json::Value,
78 error: Option<String>,
79 linked: Option<bool>,
81 link_target: Option<String>,
83 broken: Option<bool>,
85 resolved_source_path: Option<String>,
88 resolved_source_kind: Option<ResolvedSourceKind>,
91 override_paths: Option<Vec<String>>,
94}
95
96impl PackageListEntry {
97 fn into_json(self) -> serde_json::Value {
98 let scope_str = match self.scope {
99 Scope::Variant => "variant",
100 Scope::Project => "project",
101 Scope::Global => "global",
102 };
103
104 let mut map = serde_json::Map::new();
105 map.insert("name".to_string(), serde_json::Value::String(self.name));
106 map.insert(
107 "scope".to_string(),
108 serde_json::Value::String(scope_str.to_string()),
109 );
110
111 if let Some(st) = self.source_type {
113 map.insert("source_type".to_string(), serde_json::Value::String(st));
114 }
115
116 if let Some(p) = self.path {
117 map.insert("path".to_string(), serde_json::Value::String(p));
118 }
119 if let Some(s) = self.source {
120 map.insert("source".to_string(), serde_json::Value::String(s));
121 }
122
123 map.insert("active".to_string(), serde_json::Value::Bool(self.active));
124
125 if let Some(v) = self.version {
126 map.insert("version".to_string(), serde_json::Value::String(v));
127 }
128 if let Some(ia) = self.installed_at {
129 map.insert("installed_at".to_string(), serde_json::Value::String(ia));
130 }
131 if let Some(ua) = self.updated_at {
132 map.insert("updated_at".to_string(), serde_json::Value::String(ua));
133 }
134 if let Some(is) = self.install_source {
135 map.insert("install_source".to_string(), serde_json::Value::String(is));
136 }
137 if let Some(ov) = self.overrides {
138 map.insert("overrides".to_string(), serde_json::json!(ov));
139 }
140 if let Some(rsp) = self.resolved_source_path {
141 map.insert(
142 "resolved_source_path".to_string(),
143 serde_json::Value::String(rsp),
144 );
145 }
146 if let Some(rsk) = self.resolved_source_kind {
147 map.insert(
148 "resolved_source_kind".to_string(),
149 serde_json::Value::String(rsk.as_str().to_string()),
150 );
151 }
152 if let Some(op) = self.override_paths {
153 map.insert("override_paths".to_string(), serde_json::json!(op));
154 }
155
156 if let Some(err) = self.error {
161 map.insert("error".to_string(), serde_json::Value::String(err));
162 }
163 if let Some(linked) = self.linked {
164 map.insert("linked".to_string(), serde_json::Value::Bool(linked));
165 }
166 if let Some(target) = self.link_target {
167 map.insert("link_target".to_string(), serde_json::Value::String(target));
168 }
169 if let Some(broken) = self.broken {
170 map.insert("broken".to_string(), serde_json::Value::Bool(broken));
171 }
172
173 if let serde_json::Value::Object(meta_map) = self.meta {
175 for (k, v) in meta_map {
176 map.entry(k).or_insert(v);
178 }
179 }
180
181 serde_json::Value::Object(map)
182 }
183}
184
185impl AppService {
186 pub async fn pkg_list(&self, project_root: Option<String>) -> Result<String, String> {
194 let manifest_data = manifest::load_manifest().unwrap_or_default();
196
197 let resolved_root = resolve_project_root(project_root.as_deref());
199
200 let mut project_names: std::collections::HashSet<String> = std::collections::HashSet::new();
201 let mut variant_names: std::collections::HashSet<String> = std::collections::HashSet::new();
202 let mut entries: Vec<PackageListEntry> = Vec::new();
203 let mut project_root_str: Option<String> = None;
204 let mut lockfile_path_str: Option<String> = None;
205
206 if let Some(ref root) = resolved_root {
207 project_root_str = Some(root.display().to_string());
208 lockfile_path_str = Some(lockfile_path(root).display().to_string());
209
210 collect_variant_entries(root, &mut variant_names, &mut entries);
214
215 let lock_map: HashMap<String, (Option<String>, PackageSource)> =
217 match load_lockfile(root) {
218 Ok(Some(lock)) => lock
219 .packages
220 .into_iter()
221 .map(|p| (p.name, (p.version, p.source)))
222 .collect(),
223 Ok(None) => HashMap::new(),
224 Err(e) => {
225 tracing::warn!("failed to load alc.lock: {e}");
226 HashMap::new()
227 }
228 };
229
230 match load_alc_toml(root) {
232 Ok(Some(alc_toml)) => {
233 for (name, dep) in &alc_toml.packages {
234 let (version, source_type, abs_path) =
235 resolve_project_pkg_info(name, dep, &lock_map, root);
236 project_names.insert(name.clone());
237
238 let (rsp, rsk, resolve_err): (
243 Option<String>,
244 Option<ResolvedSourceKind>,
245 Option<String>,
246 ) = match source_type.as_deref() {
247 Some("path") => {
248 let rsp = abs_path
249 .as_ref()
250 .and_then(|p| resolve_source_path(std::path::Path::new(p)));
251 (rsp, Some(ResolvedSourceKind::LocalPath), None)
252 }
253 Some(st) => {
254 let kind = if st == "bundled" {
255 ResolvedSourceKind::Bundled
256 } else {
257 ResolvedSourceKind::Installed
258 };
259 match packages_dir() {
260 Ok(dir) => {
261 (resolve_source_path(&dir.join(name)), Some(kind), None)
262 }
263 Err(e) => (
264 None,
265 Some(kind),
266 Some(format!("cannot resolve packages_dir: {e}")),
267 ),
268 }
269 }
270 None => (None, None, None),
271 };
272
273 let mut entry = make_project_entry(
274 name.clone(),
275 version,
276 source_type,
277 abs_path,
278 rsp,
279 rsk,
280 resolve_err,
281 );
282 if variant_names.contains(name) {
283 entry.active = false;
284 }
285 entries.push(entry);
286 }
287 }
288 Ok(None) => {
289 collect_path_entries_from_lock(
291 &lock_map,
292 root,
293 &variant_names,
294 &mut project_names,
295 &mut entries,
296 );
297 }
298 Err(e) => {
299 tracing::warn!("failed to load alc.toml: {e}");
300 }
301 }
302 }
303
304 let mut seen: HashMap<String, Vec<(usize, String)>> = HashMap::new();
307 let global_start_idx = entries.len();
309
310 for (idx, sp) in self.search_paths.iter().enumerate() {
311 if !sp.path.is_dir() {
312 continue;
313 }
314 let read_entries = match std::fs::read_dir(&sp.path) {
315 Ok(e) => e,
316 Err(_) => continue,
317 };
318
319 for dir_entry in read_entries.flatten() {
320 let path = dir_entry.path();
321
322 let is_symlink = path
325 .symlink_metadata()
326 .map(|m| m.file_type().is_symlink())
327 .unwrap_or(false);
328
329 let link_target = if is_symlink {
330 path.read_link().ok().map(|t| t.display().to_string())
331 } else {
332 None
333 };
334
335 let broken = if is_symlink {
344 Some(!path.try_exists().unwrap_or(false))
345 } else {
346 None
347 };
348
349 if !is_symlink && !path.is_dir() {
353 continue;
354 }
355
356 if broken != Some(true) && !path.join("init.lua").exists() {
358 continue;
359 }
360
361 let name = dir_entry.file_name().to_string_lossy().to_string();
362 if is_system_package(&name) {
363 continue;
364 }
365
366 let source_display = sp.path.display().to_string();
367 seen.entry(name.clone())
368 .or_default()
369 .push((idx, source_display.clone()));
370
371 let global_active = seen[&name].len() == 1
374 && !project_names.contains(&name)
375 && !variant_names.contains(&name);
376
377 let (meta, eval_error) = if is_safe_pkg_name(&name) {
379 let code = format!(
380 r#"package.loaded["{name}"] = nil
381local pkg = require("{name}")
382return pkg.meta or {{ name = "{name}" }}"#
383 );
384 match self.executor.eval_simple(code).await {
385 Ok(v) => (v, None),
386 Err(_) => (
387 serde_json::Value::Object(serde_json::Map::new()),
388 Some("failed to load meta".to_string()),
389 ),
390 }
391 } else {
392 (
393 serde_json::Value::Object(serde_json::Map::new()),
394 Some("invalid package name".to_string()),
395 )
396 };
397
398 let (source_type, installed_at, updated_at, install_source) =
400 if let Some(entry) = manifest_data.packages.get(&name) {
401 let inferred = infer_from_legacy_source_string(&entry.source);
402 let st = match &inferred {
403 PackageSource::Git { .. } => "git".to_string(),
404 PackageSource::Installed => {
405 format!("installed (from: {})", entry.source)
407 }
408 PackageSource::Path { .. } => "path".to_string(),
409 PackageSource::Bundled { .. } => "bundled".to_string(),
410 };
411 (
412 Some(st),
413 Some(entry.installed_at.clone()),
414 Some(entry.updated_at.clone()),
415 Some(entry.source.clone()),
416 )
417 } else {
418 (None, None, None, None)
420 };
421
422 let (resolved_source_path, resolved_source_kind): (
424 Option<String>,
425 Option<ResolvedSourceKind>,
426 ) = if is_symlink {
427 let kind = Some(ResolvedSourceKind::Linked);
428 if broken == Some(true) {
429 (None, kind)
431 } else {
432 let candidate = path.read_link().ok().map(|target| {
434 if target.is_absolute() {
435 target
436 } else {
437 sp.path.join(target)
438 }
439 });
440 let rsp = candidate.as_deref().and_then(resolve_source_path);
441 (rsp, kind)
442 }
443 } else {
444 let candidate = sp.path.join(&name);
446 let rsp = resolve_source_path(&candidate);
447 let kind = match source_type.as_deref() {
448 Some("bundled") => ResolvedSourceKind::Bundled,
449 _ => ResolvedSourceKind::Installed,
450 };
451 (rsp, Some(kind))
452 };
453
454 entries.push(PackageListEntry {
455 name,
456 scope: Scope::Global,
457 source_type,
458 path: None,
459 source: Some(source_display),
460 active: global_active,
461 version: None,
462 installed_at,
463 updated_at,
464 install_source,
465 overrides: None,
466 meta,
467 error: eval_error,
468 linked: if is_symlink { Some(true) } else { None },
469 link_target,
470 broken,
471 resolved_source_path,
472 resolved_source_kind,
473 override_paths: None,
474 });
475 }
476 }
477
478 for entry in entries[global_start_idx..].iter_mut() {
484 if !entry.active {
485 continue;
486 }
487 if let Some(occurrences) = seen.get(&entry.name) {
488 if occurrences.len() > 1 {
489 entry.overrides =
490 Some(occurrences.iter().skip(1).map(|(_, s)| s.clone()).collect());
491
492 let override_ps: Vec<String> = occurrences
494 .iter()
495 .skip(1)
496 .filter_map(|(idx, _)| {
497 let candidate = self.search_paths[*idx].path.join(&entry.name);
498 resolve_source_path(&candidate)
499 })
500 .collect();
501 if !override_ps.is_empty() {
502 entry.override_paths = Some(override_ps);
503 }
504 }
505 }
506 }
507
508 for entry in entries[..global_start_idx].iter_mut() {
518 let self_path = entry.resolved_source_path.as_deref();
519 if let Some(occurrences) = seen.get(&entry.name) {
520 let ps: Vec<String> = occurrences
521 .iter()
522 .filter_map(|(idx, _)| {
523 let candidate = self.search_paths[*idx].path.join(&entry.name);
524 resolve_source_path(&candidate)
525 })
526 .filter(|p| Some(p.as_str()) != self_path)
527 .collect();
528 if !ps.is_empty() {
529 entry.override_paths = Some(ps);
530 }
531 }
532 }
533
534 let all_packages: Vec<serde_json::Value> =
536 entries.into_iter().map(|e| e.into_json()).collect();
537
538 let search_paths_json: Vec<serde_json::Value> = self
539 .search_paths
540 .iter()
541 .map(|sp| {
542 serde_json::json!({
543 "path": sp.path.display().to_string(),
544 "source": sp.source.to_string(),
545 })
546 })
547 .collect();
548
549 let mut result = serde_json::json!({
550 "packages": all_packages,
551 "search_paths": search_paths_json,
552 });
553
554 if let Some(root_str) = project_root_str {
555 result["project_root"] = serde_json::Value::String(root_str);
556 }
557 if let Some(lp) = lockfile_path_str {
558 result["lockfile_path"] = serde_json::Value::String(lp);
559 }
560
561 Ok(result.to_string())
562 }
563}
564
565fn resolve_project_pkg_info(
570 name: &str,
571 dep: &alc_toml::PackageDep,
572 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
573 root: &Path,
574) -> (Option<String>, Option<String>, Option<String>) {
575 if let Some((ver, source)) = lock_map.get(name) {
576 match source {
577 PackageSource::Path { path: raw_path } => {
578 let p = Path::new(raw_path);
579 let abs = if p.is_absolute() {
580 p.to_path_buf()
581 } else {
582 root.join(p)
583 };
584 (
585 ver.clone(),
586 Some("path".to_string()),
587 Some(abs.display().to_string()),
588 )
589 }
590 PackageSource::Installed => (ver.clone(), Some("installed".to_string()), None),
591 PackageSource::Git { .. } => (ver.clone(), Some("git".to_string()), None),
592 PackageSource::Bundled { .. } => (ver.clone(), Some("bundled".to_string()), None),
593 }
594 } else {
595 let st = match dep {
596 alc_toml::PackageDep::Version(_) => Some("installed".to_string()),
597 alc_toml::PackageDep::Path { .. } => Some("path".to_string()),
598 alc_toml::PackageDep::Git { .. } => Some("git".to_string()),
599 };
600 (None, st, None)
601 }
602}
603
604fn collect_variant_entries(
614 root: &Path,
615 variant_names: &mut std::collections::HashSet<String>,
616 entries: &mut Vec<PackageListEntry>,
617) {
618 let local = match alc_toml::load_alc_local_toml(root) {
619 Ok(Some(l)) => l,
620 Ok(None) => return,
621 Err(e) => {
622 tracing::warn!("failed to load alc.local.toml at {}: {e}", root.display());
623 return;
624 }
625 };
626
627 for vp in alc_toml::resolve_local_variant_pkgs(root, &local) {
628 variant_names.insert(vp.name.clone());
629 let abs_path = vp.pkg_dir.display().to_string();
630 let rsp = resolve_source_path(&vp.pkg_dir);
631 entries.push(PackageListEntry {
632 name: vp.name,
633 scope: Scope::Variant,
634 source_type: Some("path".to_string()),
635 path: Some(abs_path),
636 source: None,
637 active: true,
638 version: None,
639 installed_at: None,
640 updated_at: None,
641 install_source: None,
642 overrides: None,
643 meta: serde_json::Value::Object(serde_json::Map::new()),
644 error: None,
645 linked: None,
646 link_target: None,
647 broken: None,
648 resolved_source_path: rsp,
649 resolved_source_kind: Some(ResolvedSourceKind::Variant),
650 override_paths: None,
651 });
652 }
653}
654
655fn make_project_entry(
657 name: String,
658 version: Option<String>,
659 source_type: Option<String>,
660 abs_path: Option<String>,
661 resolved_source_path: Option<String>,
662 resolved_source_kind: Option<ResolvedSourceKind>,
663 error: Option<String>,
664) -> PackageListEntry {
665 PackageListEntry {
666 name,
667 scope: Scope::Project,
668 source_type,
669 path: abs_path,
670 source: None,
671 active: true,
672 version,
673 installed_at: None,
674 updated_at: None,
675 install_source: None,
676 overrides: None,
677 meta: serde_json::Value::Object(serde_json::Map::new()),
678 error,
679 linked: None,
680 link_target: None,
681 broken: None,
682 resolved_source_path,
683 resolved_source_kind,
684 override_paths: None,
685 }
686}
687
688fn collect_path_entries_from_lock(
690 lock_map: &HashMap<String, (Option<String>, PackageSource)>,
691 root: &Path,
692 variant_names: &std::collections::HashSet<String>,
693 project_names: &mut std::collections::HashSet<String>,
694 entries: &mut Vec<PackageListEntry>,
695) {
696 for (name, (version, source)) in lock_map {
697 if let PackageSource::Path { path: raw_path } = source {
698 let p = Path::new(raw_path);
699 let abs = if p.is_absolute() {
700 p.to_path_buf()
701 } else {
702 root.join(p)
703 };
704 project_names.insert(name.clone());
705 let rsp = resolve_source_path(&abs);
706 let mut entry = make_project_entry(
707 name.clone(),
708 version.clone(),
709 Some("path".to_string()),
710 Some(abs.display().to_string()),
711 rsp,
712 Some(ResolvedSourceKind::LocalPath),
713 None,
714 );
715 if variant_names.contains(name) {
716 entry.active = false;
717 }
718 entries.push(entry);
719 }
720 }
721}
722
723fn resolve_source_path(candidate: &std::path::Path) -> Option<String> {
730 std::fs::canonicalize(candidate)
731 .ok()
732 .map(|p| p.display().to_string())
733}
734
735fn is_safe_pkg_name(name: &str) -> bool {
741 !name.is_empty()
742 && name
743 .bytes()
744 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
745}