Skip to main content

zoi_cli/cmd/
downgrade.rs

1//! Implementation of the `downgrade` command for switching to older package
2//! versions.
3
4use std::collections::HashSet;
5use std::fs;
6
7use anyhow::{Result, anyhow};
8use colored::Colorize;
9use dialoguer::Select;
10use dialoguer::theme::ColorfulTheme;
11use semver::Version;
12
13use crate::pkg::{cache, local, resolve, types};
14
15/// Runs the `downgrade` command to switch to an older version of a package.
16///
17/// # Errors
18///
19/// Returns an error if the package cannot be resolved, no other versions are
20/// found, or the installation of the selected version fails.
21pub fn run(
22    package_name: &str,
23    yes: bool,
24    plugin_manager: Option<&crate::pkg::plugin::PluginManager>
25) -> Result<()> {
26    println!(
27        "{} Downgrading package '{}'...",
28        "::".bold().blue(),
29        package_name.cyan().bold()
30    );
31
32    let _request = resolve::parse_source_string(package_name)?;
33    let (pkg, current_version, _, _, registry_handle, _, _) =
34        resolve::resolve_package_and_version(package_name, None, true, false)?;
35
36    let mut versions = HashSet::new();
37
38    let handle = registry_handle.as_deref().unwrap_or("local");
39    if let Ok(package_dir) =
40        local::get_package_dir(types::Scope::User, handle, &pkg.repo, &pkg.name)
41        && let Ok(entries) = fs::read_dir(package_dir)
42    {
43        for entry in entries.flatten() {
44            let path = entry.path();
45            if path.is_dir()
46                && let Some(v_str) = path.file_name().and_then(|s| s.to_str())
47                && v_str != "latest"
48                && v_str != "dependents"
49                && Version::parse(v_str).is_ok()
50            {
51                versions.insert(v_str.to_string());
52            }
53        }
54    }
55
56    if let Ok(archive_cache) = cache::get_archive_cache_root()
57        && let Ok(entries) = fs::read_dir(archive_cache)
58    {
59        for entry in entries.flatten() {
60            let filename = entry.file_name().to_string_lossy().to_string();
61            if filename.starts_with(&pkg.name)
62                && std::path::Path::new(&filename)
63                    .extension()
64                    .is_some_and(|ext| ext.eq_ignore_ascii_case("zpa"))
65            {
66                let parts: Vec<&str> = filename.split('-').collect();
67                if let Some(v_str) = parts.get(1)
68                    && Version::parse(v_str).is_ok()
69                {
70                    versions.insert((*v_str).to_string());
71                }
72            }
73        }
74    }
75
76    if let Some(versions_map) = &pkg.versions {
77        for channel in versions_map.keys() {
78            if channel != "stable" {
79                versions.insert(format!("@{channel}"));
80            }
81        }
82    }
83
84    if versions.is_empty() {
85        return Err(anyhow!(
86            "No other versions found for '{}' in local store or cache.",
87            pkg.name
88        ));
89    }
90
91    let mut sorted_versions: Vec<String> = versions.into_iter().collect();
92    sorted_versions.sort_by(|a, b| {
93        let va = Version::parse(a.trim_start_matches('@'));
94        let vb = Version::parse(b.trim_start_matches('@'));
95        match (va, vb) {
96            (Ok(a), Ok(b)) => b.cmp(&a),
97            _ => b.cmp(a)
98        }
99    });
100
101    println!("Currently installed: {}", current_version.yellow());
102
103    if yes {
104        return Err(anyhow!(
105            "Downgrade requires interactive version selection. Use 'zoi \
106             install {}@<version>' for non-interactive scripts.",
107            pkg.name
108        ));
109    }
110
111    let selection = Select::with_theme(&ColorfulTheme::default())
112        .with_prompt("Select version to install")
113        .items(&sorted_versions)
114        .default(0)
115        .interact_opt()?
116        .ok_or_else(|| anyhow!("No version selected."))?;
117
118    let selected_version = sorted_versions
119        .get(selection)
120        .ok_or_else(|| anyhow!("Invalid selection index"))?;
121
122    if selected_version == &current_version {
123        println!("Version {selected_version} is already installed.");
124        return Ok(());
125    }
126
127    let install_source = format!(
128        "{}@{}",
129        package_name
130            .split('@')
131            .next()
132            .ok_or_else(|| anyhow!("Invalid package name: '{package_name}'"))?,
133        selected_version
134    );
135
136    println!(
137        "
138Initiating install for {}...",
139        install_source.cyan()
140    );
141
142    crate::cmd::install::run(
143        &[install_source],
144        None,
145        true,
146        false,
147        yes,
148        Some(crate::cli::InstallScope::User),
149        false,
150        false,
151        false,
152        None,
153        false,
154        plugin_manager,
155        false,
156        false,
157        false,
158        false,
159        3,
160        false,
161        false,
162        None
163    )
164}