Skip to main content

nvm_windows/
model.rs

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  /// Latest LTS version
21  Lts,
22  /// Latest version
23  Latest,
24  /// Exact semver version number.(eg: 1.1.0)
25  Exact(String),
26}
27
28// Clap 通过 FromStr 自动将其作为 value_parser
29impl 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  /// The version can be a specific version, "latest" for the latest current version, or "lts" for the
44  /// most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults
45  /// to system arch). Set [arch] to "all" to install 32 AND 64 bit versions.
46  /// Add --insecure to the end of this command to bypass SSL validation of the remote download server.
47  #[command(visible_alias = "i")]
48  Install {
49    /// The version can be a specific version, "latest" for the latest current version, or "lts" for the
50    /// most recent LTS version. [possible values: <semver>(eg: 1.1.0), lts, latest]
51    version: VersionSpec,
52    /// Specify whether to install the 32 or 64 bit version (defaults to system arch).
53    arch: Option<ArchSpec>,
54    /// Pass SSL validation of the remote download server.
55    #[arg(short, long, default_value_t = false)]
56    insecure: bool,
57  },
58  /// The version must be a specific version.
59  #[command(visible_alias = "un")]
60  Uninstall {
61    /// The version to uninstall.
62    version: VersionSpec,
63  },
64  /// List the node.js installations.
65  #[command(visible_alias = "ls")]
66  List {
67    /// Show online available versions.
68    #[arg(short, long, default_value_t = false)]
69    available: bool,
70  },
71  /// Enable node.js version management.
72  On,
73  /// Disable node.js version management.
74  Off,
75  /// Set the directory where nvm should store different versions of node.js.
76  /// If <path> is not set, the current root will be displayed.
77  Root {
78    /// The path to set as the root directory.
79    path: Option<String>,
80  },
81  /// Show if node is running in 32 or 64 bit mode.
82  Arch,
83  /// Set a proxy to use for downloads.
84  Proxy {
85    /// Leave [url] blank to see the current proxy.
86    /// Set [url] to "none" to remove the proxy.
87    url: Option<String>,
88  },
89  /// Display active version.
90  Current,
91  /// Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.
92  NodeMirror {
93    /// The node mirror to use. Leave [url] blank to use default url.
94    url: Option<String>,
95  },
96  /// Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to use default url.
97  NpmMirror {
98    /// The npm mirror to use. Leave [url] blank to use default url.
99    url: Option<String>,
100  },
101  /// Switch to use the specified version. Optionally use "latest", "lts", or "newest".
102  /// "newest" is the latest installed version. Optionally specify 32/64bit architecture.
103  /// nvm use <arch> will continue using the selected version, but switch to 32/64 bit mode.
104  Use {
105    /// The version to use.
106    version: VersionSpec,
107    /// The architecture to use.
108    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  /// 32 bit
118  #[value(name = "32")]
119  #[strum(to_string = "32")]
120  X86 = 32,
121  /// 64 bit
122  #[value(name = "64")]
123  #[strum(to_string = "64")]
124  X64 = 64,
125}
126
127#[derive(Debug, Clone, Deserialize)]
128#[serde(untagged)]
129// 字段负载仅用于反序列化时区分变体,业务上只判断变体形状
130#[allow(dead_code)]
131pub enum LtsSpec {
132  Codename(String),
133  NotLts(bool), // 只接受 false;如果 JSON 里出现 true 也会匹配进来
134}
135
136/// Node.js 版本发布信息
137#[derive(Debug, Clone, Deserialize)]
138pub struct NodeReleaseInfo {
139  /// 版本号,如 "v26.8.1"
140  pub version: String,
141
142  /// 发布日期,格式 YYYY-MM-DD
143  // #[serde(deserialize_with = "deserialize_jiff_date")]
144  #[allow(dead_code)]
145  // #[tabled(skip)]
146  pub date: String,
147
148  // /// 可用的构建产物/平台列表
149  // #[tabled(skip)]
150  // pub files: Vec<String>,
151
152  // /// 捆绑的 npm 版本
153  // #[tabled(skip)]
154  // pub npm: Option<String>,
155
156  // /// V8 引擎版本
157  // #[tabled(skip)]
158  // pub v8: String,
159
160  // /// libuv 版本
161  // #[tabled(skip)]
162  // pub uv: Option<String>,
163
164  // /// zlib 版本
165  // #[tabled(skip)]
166  // pub zlib: Option<String>,
167
168  // /// OpenSSL 版本
169  // #[tabled(skip)]
170  // pub openssl: Option<String>,
171
172  // /// Node-API (ABI) 模块版本号
173  // #[tabled(skip)]
174  // #[serde(rename = "modules")]
175  // pub abi_version: Option<String>,
176
177  // /// 是否为 LTS(长期支持)版本
178  // #[tabled(skip)]
179  // #[serde(deserialize_with = "deserialize_lts")]
180  pub lts: LtsSpec,
181  // /// 是否为安全修复版本
182  // #[tabled(skip)]
183  // pub security: bool,
184}
185
186/// 核心查询缓存(启动时构建一次)
187#[derive(Debug, Clone, Deserialize, Deref)]
188#[serde(transparent)]
189pub struct ReleaseDatabase {
190  #[deref]
191  inner: Vec<NodeReleaseInfo>,
192}
193
194impl ReleaseDatabase {
195  /// ✅ 获取最新版本
196  pub fn latest(&self) -> Option<String> {
197    self.inner.first().map(|f| f.version.clone())
198  }
199
200  /// ✅ 获取最新 LTS 版本
201  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  // /// ✅ 按条件组合查询(示例:最新 LTS + 指定平台)
210  // pub fn latest_lts_with_platform(
211  //   &self,
212  //   platform: &str,
213  // ) -> Option<&NodeReleaseInfo> {
214  //   self
215  //     .lts_indices
216  //     .iter()
217  //     .map(|&i| &self.sorted_releases[i])
218  //     .find(|r| r.files.iter().any(|f| f == platform))
219  // }
220
221  /// 版本查询
222  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  /// 获取某主版本下所有发布
235  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  /// 最新的 N 个版本
249  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  /// 最新 n 个 LTS 版本
262  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  /// Node.js storage root
295  pub root: Option<PathBuf>,
296  /// Node.js proxy
297  #[serde(deserialize_with = "deserialize_proxy")]
298  pub proxy: Option<String>,
299  /// Node.js mirror
300  pub node_mirror: Option<String>,
301  /// npm mirror
302  pub npm_mirror: Option<String>,
303  pub arch: Option<ArchSpec>,
304  pub originalpath: Option<PathBuf>,
305  pub originalversion: Option<String>,
306  // pub symlink: Option<String>,
307  #[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  /// Load config from file. Returns None if no config file exists.<br>
330  /// If both toml and yaml features are enabled, it will try to load from toml first.
331  /// If toml is not enabled, it will try to load from yaml.
332  /// If yaml is not enabled, it will return None.
333  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    // toml 优先;仅在未启用 toml 时才使用 txt,与 load() 的优先级保持一致
357    #[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}