use super::Result;
use anyhow::bail;
use derive_more::Deref;
use serde::{Deserialize, Deserializer, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::Display;
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(version, about)]
pub struct CliArgs {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Clone, Debug)]
pub enum VersionSpec {
Lts,
Latest,
Exact(String),
}
impl std::str::FromStr for VersionSpec {
type Err = String;
fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"lts" => Ok(Self::Lts),
"latest" => Ok(Self::Latest),
v => Ok(Self::Exact(format!("v{}", v))),
}
}
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(visible_alias = "i")]
Install {
version: VersionSpec,
arch: Option<ArchSpec>,
#[arg(short, long, default_value_t = false)]
insecure: bool,
},
#[command(visible_alias = "un")]
Uninstall {
version: VersionSpec,
},
#[command(visible_alias = "ls")]
List {
#[arg(short, long, default_value_t = false)]
available: bool,
},
On,
Off,
Root {
path: Option<String>,
},
Arch,
Proxy {
url: Option<String>,
},
Current,
NodeMirror {
url: Option<String>,
},
NpmMirror {
url: Option<String>,
},
Use {
version: VersionSpec,
arch: Option<ArchSpec>,
},
}
#[derive(
Clone, Debug, ValueEnum, Display, Deserialize_repr, Serialize_repr, PartialEq,
)]
#[repr(u8)]
pub enum ArchSpec {
#[value(name = "32")]
#[strum(to_string = "32")]
X86 = 32,
#[value(name = "64")]
#[strum(to_string = "64")]
X64 = 64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[allow(dead_code)]
pub enum LtsSpec {
Codename(String),
NotLts(bool), }
#[derive(Debug, Clone, Deserialize)]
pub struct NodeReleaseInfo {
pub version: String,
#[allow(dead_code)]
pub date: String,
pub lts: LtsSpec,
}
#[derive(Debug, Clone, Deserialize, Deref)]
#[serde(transparent)]
pub struct ReleaseDatabase {
#[deref]
inner: Vec<NodeReleaseInfo>,
}
impl ReleaseDatabase {
pub fn latest(&self) -> Option<String> {
self.inner.first().map(|f| f.version.clone())
}
pub fn latest_lts(&self) -> Option<String> {
self
.inner
.iter()
.find(|r| matches!(r.lts, LtsSpec::Codename(_)))
.map(|r| r.version.clone())
}
pub fn version_exists(&self, version: &str) -> bool {
log::debug!("version_exists: {:?}", version);
let version = if version.starts_with('v') {
version.to_string()
} else {
format!("v{}", version)
};
self.inner.iter().any(|f| f.version == version)
}
pub fn by_major(&self, major: u64, len: usize) -> Vec<String> {
let major = format!("v{}", major);
let list: Vec<_> = self
.inner
.iter()
.filter(|f| f.version.starts_with(&major))
.map(|f| f.version.clone())
.collect();
fill_len(list, len)
}
pub fn latest_list(&self, count: usize) -> Vec<String> {
let list: Vec<_> = self
.inner
.iter()
.filter(|r| matches!(r.lts, LtsSpec::NotLts(false)))
.take(count)
.map(|r| r.version.clone())
.collect();
fill_len(list, count)
}
pub fn lts_list(&self, count: usize) -> Vec<String> {
let list: Vec<_> = self
.inner
.iter()
.filter(|r| matches!(r.lts, LtsSpec::Codename(_)))
.take(count)
.map(|r| r.version.clone())
.collect();
fill_len(list, count)
}
}
fn fill_len<T>(mut list: Vec<T>, len: usize) -> Vec<T>
where
T: Default,
{
if list.len() < len {
for _ in 0..(len - list.len()) {
list.push(T::default());
}
}
list
}
#[cfg(any(feature = "toml", feature = "yaml"))]
const CONFIG_FILE_NAME: &str = "settings";
#[serde_with::skip_serializing_none]
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Config {
pub root: Option<PathBuf>,
#[serde(deserialize_with = "deserialize_proxy")]
pub proxy: Option<String>,
pub node_mirror: Option<String>,
pub npm_mirror: Option<String>,
pub arch: Option<ArchSpec>,
pub originalpath: Option<PathBuf>,
pub originalversion: Option<String>,
#[serde(skip)]
inner: PathBuf,
}
fn deserialize_proxy<'de, D>(
deserializer: D,
) -> core::result::Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.is_empty()
|| s.eq_ignore_ascii_case("null")
|| s.eq_ignore_ascii_case("none")
{
return Ok(None);
}
Ok(Some(s))
}
impl Config {
pub fn load() -> Config {
log::debug!("load config");
#[cfg(feature = "toml")]
{
if let Some(config) = read_from_toml() {
return config;
}
}
#[cfg(feature = "yaml")]
{
if let Some(config) = read_from_txt() {
return config;
}
}
Config::default()
}
pub fn save(&self) -> Result {
log::debug!("save config");
#[cfg(feature = "toml")]
let result = write_to_toml(self);
#[cfg(all(not(feature = "toml"), feature = "yaml"))]
let result = write_to_txt(self);
#[cfg(all(not(feature = "toml"), not(feature = "yaml")))]
let result: Result = Ok(());
result
}
pub fn is_valid(&self) -> Result {
if let Some(root) = &self.root
&& !root.is_dir()
{
bail!("root is not a directory");
}
if let Some(originalpath) = &self.originalpath
&& !originalpath.is_dir()
{
bail!("originalpath is not a directory");
}
Ok(())
}
}
#[cfg(feature = "toml")]
fn read_from_toml() -> Option<Config> {
use std::fs;
let Ok(current_dir) = get_exec_path() else {
log::warn!("failed to get current exe path");
return None;
};
let path = current_dir.join(CONFIG_FILE_NAME);
let Ok(data) = fs::read_to_string(&path) else {
log::warn!("failed to read {}", path.display());
return None;
};
let Ok(mut config) = toml::from_str::<Config>(&data) else {
log::warn!("failed to parse {}", path.display());
return None;
};
config.inner = current_dir.to_path_buf();
Some(config)
}
#[cfg(feature = "yaml")]
fn read_from_txt() -> Option<Config> {
use std::fs;
let Ok(current_dir) = get_exec_path() else {
log::warn!("failed to get current exe path");
return None;
};
let path = current_dir.join(CONFIG_FILE_NAME).with_extension("txt");
let Ok(data) = fs::read_to_string(&path) else {
log::warn!("failed to read {}", path.display());
return None;
};
let Ok(mut config) = noyalib::from_str::<Config>(&data) else {
log::warn!("failed to parse {}:", path.display());
return None;
};
config.inner = current_dir.to_path_buf();
Some(config)
}
#[cfg(feature = "toml")]
fn write_to_toml(config: &Config) -> Result {
use std::fs;
let config_str = toml::to_string(config)?;
let path_str = if cfg!(debug_assertions) {
format!("{}.toml.toml", CONFIG_FILE_NAME)
} else {
format!("{}.toml", CONFIG_FILE_NAME)
};
fs::write(&path_str, config_str)?;
Ok(())
}
#[cfg(feature = "yaml")]
fn write_to_txt(config: &Config) -> Result {
use std::fs;
let config_str = noyalib::to_string(config)?;
let path_str = if cfg!(debug_assertions) {
format!("{}.txt.txt", CONFIG_FILE_NAME)
} else {
format!("{}.txt", CONFIG_FILE_NAME)
};
fs::write(&path_str, config_str)?;
Ok(())
}
fn get_exec_path() -> Result<PathBuf> {
use std::env;
let Ok(exe_path) = env::current_exe() else {
bail!("failed to get current exe path");
};
let Some(current_dir) = exe_path.parent() else {
bail!("failed to get current exe path");
};
Ok(current_dir.to_path_buf())
}