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 == ¤t_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}