Skip to main content

zoi_cli/cmd/
downgrade.rs

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