Move commands to nenv instance

All commands are moved to the nenv instance. This
also changes responsibilities slighly and introduces
a shared config object
feature/lookup-installed
trivernis 1 year ago
parent 9c7b588865
commit ad1afd3173
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -33,5 +33,6 @@ tokio = { version = "1.24.2", features = ["rt", "macros", "tracing", "net", "fs"
toml = "0.5.11"
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
xkcd_unreachable = "0.1.1"
zip = "0.6.3"

@ -7,7 +7,7 @@ use clap::{Parser, Subcommand};
#[clap(infer_subcommands = true)]
pub struct Args {
#[command(subcommand)]
pub commmand: Command,
pub command: Command,
}
#[derive(Clone, Debug, Subcommand)]
@ -24,7 +24,8 @@ pub enum Command {
#[command()]
Default(DefaultArgs),
/// Refreshes the node environment mappings and cache
/// Refreshes the node environment mappings and cache.
/// This will erase all binary mappings not relevant to the current node version.
#[command()]
Refresh,

@ -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),
}
}
}

@ -2,12 +2,8 @@ use std::process;
use args::Args;
use clap::Parser;
use std::ffi::OsString;
use consts::VERSION_FILE_PATH;
use crossterm::style::Stylize;
use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository};
use nenv::Nenv;
mod consts;
pub mod error;
@ -15,35 +11,41 @@ pub mod mapper;
pub mod repository;
mod utils;
mod web_api;
use dialoguer::Confirm;
use miette::{IntoDiagnostic, Result};
use tokio::fs;
use crate::error::VersionError;
use miette::Result;
use xkcd_unreachable::xkcd_unreachable;
mod args;
mod config;
mod nenv;
mod version_detection;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
miette::set_panic_hook();
let args: Args = Args::parse();
match args.commmand {
args::Command::Version => {
print_version();
Ok(())
}
args::Command::Install(v) => install_version(v.version).await,
args::Command::Default(v) => set_default_version(v.version).await,
if let args::Command::Version = &args.command {
print_version();
return Ok(());
}
let mut nenv = get_nenv().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::Exec(args) => {
let exit_code = exec(args.command, args.args).await?;
let exit_code = nenv.exec(args.command, args.args).await?;
process::exit(exit_code);
}
args::Command::Refresh => refresh().await,
args::Command::ListVersions => list_versions().await,
args::Command::Refresh => nenv.refresh().await,
args::Command::ListVersions => nenv.list_versions().await,
_ => xkcd_unreachable!(),
}?;
nenv.persist().await?;
Ok(())
}
@ -51,116 +53,6 @@ fn print_version() {
println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
/// Installs a given node version
async fn install_version(version: NodeVersion) -> Result<()> {
if VERSION_FILE_PATH.exists() {
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
}
let repo = get_repository().await?;
if repo.is_installed(&version)?
&& !Confirm::new()
.with_prompt(format!(
"The version {} is already installed. Reinstall?",
version.to_string().bold()
))
.default(false)
.interact()
.unwrap()
{
return Ok(());
}
repo.install_version(&version).await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
/// Sets a default system wide node version
async fn set_default_version(version: NodeVersion) -> Result<()> {
let mut mapper = get_mapper().await?;
if !mapper.repository().is_installed(&version)?
&& 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.set_default_version(&version).await?;
println!("Now using {}", version.to_string().bold());
Ok(())
}
/// Exectues a given command
#[inline]
async fn exec(command: String, args: Vec<OsString>) -> Result<i32> {
let mapper = get_mapper().await?;
let active_version = mapper.active_version();
if !mapper.repository().is_installed(active_version)? {
mapper.repository().install_version(active_version).await?;
}
let exit_status = mapper.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
/// Refreshes the version cache and mapped binaries
async fn refresh() -> Result<()> {
get_mapper().await?.remap().await?;
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
println!("Remapped binaries and cleared version cache");
Ok(())
}
/// Lists all available node versions
async fn list_versions() -> Result<()> {
let mapper = get_mapper().await?;
let versions = mapper.repository().installed_versions().await?;
let active_version = mapper
.repository()
.lookup_version(mapper.active_version())?;
println!("{}", "Installed versions:".bold());
for version in versions {
let info = mapper
.repository()
.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_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await
}
async fn get_mapper() -> Result<Mapper> {
Ok(Mapper::load(get_repository().await?).await)
async fn get_nenv() -> Result<Nenv> {
Nenv::init().await
}

@ -55,7 +55,7 @@ impl NodeApp {
}
}
pub async fn map_node_bin(node_path: NodePath) -> Result<()> {
pub async fn map_node_bin(node_path: &NodePath) -> Result<()> {
let mapped_app_names = get_applications(&BIN_DIR)
.await?
.iter()

@ -2,70 +2,29 @@ use std::{ffi::OsString, process::ExitStatus};
use tokio::fs;
use crate::{
consts::BIN_DIR,
error::VersionError,
repository::{NodeVersion, Repository},
};
use crate::{consts::BIN_DIR, repository::node_path::NodePath};
use self::{
mapped_command::MappedCommand,
mapped_dir::map_node_bin,
version_detection::{ParallelDetector, VersionDetector},
};
use self::{mapped_command::MappedCommand, mapped_dir::map_node_bin};
use miette::{IntoDiagnostic, Result};
mod mapped_command;
mod mapped_dir;
mod version_detection;
/// Responsible for mapping to node executables
/// and managing node versions
pub struct Mapper {
repo: Repository,
active_version: NodeVersion,
node_path: NodePath,
}
impl Mapper {
pub async fn load(repository: Repository) -> Self {
let version = Self::get_version()
.await
.unwrap_or_else(|| repository.config.node.default_version.to_owned());
Self {
repo: repository,
active_version: version,
}
pub fn new(node_path: NodePath) -> Self {
Self { node_path }
}
pub fn repository(&self) -> &Repository {
&self.repo
}
/// Sets the given version as the default one
pub async fn set_default_version(&mut self, version: &NodeVersion) -> Result<()> {
self.repo
.config
.set_default_version(version.clone())
.await?;
self.active_version = version.clone();
self.map_active_version().await?;
Ok(())
}
pub fn active_version(&self) -> &NodeVersion {
&self.active_version
}
/// Executes a mapped command with the given node environment
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<ExitStatus> {
let node_path = self
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(&self.active_version))?;
let executable = node_path.bin().join(&command);
let executable = self.node_path.bin().join(&command);
let exit_status = MappedCommand::new(command, executable, args).run().await?;
self.map_active_version().await?;
self.remap_additive().await?;
Ok(exit_status)
}
@ -74,25 +33,13 @@ impl Mapper {
pub async fn remap(&self) -> Result<()> {
fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?;
fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?;
self.map_active_version().await?;
self.remap_additive().await?;
Ok(())
}
async fn get_version() -> Option<NodeVersion> {
ParallelDetector::detect_version()
.await
.ok()
.and_then(|v| v)
}
/// creates wrapper scripts for the current version
async fn map_active_version(&self) -> Result<()> {
let dir = self
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::not_installed(self.active_version.to_string()))?;
map_node_bin(dir).await?;
pub async fn remap_additive(&self) -> Result<()> {
map_node_bin(&self.node_path).await?;
Ok(())
}

@ -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
}
}

@ -12,6 +12,7 @@ use tokio::{
};
use crate::{
config::ConfigAccess,
consts::{
ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS,
},
@ -21,9 +22,8 @@ use crate::{
use miette::{IntoDiagnostic, Result};
use self::{config::Config, node_path::NodePath, versions::Versions};
use self::{node_path::NodePath, versions::Versions};
pub mod config;
pub(crate) mod extract;
pub(crate) mod node_path;
pub mod versions;
@ -86,21 +86,16 @@ impl fmt::Display for NodeVersion {
pub struct Repository {
versions: Versions,
web_api: WebApi,
pub config: Config,
}
impl Repository {
/// Initializes a new repository with the given confi
pub async fn init(config: Config) -> Result<Self> {
pub async fn init(config: ConfigAccess) -> Result<Self> {
Self::create_folders().await?;
let web_api = WebApi::new(&config.download.dist_base_url);
let web_api = WebApi::new(&config.get().await.download.dist_base_url);
let versions = load_versions(&web_api).await?;
Ok(Self {
config,
web_api,
versions,
})
Ok(Self { web_api, versions })
}
async fn create_folders() -> Result<()> {

@ -1,5 +1,6 @@
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct NodePath {
base: PathBuf,
}

@ -3,6 +3,7 @@ use std::{
time::Duration,
};
use dialoguer::Confirm;
use indicatif::{ProgressBar, ProgressStyle};
pub fn progress_bar(total: u64) -> ProgressBar {
@ -30,6 +31,14 @@ pub fn progress_spinner() -> ProgressBar {
pb
}
pub fn prompt<S: ToString>(default: bool, prompt: S) -> bool {
Confirm::new()
.with_prompt(prompt.to_string())
.default(default)
.interact()
.unwrap()
}
pub fn find_in_parents<P: AsRef<Path>>(origin: PathBuf, name: P) -> Option<PathBuf> {
for part in dir_parts(origin) {
let file = part.join(&name);

Loading…
Cancel
Save