nvm-windows 0.1.1

A tool to manage Node.js versions.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
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 {
  /// Latest LTS version
  Lts,
  /// Latest version
  Latest,
  /// Exact semver version number.(eg: 1.1.0)
  Exact(String),
}

// Clap 通过 FromStr 自动将其作为 value_parser
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 {
  /// The version can be a specific version, "latest" for the latest current version, or "lts" for the
  /// most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults
  /// to system arch). Set [arch] to "all" to install 32 AND 64 bit versions.
  /// Add --insecure to the end of this command to bypass SSL validation of the remote download server.
  #[command(visible_alias = "i")]
  Install {
    /// The version can be a specific version, "latest" for the latest current version, or "lts" for the
    /// most recent LTS version. [possible values: <semver>(eg: 1.1.0), lts, latest]
    version: VersionSpec,
    /// Specify whether to install the 32 or 64 bit version (defaults to system arch).
    arch: Option<ArchSpec>,
    /// Pass SSL validation of the remote download server.
    #[arg(short, long, default_value_t = false)]
    insecure: bool,
  },
  /// The version must be a specific version.
  #[command(visible_alias = "un")]
  Uninstall {
    /// The version to uninstall.
    version: VersionSpec,
  },
  /// List the node.js installations.
  #[command(visible_alias = "ls")]
  List {
    /// Show online available versions.
    #[arg(short, long, default_value_t = false)]
    available: bool,
  },
  /// Enable node.js version management.
  On,
  /// Disable node.js version management.
  Off,
  /// Set the directory where nvm should store different versions of node.js.
  /// If <path> is not set, the current root will be displayed.
  Root {
    /// The path to set as the root directory.
    path: Option<String>,
  },
  /// Show if node is running in 32 or 64 bit mode.
  Arch,
  /// Set a proxy to use for downloads.
  Proxy {
    /// Leave [url] blank to see the current proxy.
    /// Set [url] to "none" to remove the proxy.
    url: Option<String>,
  },
  /// Display active version.
  Current,
  /// Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.
  NodeMirror {
    /// The node mirror to use. Leave [url] blank to use default url.
    url: Option<String>,
  },
  /// Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to use default url.
  NpmMirror {
    /// The npm mirror to use. Leave [url] blank to use default url.
    url: Option<String>,
  },
  /// Switch to use the specified version. Optionally use "latest", "lts", or "newest".
  /// "newest" is the latest installed version. Optionally specify 32/64bit architecture.
  /// nvm use <arch> will continue using the selected version, but switch to 32/64 bit mode.
  Use {
    /// The version to use.
    version: VersionSpec,
    /// The architecture to use.
    arch: Option<ArchSpec>,
  },
}

#[derive(
  Clone, Debug, ValueEnum, Display, Deserialize_repr, Serialize_repr, PartialEq,
)]
#[repr(u8)]
pub enum ArchSpec {
  /// 32 bit
  #[value(name = "32")]
  #[strum(to_string = "32")]
  X86 = 32,
  /// 64 bit
  #[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), // 只接受 false;如果 JSON 里出现 true 也会匹配进来
}

/// Node.js 版本发布信息
#[derive(Debug, Clone, Deserialize)]
pub struct NodeReleaseInfo {
  /// 版本号,如 "v26.8.1"
  pub version: String,

  /// 发布日期,格式 YYYY-MM-DD
  // #[serde(deserialize_with = "deserialize_jiff_date")]
  #[allow(dead_code)]
  // #[tabled(skip)]
  pub date: String,

  // /// 可用的构建产物/平台列表
  // #[tabled(skip)]
  // pub files: Vec<String>,

  // /// 捆绑的 npm 版本
  // #[tabled(skip)]
  // pub npm: Option<String>,

  // /// V8 引擎版本
  // #[tabled(skip)]
  // pub v8: String,

  // /// libuv 版本
  // #[tabled(skip)]
  // pub uv: Option<String>,

  // /// zlib 版本
  // #[tabled(skip)]
  // pub zlib: Option<String>,

  // /// OpenSSL 版本
  // #[tabled(skip)]
  // pub openssl: Option<String>,

  // /// Node-API (ABI) 模块版本号
  // #[tabled(skip)]
  // #[serde(rename = "modules")]
  // pub abi_version: Option<String>,

  // /// 是否为 LTS(长期支持)版本
  // #[tabled(skip)]
  // #[serde(deserialize_with = "deserialize_lts")]
  pub lts: LtsSpec,
  // /// 是否为安全修复版本
  // #[tabled(skip)]
  // pub security: bool,
}

/// 核心查询缓存(启动时构建一次)
#[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())
  }

  /// ✅ 获取最新 LTS 版本
  pub fn latest_lts(&self) -> Option<String> {
    self
      .inner
      .iter()
      .find(|r| matches!(r.lts, LtsSpec::Codename(_)))
      .map(|r| r.version.clone())
  }

  // /// ✅ 按条件组合查询(示例:最新 LTS + 指定平台)
  // pub fn latest_lts_with_platform(
  //   &self,
  //   platform: &str,
  // ) -> Option<&NodeReleaseInfo> {
  //   self
  //     .lts_indices
  //     .iter()
  //     .map(|&i| &self.sorted_releases[i])
  //     .find(|r| r.files.iter().any(|f| f == platform))
  // }

  /// 版本查询
  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)
  }

  /// 最新的 N 个版本
  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)
  }

  /// 最新 n 个 LTS 版本
  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 {
  /// Node.js storage root
  pub root: Option<PathBuf>,
  /// Node.js proxy
  #[serde(deserialize_with = "deserialize_proxy")]
  pub proxy: Option<String>,
  /// Node.js mirror
  pub node_mirror: Option<String>,
  /// npm mirror
  pub npm_mirror: Option<String>,
  pub arch: Option<ArchSpec>,
  pub originalpath: Option<PathBuf>,
  pub originalversion: Option<String>,
  // pub symlink: 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 {
  /// Load config from file. Returns None if no config file exists.<br>
  /// If both toml and yaml features are enabled, it will try to load from toml first.
  /// If toml is not enabled, it will try to load from yaml.
  /// If yaml is not enabled, it will return None.
  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");

    // toml 优先;仅在未启用 toml 时才使用 txt,与 load() 的优先级保持一致
    #[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())
}