Add use command and mapper

feature/lookup-installed
trivernis 2 years ago
parent a39df138c9
commit 1bd3e79cde
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -15,6 +15,8 @@ path = "src/main.rs"
[dependencies] [dependencies]
clap = { version = "4.1.1", features = ["derive"] } clap = { version = "4.1.1", features = ["derive"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
crossterm = "0.25.0"
dialoguer = "0.10.3"
dirs = "4.0.0" dirs = "4.0.0"
futures-util = "0.3.25" futures-util = "0.3.25"
indicatif = "0.17.3" indicatif = "0.17.3"
@ -24,6 +26,7 @@ miette = "5.5.0"
reqwest = { version = "0.11.14", features = ["json", "stream"] } reqwest = { version = "0.11.14", features = ["json", "stream"] }
semver = { version = "1.0.16", features = ["std", "serde"] } semver = { version = "1.0.16", features = ["std", "serde"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
set_env = "1.3.4"
tar = "0.4.38" tar = "0.4.38"
thiserror = "1.0.38" thiserror = "1.0.38"
tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs", "time"] } tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs", "time"] }

@ -1,6 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use nenv::repository::NodeVersion;
use semver::VersionReq; use semver::VersionReq;
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
@ -28,41 +29,11 @@ pub enum Command {
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
pub struct InstallArgs { pub struct InstallArgs {
#[arg()] #[arg()]
pub version: Version, pub version: NodeVersion,
} }
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
pub struct UseArgs { pub struct UseArgs {
#[arg()] #[arg()]
pub version: Version, pub version: NodeVersion,
}
impl FromStr for Version {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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),
} }

@ -4,6 +4,7 @@ use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
mapper::error::MapperError,
repository::{config::ConfigError, extract::ExtractError}, repository::{config::ConfigError, extract::ExtractError},
web_api::error::ApiError, web_api::error::ApiError,
}; };
@ -37,6 +38,13 @@ pub enum Error {
#[diagnostic_source] #[diagnostic_source]
ConfigError, ConfigError,
), ),
#[error("Mapper failed: {0}")]
Mapper(
#[from]
#[source]
#[diagnostic_source]
MapperError,
),
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
Io(#[from] io::Error), Io(#[from] io::Error),

@ -1,16 +1,60 @@
use crossterm::style::Stylize;
use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository}; use repository::{config::Config, NodeVersion, Repository};
mod consts; mod consts;
pub mod error; pub mod error;
pub mod mapper;
pub mod repository; pub mod repository;
mod utils; mod utils;
mod web_api; mod web_api;
use dialoguer::Confirm;
use error::Result; use error::Result;
pub async fn install_version(version: NodeVersion) -> 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> { async fn get_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await Repository::init(Config::load().await?).await
} }
async fn get_mapper() -> Result<Mapper> {
Ok(Mapper::new(get_repository().await?))
}

@ -10,20 +10,9 @@ async fn main() {
color_eyre::install().unwrap(); color_eyre::install().unwrap();
let args: Args = Args::parse(); let args: Args = Args::parse();
match args.commmand { match args.commmand {
args::Command::Install(v) => nenv::install_version(version_to_req(v.version)) args::Command::Install(v) => nenv::install_version(v.version).await.unwrap(),
.await args::Command::Use(v) => nenv::use_version(v.version).await.unwrap(),
.unwrap(),
args::Command::Use(_) => todo!(),
args::Command::Default => todo!(), args::Command::Default => todo!(),
args::Command::Version => 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),
}
}

@ -0,0 +1,17 @@
use miette::Diagnostic;
use thiserror::Error;
use crate::repository::config::ConfigError;
pub type MapperResult<T> = Result<T, MapperError>;
#[derive(Error, Diagnostic, Debug)]
pub enum MapperError {
#[error("Config error: {0}")]
Config(
#[from]
#[source]
#[diagnostic_source]
ConfigError,
),
}

@ -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<NodeVersion> {
env::var("NODE_VERSION")
.ok()
.and_then(|v| NodeVersion::from_str(&v).ok())
}
}

@ -7,10 +7,13 @@ use tokio::fs;
use crate::consts::{CFG_FILE_PATH, NODE_DIST_URL}; use crate::consts::{CFG_FILE_PATH, NODE_DIST_URL};
use super::NodeVersion;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub dist_base_url: String, pub dist_base_url: String,
pub default_version: String, #[serde(with = "NodeVersion")]
pub default_version: NodeVersion,
} }
pub type ConfigResult<T> = Result<T, ConfigError>; pub type ConfigResult<T> = Result<T, ConfigError>;
@ -41,7 +44,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
dist_base_url: String::from(NODE_DIST_URL), 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<Self> { pub async fn load() -> ConfigResult<Self> {
if !CFG_FILE_PATH.exists() { if !CFG_FILE_PATH.exists() {
let cfg = Config::default(); let cfg = Config::default();
fs::write(&*CFG_FILE_PATH, toml::to_string_pretty(&cfg)?).await?; cfg.save().await?;
Ok(cfg) Ok(cfg)
} else { } else {
@ -61,4 +64,15 @@ impl Config {
Ok(cfg) 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
}
} }

@ -1,6 +1,11 @@
use std::path::{Path, PathBuf}; use core::fmt;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::{ use tokio::{
fs::{self, File}, fs::{self, File},
io::BufWriter, io::BufWriter,
@ -18,6 +23,7 @@ pub mod config;
pub(crate) mod extract; pub(crate) mod extract;
pub mod versions; pub mod versions;
#[derive(Clone, Debug)]
pub enum NodeVersion { pub enum NodeVersion {
Latest, Latest,
LatestLts, LatestLts,
@ -25,10 +31,55 @@ pub enum NodeVersion {
Req(VersionReq), Req(VersionReq),
} }
impl FromStr for NodeVersion {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self, D::Error> {
let string = String::deserialize(deserializer)?;
Self::from_str(&string).map_err(serde::de::Error::custom)
}
pub fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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 { pub struct Repository {
versions: Versions, versions: Versions,
web_api: WebApi, web_api: WebApi,
config: Config, pub config: Config,
} }
impl Repository { impl Repository {
@ -62,9 +113,30 @@ impl Repository {
Ok(()) Ok(())
} }
/// Returns a list of installed versions
pub async fn installed_versions(&self) -> LibResult<Vec<Version>> {
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<bool> {
let info = self.parse_req(version);
Ok(self.installed_versions().await?.contains(&info.version))
}
/// Installs a specified node version /// Installs a specified node version
pub async fn install_version(&self, version_req: NodeVersion) -> LibResult<()> { pub async fn install_version(&self, version_req: &NodeVersion) -> LibResult<()> {
let info = self.parse_req(version_req); let info = self.parse_req(&version_req);
let archive_path = self.download_version(&info.version).await?; let archive_path = self.download_version(&info.version).await?;
self.extract_archive(info, &archive_path)?; self.extract_archive(info, &archive_path)?;
@ -92,7 +164,7 @@ impl Repository {
Ok(()) Ok(())
} }
fn parse_req(&self, version_req: NodeVersion) -> &VersionInfo { fn parse_req(&self, version_req: &NodeVersion) -> &VersionInfo {
match version_req { match version_req {
NodeVersion::Latest => self.versions.latest(), NodeVersion::Latest => self.versions.latest(),
NodeVersion::LatestLts => self.versions.latest_lts(), NodeVersion::LatestLts => self.versions.latest_lts(),

Loading…
Cancel
Save