diff --git a/Cargo.toml b/Cargo.toml index 839050c..c89733a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ path = "src/main.rs" [dependencies] clap = { version = "4.1.1", features = ["derive"] } color-eyre = "0.6.2" +crossterm = "0.25.0" +dialoguer = "0.10.3" dirs = "4.0.0" futures-util = "0.3.25" indicatif = "0.17.3" @@ -24,6 +26,7 @@ miette = "5.5.0" reqwest = { version = "0.11.14", features = ["json", "stream"] } semver = { version = "1.0.16", features = ["std", "serde"] } serde = { version = "1.0.152", features = ["derive"] } +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"] } diff --git a/src/args.rs b/src/args.rs index 5a52b4f..547474e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use clap::{Parser, Subcommand}; +use nenv::repository::NodeVersion; use semver::VersionReq; #[derive(Clone, Debug, Parser)] @@ -28,41 +29,11 @@ pub enum Command { #[derive(Clone, Debug, Parser)] pub struct InstallArgs { #[arg()] - pub version: Version, + pub version: NodeVersion, } #[derive(Clone, Debug, Parser)] pub struct UseArgs { #[arg()] - pub version: Version, -} - -impl FromStr for Version { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let input = s.to_lowercase(); - - let version = match &*input { - "latest" => Self::Latest, - "lts" => Self::LatestLts, - _ => { - if let Ok(req) = VersionReq::parse(s) { - Self::Req(req) - } else { - Self::Lts(s.to_lowercase()) - } - } - }; - - Ok(version) - } -} - -#[derive(Clone, Debug)] -pub enum Version { - Latest, - LatestLts, - Lts(String), - Req(VersionReq), + pub version: NodeVersion, } diff --git a/src/error.rs b/src/error.rs index 9d22005..e718b72 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,7 @@ use miette::Diagnostic; use thiserror::Error; use crate::{ + mapper::error::MapperError, repository::{config::ConfigError, extract::ExtractError}, web_api::error::ApiError, }; @@ -37,6 +38,13 @@ pub enum Error { #[diagnostic_source] ConfigError, ), + #[error("Mapper failed: {0}")] + Mapper( + #[from] + #[source] + #[diagnostic_source] + MapperError, + ), #[error("IO Error: {0}")] Io(#[from] io::Error), diff --git a/src/lib.rs b/src/lib.rs index d90b77e..15315fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,60 @@ +use crossterm::style::Stylize; +use mapper::Mapper; use repository::{config::Config, NodeVersion, Repository}; mod consts; pub mod error; +pub mod mapper; pub mod repository; mod utils; mod web_api; +use dialoguer::Confirm; use error::Result; pub async fn install_version(version: NodeVersion) -> Result<()> { - get_repository().await?.install_version(version).await + let repo = get_repository().await?; + + if repo.is_installed(&version).await? { + if !Confirm::new() + .with_prompt("The version {version} is already installed. Reinstall?") + .default(false) + .interact() + .unwrap() + { + return Ok(()); + } + } + repo.install_version(&version).await?; + println!("Installed {}", version.to_string().bold()); + + Ok(()) +} + +pub async fn use_version(version: NodeVersion) -> Result<()> { + let mut mapper = get_mapper().await?; + + if !mapper.repository().is_installed(&version).await? + && Confirm::new() + .with_prompt(format!( + "The version {version} is not installed. Do you want to install it?" + )) + .default(false) + .interact() + .unwrap() + { + mapper.repository().install_version(&version).await?; + } + + mapper.use_version(&version).await?; + println!("Now using {}", version.to_string().bold()); + + Ok(()) } async fn get_repository() -> Result { Repository::init(Config::load().await?).await } + +async fn get_mapper() -> Result { + Ok(Mapper::new(get_repository().await?)) +} diff --git a/src/main.rs b/src/main.rs index 152c8d4..1ba86a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,20 +10,9 @@ async fn main() { color_eyre::install().unwrap(); let args: Args = Args::parse(); match args.commmand { - args::Command::Install(v) => nenv::install_version(version_to_req(v.version)) - .await - .unwrap(), - args::Command::Use(_) => todo!(), + args::Command::Install(v) => nenv::install_version(v.version).await.unwrap(), + args::Command::Use(v) => nenv::use_version(v.version).await.unwrap(), args::Command::Default => todo!(), args::Command::Version => todo!(), }; } - -fn version_to_req(version: args::Version) -> NodeVersion { - match version { - args::Version::Latest => NodeVersion::Latest, - args::Version::LatestLts => NodeVersion::LatestLts, - args::Version::Req(req) => NodeVersion::Req(req), - args::Version::Lts(lts_name) => NodeVersion::Lts(lts_name), - } -} diff --git a/src/mapper/error.rs b/src/mapper/error.rs new file mode 100644 index 0000000..90a4eb5 --- /dev/null +++ b/src/mapper/error.rs @@ -0,0 +1,17 @@ +use miette::Diagnostic; +use thiserror::Error; + +use crate::repository::config::ConfigError; + +pub type MapperResult = Result; + +#[derive(Error, Diagnostic, Debug)] +pub enum MapperError { + #[error("Config error: {0}")] + Config( + #[from] + #[source] + #[diagnostic_source] + ConfigError, + ), +} diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs new file mode 100644 index 0000000..b8050df --- /dev/null +++ b/src/mapper/mod.rs @@ -0,0 +1,49 @@ +use std::{env, str::FromStr}; + +use crate::repository::{NodeVersion, Repository}; + +use self::error::MapperResult; + +pub mod error; +/// Responsible for mapping to node executables +/// and managing node versions +pub struct Mapper { + repo: Repository, + active_version: NodeVersion, +} + +impl Mapper { + pub fn new(repository: Repository) -> Self { + let version = + Self::get_version().unwrap_or_else(|| repository.config.default_version.to_owned()); + Self { + repo: repository, + active_version: version, + } + } + + pub fn repository(&self) -> &Repository { + &self.repo + } + + /// Sets the given version as the default one + pub async fn use_version(&mut self, version: &NodeVersion) -> MapperResult<()> { + self.repo + .config + .set_default_version(version.clone()) + .await?; + self.active_version = version.clone(); + + Ok(()) + } + + pub fn active_version(&self) -> &NodeVersion { + &self.active_version + } + + fn get_version() -> Option { + env::var("NODE_VERSION") + .ok() + .and_then(|v| NodeVersion::from_str(&v).ok()) + } +} diff --git a/src/repository/config.rs b/src/repository/config.rs index e2c1c51..f32c4d7 100644 --- a/src/repository/config.rs +++ b/src/repository/config.rs @@ -7,10 +7,13 @@ use tokio::fs; use crate::consts::{CFG_FILE_PATH, NODE_DIST_URL}; +use super::NodeVersion; + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { pub dist_base_url: String, - pub default_version: String, + #[serde(with = "NodeVersion")] + pub default_version: NodeVersion, } pub type ConfigResult = Result; @@ -41,7 +44,7 @@ impl Default for Config { fn default() -> Self { Self { dist_base_url: String::from(NODE_DIST_URL), - default_version: String::from("latest"), + default_version: NodeVersion::LatestLts, } } } @@ -51,7 +54,7 @@ impl Config { pub async fn load() -> ConfigResult { if !CFG_FILE_PATH.exists() { let cfg = Config::default(); - fs::write(&*CFG_FILE_PATH, toml::to_string_pretty(&cfg)?).await?; + cfg.save().await?; Ok(cfg) } else { @@ -61,4 +64,15 @@ impl Config { Ok(cfg) } } + + pub async fn save(&self) -> ConfigResult<()> { + fs::write(&*CFG_FILE_PATH, toml::to_string_pretty(&self)?).await?; + + Ok(()) + } + + pub async fn set_default_version(&mut self, default_version: NodeVersion) -> ConfigResult<()> { + self.default_version = default_version; + self.save().await + } } diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 8b46c90..1493652 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,6 +1,11 @@ -use std::path::{Path, PathBuf}; +use core::fmt; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; use semver::{Version, VersionReq}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::{ fs::{self, File}, io::BufWriter, @@ -18,6 +23,7 @@ pub mod config; pub(crate) mod extract; pub mod versions; +#[derive(Clone, Debug)] pub enum NodeVersion { Latest, LatestLts, @@ -25,10 +31,55 @@ pub enum NodeVersion { Req(VersionReq), } +impl FromStr for NodeVersion { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let input = s.to_lowercase(); + + let version = match &*input { + "latest" => Self::Latest, + "lts" => Self::LatestLts, + _ => { + if let Ok(req) = VersionReq::parse(s) { + Self::Req(req) + } else { + Self::Lts(s.to_lowercase()) + } + } + }; + + Ok(version) + } +} + +impl NodeVersion { + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let string = String::deserialize(deserializer)?; + Self::from_str(&string).map_err(serde::de::Error::custom) + } + + pub fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl fmt::Display for NodeVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NodeVersion::Latest => String::from("latest"), + NodeVersion::LatestLts => String::from("lts"), + NodeVersion::Lts(name) => name.to_owned(), + NodeVersion::Req(req) => req.to_string(), + } + .fmt(f) + } +} + pub struct Repository { versions: Versions, web_api: WebApi, - config: Config, + pub config: Config, } impl Repository { @@ -62,9 +113,30 @@ impl Repository { Ok(()) } + /// Returns a list of installed versions + pub async fn installed_versions(&self) -> LibResult> { + let mut versions = Vec::new(); + let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await?; + + while let Some(entry) = iter.next_entry().await? { + if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { + versions.push(version); + }; + } + + Ok(versions) + } + + /// Returns if the given version is installed + pub async fn is_installed(&self, version: &NodeVersion) -> LibResult { + let info = self.parse_req(version); + + Ok(self.installed_versions().await?.contains(&info.version)) + } + /// Installs a specified node version - pub async fn install_version(&self, version_req: NodeVersion) -> LibResult<()> { - let info = self.parse_req(version_req); + pub async fn install_version(&self, version_req: &NodeVersion) -> LibResult<()> { + let info = self.parse_req(&version_req); let archive_path = self.download_version(&info.version).await?; self.extract_archive(info, &archive_path)?; @@ -92,7 +164,7 @@ impl Repository { Ok(()) } - fn parse_req(&self, version_req: NodeVersion) -> &VersionInfo { + fn parse_req(&self, version_req: &NodeVersion) -> &VersionInfo { match version_req { NodeVersion::Latest => self.versions.latest(), NodeVersion::LatestLts => self.versions.latest_lts(),