mirror of https://github.com/Trivernis/nenv
Move commands to nenv instance
All commands are moved to the nenv instance. This also changes responsibilities slighly and introduces a shared config objectfeature/lookup-installed
parent
9c7b588865
commit
ad1afd3173
@ -0,0 +1,111 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use miette::Context;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
|
||||
use tokio::fs;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use crate::error::SerializeTomlError;
|
||||
use crate::{
|
||||
consts::{CFG_DIR, CFG_FILE_PATH},
|
||||
error::ParseConfigError,
|
||||
};
|
||||
|
||||
use self::value::Config;
|
||||
|
||||
mod value;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigAccess {
|
||||
dirty: Arc<AtomicBool>,
|
||||
config: Arc<RwLock<Config>>,
|
||||
}
|
||||
|
||||
pub struct ModifyGuard<'a, T>(ConfigAccess, RwLockWriteGuard<'a, T>);
|
||||
|
||||
impl<'a, T> Deref for ModifyGuard<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DerefMut for ModifyGuard<'a, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for ModifyGuard<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
self.0
|
||||
.dirty
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigAccess {
|
||||
/// Loads the config file from the default config path
|
||||
pub async fn load() -> Result<Self> {
|
||||
if !CFG_FILE_PATH.exists() {
|
||||
if !CFG_DIR.exists() {
|
||||
fs::create_dir_all(&*CFG_DIR)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("creating config dir")?;
|
||||
}
|
||||
let cfg = Config::default();
|
||||
let access = Self::new(cfg);
|
||||
access.save().await?;
|
||||
|
||||
Ok(access)
|
||||
} else {
|
||||
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("reading config file")?;
|
||||
|
||||
let cfg = toml::from_str(&cfg_string)
|
||||
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
|
||||
|
||||
Ok(Self::new(cfg))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> RwLockReadGuard<Config> {
|
||||
if self.dirty.swap(false, std::sync::atomic::Ordering::Relaxed) {
|
||||
self.save().await.expect("Failed so save config");
|
||||
}
|
||||
self.config.read().await
|
||||
}
|
||||
|
||||
pub async fn get_mut(&self) -> ModifyGuard<Config> {
|
||||
if self.dirty.swap(false, std::sync::atomic::Ordering::Relaxed) {
|
||||
self.save().await.expect("Failed so save config");
|
||||
}
|
||||
ModifyGuard(self.clone(), self.config.write().await)
|
||||
}
|
||||
|
||||
fn new(config: Config) -> Self {
|
||||
Self {
|
||||
dirty: Arc::new(AtomicBool::new(false)),
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&*CFG_FILE_PATH,
|
||||
toml::to_string_pretty(&*self.config.read().await).map_err(SerializeTomlError::from)?,
|
||||
)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("writing config file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{consts::NODE_DIST_URL, repository::NodeVersion};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// Node execution related config
|
||||
pub node: NodeConfig,
|
||||
|
||||
/// Configuration for how to download node versions
|
||||
pub download: DownloadConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct NodeConfig {
|
||||
/// The default version if no version is specified
|
||||
/// in the `package.json` file or `NODE_VERSION` environment variable
|
||||
#[serde(with = "NodeVersion")]
|
||||
pub default_version: NodeVersion,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DownloadConfig {
|
||||
pub dist_base_url: String,
|
||||
}
|
||||
|
||||
impl Default for NodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_version: NodeVersion::LatestLts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DownloadConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dist_base_url: String::from(NODE_DIST_URL),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
use std::ffi::OsString;
|
||||
|
||||
use crate::{
|
||||
config::ConfigAccess,
|
||||
consts::VERSION_FILE_PATH,
|
||||
error::VersionError,
|
||||
mapper::Mapper,
|
||||
repository::{NodeVersion, Repository},
|
||||
utils::prompt,
|
||||
version_detection::{self, VersionDetector},
|
||||
};
|
||||
use crossterm::style::Stylize;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tokio::fs;
|
||||
|
||||
pub struct Nenv {
|
||||
config: ConfigAccess,
|
||||
repo: Repository,
|
||||
active_version: NodeVersion,
|
||||
}
|
||||
|
||||
impl Nenv {
|
||||
pub async fn init() -> 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);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
repo,
|
||||
active_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Installs the given node version.
|
||||
/// Prompts if that version already exists
|
||||
pub async fn install(&mut self, version: NodeVersion) -> Result<()> {
|
||||
Self::clear_version_cache().await?;
|
||||
|
||||
if self.repo.is_installed(&version)?
|
||||
&& !prompt(
|
||||
false,
|
||||
format!(
|
||||
"The version {} is already installed. Reinstall?",
|
||||
version.to_string().bold()
|
||||
),
|
||||
)
|
||||
{
|
||||
println!("Nothing changed.");
|
||||
Ok(())
|
||||
} else {
|
||||
self.repo.install_version(&version).await?;
|
||||
self.active_version = version.to_owned();
|
||||
self.get_mapper()?.remap_additive().await?;
|
||||
|
||||
println!("Installed {}", version.to_string().bold());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the system-wide default version
|
||||
pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> {
|
||||
self.active_version = version.to_owned();
|
||||
|
||||
if !self.repo.is_installed(&version)? {
|
||||
if prompt(
|
||||
false,
|
||||
format!("The version {version} is not installed. Do you want to install it?"),
|
||||
) {
|
||||
self.repo.install_version(&version).await?;
|
||||
self.config.get_mut().await.node.default_version = version.to_owned();
|
||||
self.get_mapper()?.remap_additive().await?;
|
||||
println!("Now using {}", version.to_string().bold());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
self.get_mapper()?.remap_additive().await?;
|
||||
self.config.get_mut().await.node.default_version = version.to_owned();
|
||||
println!("Now using {}", version.to_string().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a given node executable for the currently active version
|
||||
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<i32> {
|
||||
if !self.repo.is_installed(&self.active_version)? {
|
||||
self.repo.install_version(&self.active_version).await?;
|
||||
}
|
||||
let exit_status = self.get_mapper()?.exec(command, args).await?;
|
||||
|
||||
Ok(exit_status.code().unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Persits all changes made that aren't written to the disk yet
|
||||
pub async fn persist(&self) -> Result<()> {
|
||||
self.config.save().await
|
||||
}
|
||||
|
||||
/// Clears the version cache and remaps all executables
|
||||
pub async fn refresh(&self) -> Result<()> {
|
||||
Self::clear_version_cache().await?;
|
||||
self.get_mapper()?.remap().await
|
||||
}
|
||||
|
||||
/// Lists the currently installed versions
|
||||
pub async fn list_versions(&self) -> Result<()> {
|
||||
let versions = self.repo.installed_versions().await?;
|
||||
let active_version = self.repo.lookup_version(&self.active_version)?;
|
||||
|
||||
println!("{}", "Installed versions:".bold());
|
||||
|
||||
for version in versions {
|
||||
let info = self
|
||||
.repo
|
||||
.all_versions()
|
||||
.get(&version)
|
||||
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
|
||||
let lts = info
|
||||
.lts
|
||||
.as_ref()
|
||||
.map(|l| format!(" ({})", l.to_owned().green()))
|
||||
.unwrap_or_default();
|
||||
|
||||
if version == active_version.version {
|
||||
println!(" {}{} [current]", version.to_string().blue().bold(), lts)
|
||||
} else {
|
||||
println!(" {}{}", version.to_string().blue(), lts)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_active_version() -> Option<NodeVersion> {
|
||||
version_detection::ParallelDetector::detect_version()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|v| v)
|
||||
}
|
||||
|
||||
async fn clear_version_cache() -> Result<()> {
|
||||
if VERSION_FILE_PATH.exists() {
|
||||
fs::remove_file(&*VERSION_FILE_PATH)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mapper(&self) -> Result<Mapper> {
|
||||
let node_path = self
|
||||
.repo
|
||||
.get_version_path(&self.active_version)?
|
||||
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
|
||||
Ok(Mapper::new(node_path))
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
use miette::Context;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::error::SerializeTomlError;
|
||||
use crate::{
|
||||
consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL},
|
||||
error::ParseConfigError,
|
||||
};
|
||||
|
||||
use super::NodeVersion;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// Node execution related config
|
||||
pub node: NodeConfig,
|
||||
|
||||
/// Configuration for how to download node versions
|
||||
pub download: DownloadConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct NodeConfig {
|
||||
/// The default version if no version is specified
|
||||
/// in the `package.json` file or `NODE_VERSION` environment variable
|
||||
#[serde(with = "NodeVersion")]
|
||||
pub default_version: NodeVersion,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DownloadConfig {
|
||||
pub dist_base_url: String,
|
||||
}
|
||||
|
||||
impl Default for NodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_version: NodeVersion::LatestLts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DownloadConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dist_base_url: String::from(NODE_DIST_URL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Loads the config file from the default config path
|
||||
pub async fn load() -> Result<Self> {
|
||||
if !CFG_FILE_PATH.exists() {
|
||||
if !CFG_DIR.exists() {
|
||||
fs::create_dir_all(&*CFG_DIR)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("creating config dir")?;
|
||||
}
|
||||
let cfg = Config::default();
|
||||
cfg.save().await?;
|
||||
|
||||
Ok(cfg)
|
||||
} else {
|
||||
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("reading config file")?;
|
||||
|
||||
let cfg = toml::from_str(&cfg_string)
|
||||
.map_err(|e| ParseConfigError::new("config.toml", cfg_string, e))?;
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&*CFG_FILE_PATH,
|
||||
toml::to_string_pretty(&self).map_err(SerializeTomlError::from)?,
|
||||
)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.context("writing config file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_default_version(&mut self, default_version: NodeVersion) -> Result<()> {
|
||||
self.node.default_version = default_version;
|
||||
self.save().await
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue