1use std::collections::HashSet;
7
8use anyhow::{Result, anyhow};
9use comfy_table::presets::UTF8_FULL;
10use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table};
11use rayon::prelude::*;
12
13use crate::pkg::{config, local, types};
14
15pub fn run(
31 all: bool,
32 outdated: bool,
33 registry_filter: Option<&str>,
34 repo_filter: Option<&str>,
35 type_filter: Option<&str>,
36 foreign: bool,
37 names_only: bool,
38 completion: bool
39) -> Result<()> {
40 let package_type = match type_filter {
41 Some("package") => Some(types::PackageType::Package),
42 Some("collection") => Some(types::PackageType::Collection),
43 Some("app") => Some(types::PackageType::App),
44 Some("extension") => Some(types::PackageType::Extension),
45 Some(other) => return Err(anyhow!("Invalid package type: {other}")),
46 None => None
47 };
48
49 if outdated {
50 return run_list_outdated(registry_filter, repo_filter, package_type);
51 }
52
53 if names_only || completion {
54 return run_list_names(
55 all,
56 registry_filter,
57 repo_filter,
58 package_type,
59 completion
60 );
61 }
62
63 if all {
64 if foreign {
65 return Err(anyhow!(
66 "The --foreign flag cannot be used with --all."
67 ));
68 }
69 run_list_all(registry_filter, repo_filter, package_type)?;
70 } else {
71 run_list_installed(
72 registry_filter,
73 repo_filter,
74 package_type,
75 foreign
76 )?;
77 }
78 Ok(())
79}
80
81fn run_list_outdated(
83 registry_filter: Option<&str>,
84 repo_filter: Option<&str>,
85 type_filter: Option<types::PackageType>
86) -> Result<()> {
87 let installed_packages = local::get_installed_packages()?;
88
89 let mut table = Table::new();
90 table
91 .load_style(UTF8_FULL)
92 .set_content_arrangement(ContentArrangement::Dynamic)
93 .set_header(vec![
94 Cell::new("Package").add_attribute(Attribute::Bold),
95 Cell::new("Current").add_attribute(Attribute::Bold),
96 Cell::new("Latest").add_attribute(Attribute::Bold),
97 Cell::new("Repo").add_attribute(Attribute::Bold),
98 Cell::new("Registry").add_attribute(Attribute::Bold),
99 Cell::new("Scope").add_attribute(Attribute::Bold),
100 ]);
101
102 let mut found_outdated = false;
103
104 for manifest in installed_packages {
105 if let Some(registry_f) = registry_filter
106 && manifest.registry_handle != registry_f
107 {
108 continue;
109 }
110
111 if let Some(repo_f) = repo_filter {
112 let repo_matches = if repo_f.contains('/') {
113 manifest.repo == repo_f
114 } else {
115 manifest.repo.split('/').any(|part| part == repo_f)
116 };
117 if !repo_matches {
118 continue;
119 }
120 }
121
122 if let Some(type_f) = type_filter
123 && manifest.package_type != type_f
124 {
125 continue;
126 }
127
128 let source = if let Some(sub) = &manifest.sub_package {
129 format!(
130 "#{}@{}/{}:{}",
131 manifest.registry_handle, manifest.repo, manifest.name, sub
132 )
133 } else {
134 format!(
135 "#{}@{}/{}",
136 manifest.registry_handle, manifest.repo, manifest.name
137 )
138 };
139
140 if let Ok((pkg, new_version, _, _, _, _, _)) =
141 crate::pkg::resolve::resolve_package_and_version(
142 &source, None, true, false
143 )
144 && (manifest.version != new_version
145 || manifest.revision != pkg.revision)
146 {
147 let current_display = if manifest.revision == "1" {
148 manifest.version.clone()
149 } else {
150 format!("{}-{}", manifest.version, manifest.revision)
151 };
152
153 let latest_display = if pkg.revision == "1" {
154 new_version
155 } else {
156 format!("{}-{}", new_version, pkg.revision)
157 };
158
159 table.add_row(vec![
160 Cell::new(manifest.name.clone()).fg(Color::Cyan),
161 Cell::new(current_display).fg(Color::Yellow),
162 Cell::new(latest_display).fg(Color::Green),
163 Cell::new(manifest.repo.clone()).fg(Color::DarkGrey),
164 Cell::new(manifest.registry_handle.clone()).fg(Color::DarkGrey),
165 Cell::new(format!("{:?}", manifest.scope).to_lowercase())
166 .fg(Color::DarkGrey),
167 ]);
168 found_outdated = true;
169 }
170 }
171
172 if found_outdated {
173 println!("{table}");
174 } else {
175 println!("No outdated packages found.");
176 }
177
178 Ok(())
179}
180
181fn run_list_installed(
183 registry_filter: Option<&str>,
184 repo_filter: Option<&str>,
185 type_filter: Option<types::PackageType>,
186 foreign: bool
187) -> Result<()> {
188 let config = config::read_config()?;
189 let mut active_registries = HashSet::new();
190 if let Some(default) = &config.default_registry {
191 active_registries.insert(default.handle.clone());
192 }
193 for reg in &config.added_registries {
194 active_registries.insert(reg.handle.clone());
195 }
196
197 let mut db_failed = false;
198 let packages_from_db =
199 if let Ok(pkgs) = crate::pkg::db::list_all_packages("local") {
200 pkgs
201 } else {
202 db_failed = true;
203 Vec::new()
204 };
205
206 let mut table = Table::new();
207 table
208 .load_style(UTF8_FULL)
209 .set_content_arrangement(ContentArrangement::Dynamic)
210 .set_header(vec![
211 Cell::new("Package").add_attribute(Attribute::Bold),
212 Cell::new("Version").add_attribute(Attribute::Bold),
213 Cell::new("Repo").add_attribute(Attribute::Bold),
214 Cell::new("Registry").add_attribute(Attribute::Bold),
215 Cell::new("Type").add_attribute(Attribute::Bold),
216 Cell::new("Scope").add_attribute(Attribute::Bold),
217 ]);
218
219 let mut found_packages = false;
220 let mut seen_rows = HashSet::new();
223
224 if !db_failed && !packages_from_db.is_empty() {
225 for pkg in packages_from_db {
226 if foreign
227 && let Some(reg) = &pkg.registry_handle
228 && active_registries.contains(reg)
229 {
230 continue;
231 }
232
233 if let Some(reg_f) = registry_filter
234 && pkg.registry_handle.as_deref() != Some(reg_f)
235 {
236 continue;
237 }
238
239 if let Some(repo_f) = repo_filter {
240 let repo_matches = if repo_f.contains('/') {
241 pkg.repo == repo_f
242 } else {
243 pkg.repo.split('/').any(|part| part == repo_f)
244 };
245 if !repo_matches {
246 continue;
247 }
248 }
249 if type_filter.is_some()
250 && pkg.package_type
251 != type_filter.ok_or_else(|| {
252 anyhow!("type_filter missing despite being set")
253 })?
254 {
255 continue;
256 }
257
258 let version_display = if pkg.revision == "1" {
259 pkg.version.as_deref().unwrap_or("N/A").to_string()
260 } else {
261 format!(
262 "{}-{}",
263 pkg.version.as_deref().unwrap_or("N/A"),
264 pkg.revision
265 )
266 };
267
268 let scope_display = format!("{:?}", pkg.scope).to_lowercase();
269 let row_key = format!(
270 "{}/{}/{}/{}/{}/{}/{}",
271 pkg.registry_handle.as_deref().unwrap_or("none"),
272 pkg.repo,
273 pkg.name,
274 pkg.sub_package.as_deref().unwrap_or(""),
275 scope_display,
276 version_display,
277 pkg.revision,
278 );
279 if !seen_rows.insert(row_key) {
280 continue;
281 }
282
283 let package_display = if let Some(sub) = &pkg.sub_package {
284 format!("{}:{}", pkg.name, sub)
285 } else {
286 pkg.name
287 };
288
289 let repo_display = &pkg.repo;
290
291 table.add_row(vec![
292 Cell::new(package_display).fg(Color::Cyan),
293 Cell::new(version_display).fg(Color::Yellow),
294 Cell::new(repo_display.clone()).fg(Color::Green),
295 Cell::new(
296 pkg.registry_handle.unwrap_or_else(|| "none".to_string())
297 )
298 .fg(Color::DarkGrey),
299 Cell::new(format!("{:?}", pkg.package_type))
300 .fg(Color::DarkGrey),
301 Cell::new(scope_display).fg(Color::DarkGrey),
302 ]);
303 found_packages = true;
304 }
305 } else {
306 let packages = local::get_installed_packages_with_type()?;
307 if packages.is_empty() {
308 println!("No packages installed by Zoi.");
309 return Ok(());
310 }
311
312 for pkg in packages {
313 let manifest = local::is_package_installed(
314 &pkg.name,
315 pkg.sub_package.as_deref(),
316 types::Scope::User
317 )?
318 .or(local::is_package_installed(
319 &pkg.name,
320 pkg.sub_package.as_deref(),
321 types::Scope::System
322 )?)
323 .or(local::is_package_installed(
324 &pkg.name,
325 pkg.sub_package.as_deref(),
326 types::Scope::Project
327 )?);
328
329 let Some(m) = manifest else { continue };
330
331 if foreign && active_registries.contains(&m.registry_handle) {
332 continue;
333 }
334
335 if let Some(reg_f) = registry_filter
336 && m.registry_handle != reg_f
337 {
338 continue;
339 }
340 if let Some(repo_f) = repo_filter {
341 let repo_matches = if repo_f.contains('/') {
342 pkg.repo == repo_f
343 } else {
344 pkg.repo.split('/').any(|part| part == repo_f)
345 };
346 if !repo_matches {
347 continue;
348 }
349 }
350 if type_filter.is_some()
351 && pkg.package_type
352 != type_filter.ok_or_else(|| {
353 anyhow!("type_filter missing despite being set")
354 })?
355 {
356 continue;
357 }
358
359 let version_display = if m.revision == "1" {
360 pkg.version.clone()
361 } else {
362 format!("{}-{}", pkg.version, m.revision)
363 };
364
365 let scope_display = format!("{:?}", m.scope).to_lowercase();
366 let row_key = format!(
367 "{}/{}/{}/{}/{}/{}/{}",
368 m.registry_handle,
369 pkg.repo,
370 pkg.name,
371 pkg.sub_package.as_deref().unwrap_or(""),
372 scope_display,
373 version_display,
374 m.revision,
375 );
376 if !seen_rows.insert(row_key) {
377 continue;
378 }
379
380 let package_display = if let Some(sub) = &pkg.sub_package {
381 format!("{}:{}", pkg.name, sub)
382 } else {
383 pkg.name
384 };
385
386 let repo_display = &pkg.repo;
387
388 table.add_row(vec![
389 Cell::new(package_display).fg(Color::Cyan),
390 Cell::new(version_display).fg(Color::Yellow),
391 Cell::new(repo_display.clone()).fg(Color::Green),
392 Cell::new(m.registry_handle).fg(Color::DarkGrey),
393 Cell::new(format!("{:?}", pkg.package_type))
394 .fg(Color::DarkGrey),
395 Cell::new(scope_display).fg(Color::DarkGrey),
396 ]);
397 found_packages = true;
398 }
399 }
400
401 if found_packages {
402 println!("{table}");
403 } else {
404 println!("No installed packages match your criteria.");
405 }
406
407 Ok(())
408}
409
410fn run_list_names(
412 all: bool,
413 registry_filter: Option<&str>,
414 repo_filter: Option<&str>,
415 type_filter: Option<types::PackageType>,
416 completion: bool
417) -> Result<()> {
418 let mut entries = Vec::new();
419 let config = config::read_config()?;
420
421 if all {
422 let mut registries = Vec::new();
423 if let Some(reg) = registry_filter {
424 registries.push(reg.to_string());
425 } else {
426 if let Some(default) = &config.default_registry {
427 registries.push(default.handle.clone());
428 }
429 for reg in &config.added_registries {
430 registries.push(reg.handle.clone());
431 }
432 }
433
434 let default_handle =
435 config.default_registry.as_ref().map(|r| &r.handle);
436
437 for handle in registries {
438 if let Ok(pkgs) =
439 crate::pkg::db::get_packages_for_completion(&handle)
440 {
441 let is_default = default_handle == Some(&handle);
442 for pkg in pkgs {
443 if let Some(repo_f) = repo_filter
444 && !pkg.repo.contains(repo_f)
445 {
446 continue;
447 }
448
449 let base_name = if is_default {
450 format!("@{}/{}", pkg.repo, pkg.name)
451 } else {
452 format!("#{}@{}/{}", handle, pkg.repo, pkg.name)
453 };
454
455 let name_with_sub = if let Some(sub) = &pkg.sub_package {
456 format!("{base_name}:{sub}")
457 } else {
458 base_name
459 };
460
461 let entry = if completion {
462 format!(
463 "{}:{}",
464 name_with_sub,
465 pkg.description.replace(':', " ")
466 )
467 } else {
468 name_with_sub
469 };
470 entries.push(entry);
471 }
472 }
473 }
474 } else {
475 let installed = if completion {
476 crate::pkg::db::list_all_packages("local")?
477 } else {
478 local::get_installed_packages().map(|v| {
479 v.into_iter()
480 .map(zoi_core::types::InstallManifest::into_package)
481 .collect()
482 })?
483 };
484
485 for pkg in installed {
486 if let Some(type_f) = type_filter
487 && pkg.package_type != type_f
488 {
489 continue;
490 }
491
492 if let Some(reg_f) = registry_filter
493 && pkg.registry_handle.as_deref() != Some(reg_f)
494 {
495 continue;
496 }
497
498 if let Some(repo_f) = repo_filter
499 && pkg.repo != repo_f
500 {
501 continue;
502 }
503
504 let name = if let Some(sub) = pkg.sub_package {
505 format!("{}:{}", pkg.name, sub)
506 } else {
507 pkg.name
508 };
509
510 let entry = if completion {
511 format!("{}:{}", name, pkg.description.replace(':', " "))
512 } else {
513 name
514 };
515 entries.push(entry);
516 }
517 }
518
519 entries.sort();
520 entries.dedup();
521 for entry in entries {
522 println!("{entry}");
523 }
524
525 Ok(())
526}
527
528fn run_list_all(
530 registry_filter: Option<&str>,
531 repo_filter: Option<&str>,
532 type_filter: Option<types::PackageType>
533) -> Result<()> {
534 let installed_pkgs = local::get_installed_packages()?
535 .into_iter()
536 .map(|p| {
537 if let Some(sub) = p.sub_package {
538 format!("{}:{}", p.name, sub)
539 } else {
540 p.name
541 }
542 })
543 .collect::<HashSet<_>>();
544
545 let config = config::read_config()?;
546
547 let mut all_available = Vec::new();
548 let mut db_failed = false;
549
550 if let Some(reg_handle) = registry_filter {
551 match crate::pkg::db::list_all_packages(reg_handle) {
552 Ok(pkgs) => all_available.extend(pkgs),
553 Err(_) => db_failed = true
554 }
555 } else {
556 let mut registries = Vec::new();
557 if let Some(default) = &config.default_registry {
558 registries.push(default.handle.clone());
559 }
560 for reg in &config.added_registries {
561 registries.push(reg.handle.clone());
562 }
563
564 let results: Vec<Result<Vec<types::Package>>> = registries
565 .into_par_iter()
566 .filter(|h| !h.is_empty())
567 .map(|handle| crate::pkg::db::list_all_packages(&handle))
568 .collect();
569
570 for res in results {
571 if let Ok(pkgs) = res {
572 all_available.extend(pkgs);
573 } else {
574 db_failed = true;
575 break;
576 }
577 }
578 }
579
580 let available_pkgs = if db_failed
581 || (all_available.is_empty()
582 && repo_filter.is_none()
583 && registry_filter.is_none())
584 {
585 if let Some(reg_handle) = registry_filter {
586 let all_repo_names = config::get_all_repos()?;
587 let full_repos: Vec<String> = all_repo_names
588 .into_iter()
589 .map(|r_name| format!("{reg_handle}/{r_name}"))
590 .filter(|full_repo_name| {
591 if let Some(repo_f) = repo_filter {
592 if repo_f.contains('/') {
593 full_repo_name == repo_f
594 } else {
595 full_repo_name.split('/').any(|part| part == repo_f)
596 }
597 } else {
598 true
599 }
600 })
601 .collect();
602 local::get_packages_from_repos(&full_repos)?
603 } else if let Some(repo_f) = repo_filter {
604 let handle = if let Some(reg) = &config.default_registry {
605 reg.handle.clone()
606 } else {
607 return Err(anyhow!("Default registry not configured."));
608 };
609 if handle.is_empty() {
610 return Err(anyhow!(
611 "Default registry handle is not set. Please run 'zoi \
612 sync'.."
613 ));
614 }
615 let all_repo_names = config::get_all_repos()?;
616 let repos_to_search: Vec<String> = all_repo_names
617 .into_iter()
618 .map(|r_name| format!("{handle}/{r_name}"))
619 .filter(|full_repo_name| {
620 if repo_f.contains('/') {
621 full_repo_name == repo_f
622 } else {
623 full_repo_name.split('/').any(|part| part == repo_f)
624 }
625 })
626 .collect();
627 local::get_packages_from_repos(&repos_to_search)?
628 } else {
629 local::get_all_available_packages()?
630 }
631 } else {
632 if let Some(rf) = repo_filter {
633 all_available.retain(|p| {
634 if rf.contains('/') {
635 p.repo == rf
636 } else {
637 p.repo.split('/').any(|part| part == rf)
638 }
639 });
640 }
641 all_available
642 };
643
644 let handle_for_version = registry_filter.or(config
645 .default_registry
646 .as_ref()
647 .map(|reg| reg.handle.as_str()));
648
649 if available_pkgs.is_empty() {
650 if let Some(repo) = repo_filter {
651 println!("No packages found in repo '{repo}'.");
652 } else {
653 println!("No packages found in active repositories.");
654 }
655 return Ok(());
656 }
657
658 let mut table = Table::new();
659 table
660 .load_style(UTF8_FULL)
661 .set_content_arrangement(ContentArrangement::Dynamic)
662 .set_header(vec![
663 Cell::new("Status").add_attribute(Attribute::Bold),
664 Cell::new("Package").add_attribute(Attribute::Bold),
665 Cell::new("Version").add_attribute(Attribute::Bold),
666 Cell::new("Repo").add_attribute(Attribute::Bold),
667 Cell::new("Type").add_attribute(Attribute::Bold),
668 ]);
669
670 for pkg in available_pkgs {
671 if type_filter.is_some()
672 && pkg.package_type
673 != type_filter.ok_or_else(|| {
674 anyhow!("type_filter missing despite being set")
675 })?
676 {
677 continue;
678 }
679
680 let version =
681 crate::pkg::resolve::get_default_version(&pkg, handle_for_version)
682 .unwrap_or_else(|_| "N/A".to_string());
683
684 let version_display = if pkg.revision == "1" {
685 version.clone()
686 } else {
687 format!("{}-{}", version, pkg.revision)
688 };
689
690 let repo_display =
691 pkg.repo.split_once('/').map_or(pkg.repo.as_str(), |x| x.1);
692
693 let full_name = if let Some(sub) = &pkg.sub_package {
694 format!("{}:{}", pkg.name, sub)
695 } else {
696 pkg.name.clone()
697 };
698
699 if let Some(subs) = &pkg.sub_packages {
700 for sub in subs {
701 let full_name_sub = format!("{}:{}", pkg.name, sub);
702 let (status_str, status_color) =
703 if installed_pkgs.contains(&full_name_sub) {
704 ("✓", Color::Green)
705 } else {
706 ("", Color::Reset)
707 };
708 table.add_row(vec![
709 Cell::new(status_str).fg(status_color),
710 Cell::new(full_name_sub).fg(Color::Cyan),
711 Cell::new(version_display.clone()).fg(Color::Yellow),
712 Cell::new(repo_display.to_string()).fg(Color::Green),
713 Cell::new(format!("{:?}", pkg.package_type))
714 .fg(Color::DarkGrey),
715 ]);
716 }
717 } else {
718 let (status_str, status_color) =
719 if installed_pkgs.contains(&full_name) {
720 ("✓", Color::Green)
721 } else {
722 ("", Color::Reset)
723 };
724 table.add_row(vec![
725 Cell::new(status_str).fg(status_color),
726 Cell::new(full_name).fg(Color::Cyan),
727 Cell::new(version_display).fg(Color::Yellow),
728 Cell::new(repo_display.to_string()).fg(Color::Green),
729 Cell::new(format!("{:?}", pkg.package_type))
730 .fg(Color::DarkGrey),
731 ]);
732 }
733 }
734
735 println!("{table}");
736 Ok(())
737}