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