diff --git a/Cargo.toml b/Cargo.toml index ac59c79..46d8851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ path = "src/main.rs" async-trait = "0.1.62" bincode = "1.3.3" clap = { version = "4.1.1", features = ["derive"] } -color-eyre = "0.6.2" crossterm = "0.25.0" dialoguer = "0.10.3" dirs = "4.0.0" @@ -35,7 +34,6 @@ reqwest = { version = "0.11.14", features = ["json", "stream"] } semver = { version = "1.0.16", features = ["std", "serde"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" -set_env = "1.3.4" tar = "0.4.38" thiserror = "1.0.38" tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs", "time", "process"] } diff --git a/src/args.rs b/src/args.rs index 10d34a6..a375279 100644 --- a/src/args.rs +++ b/src/args.rs @@ -28,6 +28,10 @@ pub enum Command { #[command()] Install(InstallArgs), + /// Uninstalls the given node version + #[command()] + Uninstall(UninstallArgs), + /// Sets the specified version as the global default #[command()] Default(DefaultArgs), @@ -69,6 +73,12 @@ pub struct InstallArgs { pub version: NodeVersion, } +#[derive(Clone, Debug, Parser)] +pub struct UninstallArgs { + /// the version to install + pub version: NodeVersion, +} + #[derive(Clone, Debug, Parser)] pub struct DefaultArgs { /// The version to set as default diff --git a/src/consts.rs b/src/consts.rs index f4dab5a..7ab03d4 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -15,6 +15,7 @@ lazy_static! { .join(PathBuf::from("nenv")); pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml"); pub static ref VERSION_FILE_PATH: PathBuf = CACHE_DIR.join("versions.cache"); + pub static ref INSTALLED_VERSION_FILE: PathBuf = DATA_DIR.join("installed_versions"); pub static ref BIN_DIR: PathBuf = DATA_DIR.join("bin"); pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions"); pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}"); diff --git a/src/main.rs b/src/main.rs index 3d313d9..1ec8a0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ async fn main() -> Result<()> { match args.command { args::Command::Install(v) => nenv.install(v.version).await, + args::Command::Uninstall(v) => nenv.uninstall(v.version).await, args::Command::Default(v) => nenv.set_system_default(v.version).await, args::Command::Exec(args) => { let exit_code = nenv.exec(args.command, args.args).await?; diff --git a/src/nenv.rs b/src/nenv.rs index d59614e..c842973 100644 --- a/src/nenv.rs +++ b/src/nenv.rs @@ -62,6 +62,24 @@ impl Nenv { } } + #[tracing::instrument(skip(self))] + pub async fn uninstall(&mut self, version: NodeVersion) -> Result<()> { + if prompt( + false, + format!( + "Do you really want to uninstall node {}?", + version.to_string().bold() + ), + ) { + self.repo.uninstall(&version).await?; + println!("Node {} has been removed.", version.to_string().bold()) + } else { + println!("Nothing changed."); + } + + Ok(()) + } + /// Sets the system-wide default version #[tracing::instrument(skip(self))] pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> { @@ -108,8 +126,11 @@ impl Nenv { /// Lists the currently installed versions #[tracing::instrument(skip(self))] pub async fn list_versions(&mut self) -> Result<()> { - let versions = self.repo.installed_versions().await?; - let active_version = self.repo.lookup_version(&self.active_version).await?; + let versions = self.repo.installed_versions(); + let active_version = self + .repo + .lookup_remote_version(&self.active_version) + .await?; let active_version = active_version.version.into(); println!("{}", "Installed versions:".bold()); @@ -216,8 +237,7 @@ impl Nenv { async fn get_mapper(&mut self) -> Result { let node_path = self .repo - .get_version_path(&self.active_version) - .await? + .get_version_path(&self.active_version)? .ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?; Ok(Mapper::new(node_path)) } diff --git a/src/repository/downloader/versions.rs b/src/repository/downloader/versions.rs index c98f35c..816636e 100644 --- a/src/repository/downloader/versions.rs +++ b/src/repository/downloader/versions.rs @@ -16,7 +16,7 @@ use super::VersionInfo; #[derive(Clone, Serialize, Deserialize)] pub struct Versions { - lts_versions: HashMap, + lts_versions: HashMap, versions: HashMap, // as this field is not serialized // it needs to be calculated after serialization @@ -53,7 +53,7 @@ impl Versions { pub fn new(all_versions: Vec) -> Self { let lts_versions = all_versions .iter() - .filter_map(|v| Some((v.lts.lts_ref()?.to_lowercase(), v.version.major as u16))) + .filter_map(|v| Some((v.lts.lts_ref()?.to_lowercase(), v.version.major as u8))) .collect::>(); let mut sorted_versions = all_versions .iter() @@ -133,7 +133,7 @@ impl Versions { /// Returns any version that fulfills the given requirement #[tracing::instrument(level = "debug", skip(self))] - fn get_latest_for_major(&self, major: u16) -> Option<&VersionMetadata> { + fn get_latest_for_major(&self, major: u8) -> Option<&VersionMetadata> { let fulfilling_versions = self .sorted_versions .iter() diff --git a/src/repository/local_versions.rs b/src/repository/local_versions.rs index e69de29..d657ff2 100644 --- a/src/repository/local_versions.rs +++ b/src/repository/local_versions.rs @@ -0,0 +1,108 @@ +use std::{fs::File, io::Write}; + +use semver::VersionReq; +use serde::{Deserialize, Serialize}; + +use crate::{ + consts::INSTALLED_VERSION_FILE, + versioning::{SimpleVersion, VersionMetadata}, +}; +use miette::{Context, IntoDiagnostic, Result}; + +#[derive(Serialize, Deserialize, Default)] +pub struct InstalledVersions { + ordered_versions: Vec<(SimpleVersion, VersionMetadata)>, +} + +impl InstalledVersions { + pub fn new(mut versions: Vec<(SimpleVersion, VersionMetadata)>) -> Self { + versions.sort_by_key(|e| e.0); + versions.dedup_by_key(|e| e.0); + Self { + ordered_versions: versions, + } + } + + /// Loads the local versions + pub fn load() -> Result { + let reader = File::open(&*INSTALLED_VERSION_FILE) + .into_diagnostic() + .context("Opening local versions file")?; + let versions = bincode::deserialize_from(reader) + .into_diagnostic() + .context("Deserializing local versions")?; + + Ok(versions) + } + + /// Saves the local versions + pub fn save(&self) -> Result<()> { + let mut file = File::create(&*INSTALLED_VERSION_FILE) + .into_diagnostic() + .context("Opening local versions file")?; + bincode::serialize_into(&mut file, &self) + .into_diagnostic() + .context("Serializing local versions")?; + file.flush() + .into_diagnostic() + .context("Flushing local versions to file")?; + + Ok(()) + } + + /// Inserts a new version. This requires reordering the list + pub fn insert(&mut self, version: (SimpleVersion, VersionMetadata)) { + self.ordered_versions.push(version); + self.ordered_versions.sort_by_key(|e| e.0); + self.ordered_versions.dedup_by_key(|e| e.0); + } + + /// Removes a version. This keeps the order intact + pub fn remove(&mut self, version: &SimpleVersion) { + self.ordered_versions.retain(|(v, _)| v != version) + } + + pub fn all(&self) -> Vec<&SimpleVersion> { + self.ordered_versions.iter().map(|(v, _)| v).collect() + } + + pub fn latest(&self) -> Option<&VersionMetadata> { + self.ordered_versions.last().map(|(_, m)| m) + } + + pub fn latest_lts(&self) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter(|(_, m)| m.lts.is_some()) + .last() + .map(|(_, m)| m) + } + + pub fn lts>(&self, lts: S) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter_map(|(v, m)| Some((v, m.lts.clone()?, m))) + .filter(|(_, n, _)| n == lts.as_ref()) + .last() + .map(|(_, _, m)| m) + } + + pub fn fulfilling(&self, req: &VersionReq) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter(|(v, _)| req.matches(&v.to_owned().into())) + .last() + .map(|(_, m)| m) + } +} + +impl From> for InstalledVersions { + fn from(versions: Vec) -> Self { + let versions = versions + .into_iter() + .map(|v| (v.version.to_owned(), v)) + .collect::>(); + + Self::new(versions) + } +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs index d5f5f6e..f7d91e2 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -13,10 +13,11 @@ use crate::{ versioning::{SimpleVersion, VersionMetadata}, }; -use miette::{IntoDiagnostic, Result}; +use miette::{Context, IntoDiagnostic, Result}; use self::{ downloader::{versions::Versions, NodeDownloader}, + local_versions::InstalledVersions, node_path::NodePath, }; @@ -91,6 +92,7 @@ impl fmt::Display for NodeVersion { pub struct Repository { downloader: NodeDownloader, + installed_versions: InstalledVersions, } impl Repository { @@ -98,9 +100,24 @@ impl Repository { #[tracing::instrument(level = "debug", skip_all)] pub async fn init(config: ConfigAccess) -> Result { Self::create_folders().await?; - let downloader = NodeDownloader::new(config.clone()); + let mut downloader = NodeDownloader::new(config.clone()); + + let installed_versions = match InstalledVersions::load() { + Ok(v) => v, + Err(_) => { + let installed: InstalledVersions = + load_installed_versions_info(downloader.versions().await?) + .await? + .into(); + installed.save()?; + installed + } + }; - Ok(Self { downloader }) + Ok(Self { + downloader, + installed_versions, + }) } #[tracing::instrument(level = "debug")] @@ -132,8 +149,8 @@ impl Repository { /// Returns the path for the given node version #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_version_path(&mut self, version: &NodeVersion) -> Result> { - let info = self.lookup_version(version).await?; + pub fn get_version_path(&mut self, version: &NodeVersion) -> Result> { + let info = self.lookup_local_version(version)?; let path = build_version_path(&info.version); Ok(if path.exists() { @@ -144,24 +161,22 @@ impl Repository { } /// Returns a list of installed versions - #[tracing::instrument(level = "debug", skip(self))] - pub async fn installed_versions(&self) -> Result> { - let mut versions = Vec::new(); - let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; - - while let Some(entry) = iter.next_entry().await.into_diagnostic()? { - if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { - versions.push(version); - }; - } - - Ok(versions) + pub fn installed_versions(&self) -> Vec { + self.installed_versions + .all() + .into_iter() + .map(|v| v.clone().into()) + .collect() } /// Returns if the given version is installed #[tracing::instrument(level = "debug", skip(self))] pub async fn is_installed(&mut self, version: &NodeVersion) -> Result { - let info = self.lookup_version(version).await?; + let info = if let Ok(v) = self.lookup_local_version(version) { + v + } else { + self.lookup_remote_version(version).await? + }; Ok(build_version_path(&info.version).exists()) } @@ -169,15 +184,40 @@ impl Repository { /// Installs the given node version #[tracing::instrument(level = "debug", skip(self))] pub async fn install_version(&mut self, version: &NodeVersion) -> Result<()> { - let info = self.lookup_version(version).await?.to_owned(); + let info = self.lookup_remote_version(version).await?.to_owned(); self.downloader.download(&info.version).await?; + self.installed_versions.insert((info.version, info)); + self.installed_versions.save()?; + + Ok(()) + } + + /// Uninstalls the given node version by deleting the versions directory + #[tracing::instrument(level = "debug", skip(self))] + pub async fn uninstall(&mut self, version: &NodeVersion) -> Result<()> { + let info = self.lookup_local_version(version)?.clone(); + let version_dir = NODE_VERSIONS_DIR.join(info.version.to_string()); + + if !version_dir.exists() { + return Err(VersionError::not_installed(version).into()); + } + + fs::remove_dir_all(version_dir) + .await + .into_diagnostic() + .context("Deleting node version")?; + self.installed_versions.remove(&info.version); + self.installed_versions.save()?; Ok(()) } /// Performs a lookup for the given node version #[tracing::instrument(level = "debug", skip(self))] - pub async fn lookup_version(&mut self, version_req: &NodeVersion) -> Result<&VersionMetadata> { + pub async fn lookup_remote_version( + &mut self, + version_req: &NodeVersion, + ) -> Result<&VersionMetadata> { let versions = self.downloader.versions().await?; let version = match version_req { @@ -194,6 +234,28 @@ impl Repository { Ok(version) } + /// Performs a lookup for the given node version + #[tracing::instrument(level = "debug", skip(self))] + pub fn lookup_local_version(&self, version_req: &NodeVersion) -> Result<&VersionMetadata> { + let versions = &self.installed_versions; + let version = match version_req { + NodeVersion::Latest => versions + .latest() + .ok_or_else(|| VersionError::not_installed("latest"))?, + NodeVersion::LatestLts => versions + .latest_lts() + .ok_or_else(|| VersionError::not_installed("lts"))?, + NodeVersion::Lts(lts) => versions + .lts(lts) + .ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?, + NodeVersion::Req(req) => versions + .fulfilling(req) + .ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?, + }; + + Ok(version) + } + /// Returns the reference to all known versions #[tracing::instrument(level = "debug", skip(self))] pub async fn all_versions(&mut self) -> Result<&Versions> { @@ -206,3 +268,21 @@ fn build_version_path(version: &SimpleVersion) -> PathBuf { .join(version.to_string()) .join(format!("node-v{}-{}-{}", version, OS, ARCH)) } + +async fn load_installed_versions_info(versions: &Versions) -> Result> { + let mut installed_versions = Vec::new(); + let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; + + while let Some(entry) = iter.next_entry().await.into_diagnostic()? { + if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { + installed_versions.push(version); + }; + } + let versions = installed_versions + .into_iter() + .filter_map(|v| versions.get(&v)) + .cloned() + .collect(); + + Ok(versions) +} diff --git a/src/utils.rs b/src/utils.rs index 0081b27..2a2c789 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,7 +3,7 @@ use std::{ time::Duration, }; -use dialoguer::Confirm; +use dialoguer::{theme::ColorfulTheme, Confirm}; use indicatif::{ProgressBar, ProgressStyle}; pub fn progress_bar(total: u64) -> ProgressBar { @@ -32,7 +32,7 @@ pub fn progress_spinner() -> ProgressBar { } pub fn prompt(default: bool, prompt: S) -> bool { - Confirm::new() + Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(prompt.to_string()) .default(default) .interact() diff --git a/src/versioning.rs b/src/versioning.rs index bf7b860..047a643 100644 --- a/src/versioning.rs +++ b/src/versioning.rs @@ -6,9 +6,9 @@ use crate::repository::downloader::VersionInfo; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize, Hash)] pub struct SimpleVersion { - pub major: u16, - pub minor: u16, - pub patch: u32, + pub major: u8, + pub minor: u8, + pub patch: u16, } #[derive(Clone, Serialize, Deserialize)] @@ -22,9 +22,9 @@ pub struct VersionMetadata { impl From for SimpleVersion { fn from(value: semver::Version) -> Self { Self { - major: value.major as u16, - minor: value.minor as u16, - patch: value.patch as u32, + major: value.major as u8, + minor: value.minor as u8, + patch: value.patch as u16, } } }