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