1use super::Result;
2use anyhow::bail;
3use derive_more::Deref;
4use serde::{Deserialize, Deserializer, Serialize};
5use serde_repr::{Deserialize_repr, Serialize_repr};
6use strum::Display;
7
8use clap::{Parser, Subcommand, ValueEnum};
9use std::path::PathBuf;
10
11#[derive(Parser, Debug)]
12#[command(version, about)]
13pub struct CliArgs {
14 #[command(subcommand)]
15 pub command: Commands,
16}
17
18#[derive(Clone, Debug)]
19pub enum VersionSpec {
20 Lts,
22 Latest,
24 Exact(String),
26}
27
28impl std::str::FromStr for VersionSpec {
30 type Err = String;
31
32 fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
33 match s.to_lowercase().as_str() {
34 "lts" => Ok(Self::Lts),
35 "latest" => Ok(Self::Latest),
36 v => Ok(Self::Exact(format!("v{}", v))),
37 }
38 }
39}
40
41#[derive(Subcommand, Debug)]
42pub enum Commands {
43 #[command(visible_alias = "i")]
48 Install {
49 version: VersionSpec,
52 arch: Option<ArchSpec>,
54 #[arg(short, long, default_value_t = false)]
56 insecure: bool,
57 },
58 #[command(visible_alias = "un")]
60 Uninstall {
61 version: VersionSpec,
63 },
64 #[command(visible_alias = "ls")]
66 List {
67 #[arg(short, long, default_value_t = false)]
69 available: bool,
70 },
71 On,
73 Off,
75 Root {
78 path: Option<String>,
80 },
81 Arch,
83 Proxy {
85 url: Option<String>,
88 },
89 Current,
91 NodeMirror {
93 url: Option<String>,
95 },
96 NpmMirror {
98 url: Option<String>,
100 },
101 Use {
105 version: VersionSpec,
107 arch: Option<ArchSpec>,
109 },
110}
111
112#[derive(
113 Clone, Debug, ValueEnum, Display, Deserialize_repr, Serialize_repr, PartialEq,
114)]
115#[repr(u8)]
116pub enum ArchSpec {
117 #[value(name = "32")]
119 #[strum(to_string = "32")]
120 X86 = 32,
121 #[value(name = "64")]
123 #[strum(to_string = "64")]
124 X64 = 64,
125}
126
127#[derive(Debug, Clone, Deserialize)]
128#[serde(untagged)]
129#[allow(dead_code)]
131pub enum LtsSpec {
132 Codename(String),
133 NotLts(bool), }
135
136#[derive(Debug, Clone, Deserialize)]
138pub struct NodeReleaseInfo {
139 pub version: String,
141
142 #[allow(dead_code)]
145 pub date: String,
147
148 pub lts: LtsSpec,
181 }
185
186#[derive(Debug, Clone, Deserialize, Deref)]
188#[serde(transparent)]
189pub struct ReleaseDatabase {
190 #[deref]
191 inner: Vec<NodeReleaseInfo>,
192}
193
194impl ReleaseDatabase {
195 pub fn latest(&self) -> Option<String> {
197 self.inner.first().map(|f| f.version.clone())
198 }
199
200 pub fn latest_lts(&self) -> Option<String> {
202 self
203 .inner
204 .iter()
205 .find(|r| matches!(r.lts, LtsSpec::Codename(_)))
206 .map(|r| r.version.clone())
207 }
208
209 pub fn version_exists(&self, version: &str) -> bool {
223 log::debug!("version_exists: {:?}", version);
224
225 let version = if version.starts_with('v') {
226 version.to_string()
227 } else {
228 format!("v{}", version)
229 };
230
231 self.inner.iter().any(|f| f.version == version)
232 }
233
234 pub fn by_major(&self, major: u64, len: usize) -> Vec<String> {
236 let major = format!("v{}", major);
237
238 let list: Vec<_> = self
239 .inner
240 .iter()
241 .filter(|f| f.version.starts_with(&major))
242 .map(|f| f.version.clone())
243 .collect();
244
245 fill_len(list, len)
246 }
247
248 pub fn latest_list(&self, count: usize) -> Vec<String> {
250 let list: Vec<_> = self
251 .inner
252 .iter()
253 .filter(|r| matches!(r.lts, LtsSpec::NotLts(false)))
254 .take(count)
255 .map(|r| r.version.clone())
256 .collect();
257
258 fill_len(list, count)
259 }
260
261 pub fn lts_list(&self, count: usize) -> Vec<String> {
263 let list: Vec<_> = self
264 .inner
265 .iter()
266 .filter(|r| matches!(r.lts, LtsSpec::Codename(_)))
267 .take(count)
268 .map(|r| r.version.clone())
269 .collect();
270
271 fill_len(list, count)
272 }
273}
274
275fn fill_len<T>(mut list: Vec<T>, len: usize) -> Vec<T>
276where
277 T: Default,
278{
279 if list.len() < len {
280 for _ in 0..(len - list.len()) {
281 list.push(T::default());
282 }
283 }
284
285 list
286}
287
288#[cfg(any(feature = "toml", feature = "yaml"))]
289const CONFIG_FILE_NAME: &str = "settings";
290
291#[serde_with::skip_serializing_none]
292#[derive(Debug, Default, Clone, Deserialize, Serialize)]
293pub struct Config {
294 pub root: Option<PathBuf>,
296 #[serde(deserialize_with = "deserialize_proxy")]
298 pub proxy: Option<String>,
299 pub node_mirror: Option<String>,
301 pub npm_mirror: Option<String>,
303 pub arch: Option<ArchSpec>,
304 pub originalpath: Option<PathBuf>,
305 pub originalversion: Option<String>,
306 #[serde(skip)]
308 inner: PathBuf,
309}
310
311fn deserialize_proxy<'de, D>(
312 deserializer: D,
313) -> core::result::Result<Option<String>, D::Error>
314where
315 D: Deserializer<'de>,
316{
317 let s = String::deserialize(deserializer)?;
318 if s.is_empty()
319 || s.eq_ignore_ascii_case("null")
320 || s.eq_ignore_ascii_case("none")
321 {
322 return Ok(None);
323 }
324
325 Ok(Some(s))
326}
327
328impl Config {
329 pub fn load() -> Config {
334 log::debug!("load config");
335
336 #[cfg(feature = "toml")]
337 {
338 if let Some(config) = read_from_toml() {
339 return config;
340 }
341 }
342
343 #[cfg(feature = "yaml")]
344 {
345 if let Some(config) = read_from_txt() {
346 return config;
347 }
348 }
349
350 Config::default()
351 }
352
353 pub fn save(&self) -> Result {
354 log::debug!("save config");
355
356 #[cfg(feature = "toml")]
358 let result = write_to_toml(self);
359
360 #[cfg(all(not(feature = "toml"), feature = "yaml"))]
361 let result = write_to_txt(self);
362
363 #[cfg(all(not(feature = "toml"), not(feature = "yaml")))]
364 let result: Result = Ok(());
365
366 result
367 }
368
369 pub fn is_valid(&self) -> Result {
370 if let Some(root) = &self.root
371 && !root.is_dir()
372 {
373 bail!("root is not a directory");
374 }
375
376 if let Some(originalpath) = &self.originalpath
377 && !originalpath.is_dir()
378 {
379 bail!("originalpath is not a directory");
380 }
381
382 Ok(())
383 }
384}
385
386#[cfg(feature = "toml")]
387fn read_from_toml() -> Option<Config> {
388 use std::fs;
389
390 let Ok(current_dir) = get_exec_path() else {
391 log::warn!("failed to get current exe path");
392 return None;
393 };
394
395 let path = current_dir.join(CONFIG_FILE_NAME);
396 let Ok(data) = fs::read_to_string(&path) else {
397 log::warn!("failed to read {}", path.display());
398 return None;
399 };
400
401 let Ok(mut config) = toml::from_str::<Config>(&data) else {
402 log::warn!("failed to parse {}", path.display());
403 return None;
404 };
405
406 config.inner = current_dir.to_path_buf();
407
408 Some(config)
409}
410
411#[cfg(feature = "yaml")]
412fn read_from_txt() -> Option<Config> {
413 use std::fs;
414
415 let Ok(current_dir) = get_exec_path() else {
416 log::warn!("failed to get current exe path");
417 return None;
418 };
419
420 let path = current_dir.join(CONFIG_FILE_NAME).with_extension("txt");
421 let Ok(data) = fs::read_to_string(&path) else {
422 log::warn!("failed to read {}", path.display());
423 return None;
424 };
425 let Ok(mut config) = noyalib::from_str::<Config>(&data) else {
426 log::warn!("failed to parse {}:", path.display());
427 return None;
428 };
429
430 config.inner = current_dir.to_path_buf();
431
432 Some(config)
433}
434
435#[cfg(feature = "toml")]
436fn write_to_toml(config: &Config) -> Result {
437 use std::fs;
438
439 let config_str = toml::to_string(config)?;
440 let path_str = if cfg!(debug_assertions) {
441 format!("{}.toml.toml", CONFIG_FILE_NAME)
442 } else {
443 format!("{}.toml", CONFIG_FILE_NAME)
444 };
445 fs::write(&path_str, config_str)?;
446 Ok(())
447}
448
449#[cfg(feature = "yaml")]
450fn write_to_txt(config: &Config) -> Result {
451 use std::fs;
452
453 let config_str = noyalib::to_string(config)?;
454 let path_str = if cfg!(debug_assertions) {
455 format!("{}.txt.txt", CONFIG_FILE_NAME)
456 } else {
457 format!("{}.txt", CONFIG_FILE_NAME)
458 };
459 fs::write(&path_str, config_str)?;
460 Ok(())
461}
462
463fn get_exec_path() -> Result<PathBuf> {
464 use std::env;
465
466 let Ok(exe_path) = env::current_exe() else {
467 bail!("failed to get current exe path");
468 };
469
470 let Some(current_dir) = exe_path.parent() else {
471 bail!("failed to get current exe path");
472 };
473
474 Ok(current_dir.to_path_buf())
475}