1use 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
15pub 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 == ¤t_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}