Compare commits

...

12 Commits

@ -21,10 +21,10 @@ 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"
envmnt = "0.10.4"
futures = "0.3.25"
futures-util = "0.3.25"
indicatif = "0.17.3"
@ -35,7 +35,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"] }

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@ -10,6 +10,10 @@ pub struct Args {
#[arg(long)]
pub verbose: bool,
/// Overrides all versions found in the environment and uses this one instead
#[arg(long)]
pub use_version: Option<NodeVersion>,
#[command(subcommand)]
pub command: Command,
}
@ -28,9 +32,13 @@ 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),
SetDefault(DefaultArgs),
/// Creates wrapper scripts for node binaries
/// so they can be found in the path and are executed
@ -69,6 +77,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

@ -71,6 +71,7 @@ impl ConfigAccess {
let cfg = toml::from_str(&cfg_string)
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
tracing::debug!("{cfg:?}");
Ok(Self::new(cfg))
}

@ -1,3 +1,5 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{consts::NODE_DIST_URL, repository::NodeVersion};
@ -9,6 +11,10 @@ pub struct Config {
/// Configuration for how to download node versions
pub download: DownloadConfig,
/// List of executables that are hardwired to a given node version
/// and can still be executed from other versions with this given version.
pub bins: HashMap<String, ExecutableConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -23,6 +29,14 @@ pub struct DownloadConfig {
pub dist_base_url: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExecutableConfig {
/// The node version to run this executable with.
/// This means that whatever the currently active version is
/// the given executable will always be executed with the configured one.
pub node_version: NodeVersion,
}
impl Default for NodeConfig {
fn default() -> Self {
Self {

@ -2,6 +2,10 @@ use lazy_static::lazy_static;
use std::path::PathBuf;
pub const NODE_DIST_URL: &str = "https://nodejs.org/dist";
#[cfg(not(windows))]
pub const SEARCH_PATH_SEPARATOR: &str = ":";
#[cfg(windows)]
pub const SEARCH_PATH_SEPARATOR: &str = ";";
lazy_static! {
pub static ref CFG_DIR: PathBuf = dirs::config_dir()
@ -15,6 +19,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}");

@ -19,8 +19,14 @@ pub struct VersionError {
impl VersionError {
pub fn new<S1: ToString, S2: ToString>(src: S1, detail: S2) -> Self {
let src = src.to_string();
let pos = (0, src.len()).into();
let mut src = src.to_string();
let mut pos = (0, src.len()).into();
let clean_src = src.trim_start_matches('^');
if let Some((arg_str, arg_pos)) = find_in_args(clean_src) {
pos = arg_pos;
src = arg_str;
}
Self {
src,
@ -40,6 +46,13 @@ impl VersionError {
pub fn not_installed<S: ToString>(src: S) -> Self {
Self::new(src, "The version is not installed.")
}
pub fn unsupported<S: ToString>(src: S) -> Self {
Self::new(
src,
"This type of version string is not supported with this operation.",
)
}
}
#[derive(Debug, Error, Diagnostic)]
@ -132,14 +145,18 @@ pub struct CommandNotFoundError {
impl CommandNotFoundError {
pub fn new(command: String, args: Vec<OsString>, path: PathBuf) -> Self {
let pos = (0, command.len()).into();
let full_command = format!(
"{command} {}",
args.into_iter()
.map(|a| a.into_string().unwrap_or_default())
.collect::<Vec<_>>()
.join(" ")
);
let (full_command, pos) = find_in_args(&command).unwrap_or_else(|| {
let pos = (0, command.len()).into();
let full_command = format!(
"{command} {}",
args.into_iter()
.map(|a| a.into_string().unwrap_or_default())
.collect::<Vec<_>>()
.join(" ")
);
(full_command, pos)
});
Self {
command,
full_command,
@ -161,3 +178,11 @@ pub struct MapDirError {
#[source]
pub caused_by: std::io::Error,
}
pub fn find_in_args(query: &str) -> Option<(String, SourceSpan)> {
let args_string = std::env::args().fold(String::new(), |s, acc| format!("{s} {acc}"));
args_string
.find(query)
.map(|index| (args_string, (index, query.len()).into()))
}

@ -1,4 +1,4 @@
use std::process;
use std::{env, process};
use args::Args;
use clap::Parser;
@ -11,6 +11,7 @@ pub mod mapper;
pub mod repository;
mod utils;
use miette::Result;
use repository::NodeVersion;
use tracing::metadata::LevelFilter;
use tracing_subscriber::fmt::format::FmtSpan;
use xkcd_unreachable::xkcd_unreachable;
@ -35,11 +36,12 @@ async fn main() -> Result<()> {
return Ok(());
}
let mut nenv = get_nenv().await?;
let mut nenv = get_nenv(args.use_version.clone()).await?;
match args.command {
args::Command::Install(v) => nenv.install(v.version).await,
args::Command::Default(v) => nenv.set_system_default(v.version).await,
args::Command::Uninstall(v) => nenv.uninstall(v.version).await,
args::Command::SetDefault(v) => nenv.set_system_default(v.version).await,
args::Command::Exec(args) => {
let exit_code = nenv.exec(args.command, args.args).await?;
@ -61,8 +63,8 @@ fn print_version() {
println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
async fn get_nenv() -> Result<Nenv> {
Nenv::init().await
async fn get_nenv(version_override: Option<NodeVersion>) -> Result<Nenv> {
Nenv::init(version_override).await
}
fn init_tracing() {

@ -1,4 +1,5 @@
use std::{
env,
ffi::OsString,
path::PathBuf,
process::{ExitStatus, Stdio},
@ -8,6 +9,7 @@ use crate::error::CommandNotFoundError;
use miette::{Context, IntoDiagnostic, Result};
use tokio::process::Command;
#[derive(Debug)]
pub struct MappedCommand {
name: String,
path: PathBuf,
@ -19,10 +21,11 @@ impl MappedCommand {
Self { name, path, args }
}
#[tracing::instrument(skip_all, level = "debug")]
#[tracing::instrument(level = "debug")]
pub async fn run(mut self) -> Result<ExitStatus> {
self.adjust_path()?;
let exit_status = Command::new(self.path)
.envs(env::vars_os())
.args(self.args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())

@ -1,22 +1,26 @@
use std::{collections::HashSet, io, path::Path};
use std::{
collections::HashSet,
io,
path::{Path, PathBuf},
};
use tokio::fs::{self, DirEntry};
use miette::miette;
use tokio::fs;
use crate::{consts::BIN_DIR, error::MapDirError, repository::node_path::NodePath};
use miette::{Context, IntoDiagnostic, Result};
struct NodeApp {
info: DirEntry,
pub struct NodeApp {
path: PathBuf,
name: String,
}
impl NodeApp {
pub fn new(info: DirEntry) -> Self {
let path = info.path();
pub fn new(path: PathBuf) -> Self {
let name = path.file_stem().unwrap();
let name = name.to_string_lossy().into_owned();
Self { info, name }
Self { path, name }
}
pub fn name(&self) -> &String {
@ -25,36 +29,55 @@ impl NodeApp {
/// creates wrappers to map this application
pub async fn map_executable(&self) -> Result<()> {
let src_path = BIN_DIR.join(self.info.file_name());
let src_path = BIN_DIR.join(
self.path
.file_name()
.ok_or_else(|| miette!("The given path is not a file."))?,
);
self.write_wrapper_script(&src_path)
.await
.into_diagnostic()
.context("Creating executable wrapper script")
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(windows))]
async fn write_wrapper_script(&self, path: &Path) -> Result<(), io::Error> {
fs::write(path, format!("#!/bin/sh\nnenv exec {} \"$@\"", self.name)).await?;
let src_metadata = self.info.metadata().await?;
let src_metadata = self.path.metadata()?;
fs::set_permissions(&path, src_metadata.permissions()).await?;
Ok(())
}
#[cfg(target_os = "windows")]
#[cfg(windows)]
async fn write_wrapper_script(&self, path: &Path) -> Result<(), io::Error> {
fs::write(
path.with_extension("bat"),
format!("@echo off\nnenv exec {} %*", self.name),
)
.await?;
let src_metadata = self.info.metadata().await?;
let src_metadata = self.path.metadata()?;
fs::set_permissions(&path, src_metadata.permissions()).await?;
Ok(())
}
}
pub async fn map_direct(paths: Vec<PathBuf>) -> Result<()> {
let results = futures::future::join_all(
paths
.into_iter()
.map(NodeApp::new)
.map(|n| async move { n.map_executable().await }),
)
.await;
results
.into_iter()
.fold(Result::Ok(()), |acc, res| acc.and(res))?;
Ok(())
}
pub async fn map_node_bin(node_path: &NodePath) -> Result<()> {
let mapped_app_names = get_applications(&BIN_DIR)
.await?
@ -87,19 +110,19 @@ async fn get_applications(path: &Path) -> Result<Vec<NodeApp>> {
let entry_path = entry.path();
if entry_path.is_file() && !exclude_path(&entry_path) {
files.push(NodeApp::new(entry));
files.push(NodeApp::new(entry.path()));
}
}
Ok(files)
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(windows))]
fn exclude_path(_path: &Path) -> bool {
false
}
#[cfg(target_os = "windows")]
#[cfg(windows)]
fn exclude_path(path: &Path) -> bool {
let Some(extension) = path.extension() else {
return true;

@ -1,10 +1,17 @@
use std::{ffi::OsString, process::ExitStatus};
use std::{env, ffi::OsString, process::ExitStatus};
use envmnt::ListOptions;
use tokio::fs;
use crate::{consts::BIN_DIR, repository::node_path::NodePath};
use crate::{
consts::{BIN_DIR, SEARCH_PATH_SEPARATOR},
repository::node_path::NodePath,
};
use self::{mapped_command::MappedCommand, mapped_dir::map_node_bin};
use self::{
mapped_command::MappedCommand,
mapped_dir::{map_direct, map_node_bin},
};
use miette::{IntoDiagnostic, Result};
mod mapped_command;
@ -23,6 +30,7 @@ impl Mapper {
/// Executes a mapped command with the given node environment
#[tracing::instrument(level = "debug", skip(self))]
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<ExitStatus> {
self.set_env();
let executable = self.node_path.bin().join(&command);
let exit_status = MappedCommand::new(command, executable, args).run().await?;
self.remap_additive().await?;
@ -46,4 +54,29 @@ impl Mapper {
Ok(())
}
/// Maps all binaries
pub async fn map_bins(&self, binaries: Vec<(String, NodePath)>) -> Result<()> {
map_direct(
binaries
.into_iter()
.map(|(cmd, path)| path.bin().join(cmd))
.collect(),
)
.await
}
fn set_env(&self) {
env::set_var(
"NODE_PATH",
self.node_path.node_modules().to_string_lossy().to_string(),
);
let list_options = ListOptions {
separator: Some(SEARCH_PATH_SEPARATOR.to_string()),
ignore_empty: true,
};
let mut path_env = envmnt::get_list_with_options("PATH", &list_options).unwrap_or_default();
path_env.insert(0, self.node_path.bin().to_string_lossy().to_string());
envmnt::set_list_with_options("PATH", &path_env, &list_options);
}
}

@ -5,7 +5,7 @@ use crate::{
consts::{BIN_DIR, CACHE_DIR, VERSION_FILE_PATH},
error::VersionError,
mapper::Mapper,
repository::{NodeVersion, Repository},
repository::{node_path::NodePath, NodeVersion, Repository},
utils::prompt,
version_detection::{self, VersionDetector},
};
@ -22,11 +22,16 @@ pub struct Nenv {
impl Nenv {
#[tracing::instrument(level = "debug")]
pub async fn init() -> Result<Self> {
pub async fn init(version_override: Option<NodeVersion>) -> Result<Self> {
let config = ConfigAccess::load().await?;
let repo = Repository::init(config.clone()).await?;
let default_version = { config.get().await.node.default_version.to_owned() };
let active_version = Self::get_active_version().await.unwrap_or(default_version);
let active_version = if let Some(version) = version_override {
version
} else {
Self::get_active_version().await.unwrap_or(default_version)
};
Ok(Self {
config,
@ -55,13 +60,37 @@ impl Nenv {
} else {
self.repo.install_version(&version).await?;
self.active_version = version.to_owned();
self.get_mapper().await?.remap_additive().await?;
let mapper = self.get_mapper().await?;
mapper.remap_additive().await?;
mapper
.map_bins(self.get_binaries_with_path().await?)
.await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
}
#[tracing::instrument(skip(self))]
pub async fn uninstall(&mut self, version: NodeVersion) -> Result<()> {
self.repo.lookup_local_version(&version)?;
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<()> {
@ -91,6 +120,9 @@ impl Nenv {
/// Executes a given node executable for the currently active version
#[tracing::instrument(skip(self))]
pub async fn exec(&mut self, command: String, args: Vec<OsString>) -> Result<i32> {
if let Some(cfg) = self.config.get().await.bins.get(&command) {
self.active_version = cfg.node_version.to_owned();
}
if !self.repo.is_installed(&self.active_version).await? {
self.repo.install_version(&self.active_version).await?;
}
@ -102,14 +134,19 @@ impl Nenv {
/// Clears the version cache and remaps all executables
#[tracing::instrument(skip(self))]
pub async fn remap(&mut self) -> Result<()> {
self.get_mapper().await?.remap().await
let mapper = self.get_mapper().await?;
mapper.remap().await?;
mapper.map_bins(self.get_binaries_with_path().await?).await
}
/// 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());
@ -221,4 +258,19 @@ impl Nenv {
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path))
}
async fn get_binaries_with_path(&mut self) -> Result<Vec<(String, NodePath)>> {
let mut binaries_with_path = Vec::new();
for (bin, cfg) in &self.config.get().await.bins {
let path = self
.repo
.get_version_path(&cfg.node_version)
.await?
.ok_or_else(|| VersionError::not_installed(&cfg.node_version))?;
binaries_with_path.push((bin.to_owned(), path));
}
Ok(binaries_with_path)
}
}

@ -16,7 +16,7 @@ use super::VersionInfo;
#[derive(Clone, Serialize, Deserialize)]
pub struct Versions {
lts_versions: HashMap<String, u16>,
lts_versions: HashMap<String, u8>,
versions: HashMap<SimpleVersion, VersionMetadata>,
// 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<VersionInfo>) -> 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::<HashMap<_, _>>();
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()

@ -0,0 +1,96 @@
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<Self> {
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 lts<S: AsRef<str>>(&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<Vec<VersionMetadata>> for InstalledVersions {
fn from(versions: Vec<VersionMetadata>) -> Self {
let versions = versions
.into_iter()
.map(|v| (v.version.to_owned(), v))
.collect::<Vec<_>>();
Self::new(versions)
}
}

@ -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> {
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")]
@ -112,20 +129,18 @@ impl Repository {
&*BIN_DIR,
&*NODE_VERSIONS_DIR,
];
for result in future::join_all(dirs.into_iter().map(|dir| async move {
future::join_all(dirs.into_iter().map(|dir| async move {
if !dir.exists() {
fs::create_dir_all(dir).await.into_diagnostic()?;
fs::create_dir_all(dir).await?;
}
Ok(())
Result::<(), std::io::Error>::Ok(())
}))
.await
{
#[allow(clippy::question_mark)]
if let Err(e) = result {
return Err(e);
}
}
.into_iter()
.fold(Result::Ok(()), |acc, res| acc.and(res))
.into_diagnostic()
.wrap_err("Failed to create application directory")?;
Ok(())
}
@ -133,7 +148,11 @@ 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<Option<NodePath>> {
let info = self.lookup_version(version).await?;
let info = if let Ok(i) = self.lookup_local_version(version) {
i
} else {
self.lookup_remote_version(version).await?
};
let path = build_version_path(&info.version);
Ok(if path.exists() {
@ -144,24 +163,22 @@ impl Repository {
}
/// Returns a list of installed versions
#[tracing::instrument(level = "debug", skip(self))]
pub async fn installed_versions(&self) -> Result<Vec<Version>> {
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<Version> {
self.installed_versions
.all()
.into_iter()
.map(|v| (*v).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<bool> {
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 +186,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 +236,23 @@ 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::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()))?,
_ => return Err(VersionError::unsupported(version_req.to_owned()).into()),
};
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 +265,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<Vec<VersionMetadata>> {
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)
}

@ -12,7 +12,15 @@ impl NodePath {
#[cfg(not(target_os = "windows"))]
pub fn bin(&self) -> PathBuf {
self.base.join("bin")
self.base.join("bin").canonicalize().unwrap()
}
pub fn lib(&self) -> PathBuf {
self.base.join("lib").canonicalize().unwrap()
}
pub fn node_modules(&self) -> PathBuf {
self.lib().join("node_modules")
}
#[cfg(target_os = "windows")]

@ -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<S: ToString>(default: bool, prompt: S) -> bool {
Confirm::new()
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(prompt.to_string())
.default(default)
.interact()

@ -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<semver::Version> 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,
}
}
}

Loading…
Cancel
Save