Skip to main content

zoi_cli/cmd/
list.rs

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) = &registry_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) = &registry_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) = &registry_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) = &registry_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) = &registry_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) = &registry_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}