@wwkit/opm 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,15 @@
4
4
  * 只负责运行环境本身(node/python/blues-lib),插件(cft/opencode/harness/sshproxy)
5
5
  * 由 blues-lib 提供的 ww init / ww init -u 全权处理,不再单独安装/检查。
6
6
  *
7
- * install 流程(5 步):
8
- * 镜像检查(必查 npm/pip + 按需 nvm/node/uv) → ensure node/python → 写 NPM/PIP_REGISTRY/REGISTRY_PROXY → 装 blues-lib → ww init
7
+ * install 流程(6 步):
8
+ * 镜像检查(必查 npm/pip + 按需 node/uv) → ensure node/python → 写 NPM/PIP_REGISTRY/REGISTRY_PROXY → 装 blues-lib → ww init → 插件命令 PATH 保障
9
9
  * upgrade 流程(3 步):
10
10
  * 镜像检查(必查 npm/pip) → 写 NPM/PIP_REGISTRY/REGISTRY_PROXY → ww upgrade
11
11
  *
12
12
  * 命令: version / versions / installed / install / uninstall / upgrade / help
13
13
  */
14
14
 
15
- import { execSync } from 'node:child_process'
15
+ import { execSync, spawnSync } from 'node:child_process'
16
16
 
17
17
  import { Shell } from '@wwkit/shared'
18
18
  import { parseFlags } from '../../cli/helpers/args.js'
@@ -20,7 +20,8 @@ import { output } from '../../formatter.js'
20
20
  import { getActiveProxy, getConfig, getActiveRegistry } from '../../config.js'
21
21
  import { PipManager } from '../../managers/pip.js'
22
22
  import { PythonManager, isUvInstalled, installUv } from '../../managers/python.js'
23
- import { NodeManager, isNvmInstalled, installNvm } from '../../managers/node.js'
23
+ import { NodeManager, isMiseInstalled, installMise, getMiseInstallDir, detectSystemNodeConflict, prependSystemPathDirs } from '../../managers/node.js'
24
+ import { getMiseShimsDir } from '../../cli/groups/mise.js'
24
25
  import { checkReachability } from '../../cli/groups/ping.js'
25
26
 
26
27
  const shell = new Shell()
@@ -35,7 +36,7 @@ const ACTIONS = {
35
36
  version: { desc: 'Show installed version of each component' },
36
37
  versions: { desc: 'Show latest available versions (default 10, -n <count>)' },
37
38
  installed: { desc: 'Show install status (true/false) of each component' },
38
- install: { desc: 'Mirror check + ensure runtime + write registry env + blues-lib + ww init' },
39
+ install: { desc: 'Mirror check + ensure runtime + write registry env + blues-lib + ww init + PATH 保障' },
39
40
  uninstall: { desc: 'Uninstall all components in reverse order' },
40
41
  upgrade: { desc: 'Mirror check + write registry env + ww upgrade' },
41
42
  help: { desc: 'Show this help' },
@@ -117,9 +118,9 @@ export class WebworkGroup {
117
118
  output(result)
118
119
  }
119
120
 
120
- /**
121
- * 批量执行组件操作(uninstall 用)
122
- * 每步打印 [n/total] 组件名 + 跳过/成功/失败日志,最后汇总
121
+ /**
122
+ * 批量执行组件操作(uninstall 用)
123
+ * 每步打印 [n/total] 组件名 + 跳过/成功/失败日志,最后汇总
123
124
  * @param {string} verb - install|uninstall|upgrade
124
125
  * @param {Array} components - 组件列表
125
126
  * @param {string} proxy - 代理地址
@@ -160,15 +161,78 @@ export class WebworkGroup {
160
161
  return { results, ok, skipped, failed }
161
162
  }
162
163
 
164
+ /**
165
+ * 检测当前进程是否以管理员权限运行(Windows)
166
+ *
167
+ * 首选 `net session`(快):管理员返回 0,普通权限 Access Denied。
168
+ * net session 依赖 LanmanServer 服务——被禁用时管理员也会失败,故失败时用
169
+ * PowerShell IsInRole 复核(准确但慢 ~1s),避免误判阻止合法的管理员用户。
170
+ * 非 Windows 恒返回 true(无此概念)。
171
+ * @param {Function} [execSyncImpl] - 可注入的 execSync(测试用)
172
+ * @returns {boolean}
173
+ * @private
174
+ */
175
+ _isWindowsAdmin(execSyncImpl = execSync) {
176
+ if (!shell.isWindows) return true
177
+ try {
178
+ execSyncImpl('net session', { stdio: ['pipe', 'pipe', 'pipe'] })
179
+ return true
180
+ } catch {}
181
+ try {
182
+ const out = execSyncImpl(
183
+ 'powershell -NoProfile -Command "[int]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"',
184
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
185
+ )
186
+ return String(out).trim() === '1'
187
+ } catch {
188
+ return false
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Windows 下要求管理员权限运行 install:非管理员直接阻止(fail fast)。
194
+ *
195
+ * install 需要写系统/用户环境变量(PATH、注册表)让 node 在任意新终端可用;
196
+ * 普通权限无法修复系统 PATH 的旧 node 冲突,会留下"装完但终端看不到"的半成品
197
+ * 环境,故在入口直接阻止并给出提权指导。非 Windows 直接放行。
198
+ * @param {Function} [execSyncImpl] - 可注入的 execSync(测试用)
199
+ * @returns {boolean} true = 已阻止(调用方应终止);false = 放行
200
+ * @private
201
+ */
202
+ _requireWindowsAdmin(execSyncImpl = execSync) {
203
+ if (!shell.isWindows) return false
204
+ if (this._isWindowsAdmin(execSyncImpl)) return false
205
+ console.error(
206
+ [
207
+ '',
208
+ '✗ 需要管理员权限:opm webwork install 已阻止执行',
209
+ '',
210
+ ' 原因:install 要写系统/用户环境变量(PATH、注册表),让 mise 管理的 node/npm',
211
+ ' 在任意新终端可用。普通权限无法修复系统 PATH 中的旧版 node 冲突,',
212
+ ' 会导致"安装完成但终端看到的仍是旧版 node"的半成品状态。',
213
+ '',
214
+ ' 请以管理员身份重新运行:',
215
+ ' Win11:右键开始按钮 → "终端(管理员)"(或按 Win+X 再按 A)',
216
+ ' Win10:搜索 PowerShell → 右键 → "以管理员身份运行"',
217
+ ' 然后执行:opm webwork install',
218
+ '',
219
+ ].join('\n')
220
+ )
221
+ return true
222
+ }
223
+
163
224
  async _install(parsed) {
225
+ if (this._requireWindowsAdmin()) {
226
+ process.exit(1)
227
+ }
164
228
  const proxy = this._resolveProxy(parsed)
165
229
  const force = !!(parsed.flags.force || parsed.flags.f)
166
230
 
167
- console.log('\n========== 步骤 1/5: 镜像可达检查 ==========')
231
+ console.log('\n========== 步骤 1/6: 镜像可达检查 ==========')
168
232
  const env = await this._checkEnv()
169
233
  await this._checkMirrors(proxy, env)
170
234
 
171
- console.log('\n========== 步骤 2/5: Ensure node / python ==========')
235
+ console.log('\n========== 步骤 2/6: Ensure node / python ==========')
172
236
  const ensured = []
173
237
  if (!env.node.satisfied) {
174
238
  console.log(` [ENSURE] node >= ${env.node.required}...`)
@@ -185,10 +249,10 @@ export class WebworkGroup {
185
249
  console.log(` → python ${env.python.provided} 已满足`)
186
250
  }
187
251
 
188
- console.log('\n========== 步骤 3/5: 写入 NPM_REGISTRY / PIP_REGISTRY / REGISTRY_PROXY ==========')
252
+ console.log('\n========== 步骤 3/6: 写入 NPM_REGISTRY / PIP_REGISTRY / REGISTRY_PROXY ==========')
189
253
  this._writeRegistryEnv(proxy)
190
254
 
191
- console.log('\n========== 步骤 4/5: 安装 blues-lib ==========')
255
+ console.log('\n========== 步骤 4/6: 安装 blues-lib ==========')
192
256
  const blue = await this._installPipPackage('blues-lib', proxy, force)
193
257
  if (blue.action === 'skipped') {
194
258
  console.log(` → blues-lib ${blue.version} 已安装,跳过`)
@@ -198,10 +262,15 @@ export class WebworkGroup {
198
262
  console.log(` ✓ blues-lib 安装完成(提供 ww 命令)`)
199
263
  }
200
264
 
201
- console.log('\n========== 步骤 5/5: ww init ==========')
265
+ console.log('\n========== 步骤 5/6: ww init ==========')
202
266
  this._runWwInit('init')
203
267
 
204
- output({ action: 'install', env, ensured, bluesLib: blue, init: 'ww init' })
268
+ console.log('\n========== 步骤 6/6: 插件命令 PATH 保障 ==========')
269
+ const pathFix = this._ensurePluginCommandsOnPath()
270
+ const shimsFix = this._ensureMiseShimsOnPath()
271
+ this._printCftEnvHint()
272
+
273
+ output({ action: 'install', env, ensured, bluesLib: blue, init: 'ww init', pathFix, shimsFix })
205
274
  }
206
275
 
207
276
  async _upgrade(parsed) {
@@ -229,6 +298,191 @@ export class WebworkGroup {
229
298
  execSync(`ww ${action}`, { encoding: 'utf8', stdio: 'inherit' })
230
299
  }
231
300
 
301
+ /**
302
+ * 获取 npm 全局 bin 目录(npm config get prefix)
303
+ *
304
+ * Windows 上该目录即 npm 全局安装的 shim 目录(.cmd/.ps1/无扩展名脚本);
305
+ * ww init 的插件安装(cft/opencode/sshproxy/harness)把命令装在这里。
306
+ * @returns {string|null}
307
+ * @private
308
+ */
309
+ _npmGlobalBin() {
310
+ try {
311
+ const out = execSync('npm config get prefix', {
312
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
313
+ })
314
+ return out.trim() || null
315
+ } catch {
316
+ return null
317
+ }
318
+ }
319
+
320
+ /**
321
+ * 检查注册表 HKCU\Environment 是否存在指定用户环境变量(Windows)
322
+ * @param {string} name - 环境变量名
323
+ * @returns {boolean}
324
+ * @private
325
+ */
326
+ _registryEnvHas(name) {
327
+ if (!shell.isWindows) return false
328
+ try {
329
+ execSync(`reg query "HKCU\\Environment" /v ${name}`, {
330
+ stdio: ['pipe', 'pipe', 'pipe'],
331
+ })
332
+ return true
333
+ } catch {
334
+ return false
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Windows 下确保 npm 全局 bin 目录进入持久化用户 PATH(幂等)
340
+ *
341
+ * 解决场景:ww init 在 MINGW64 / bash(PATH 含 nvm node)中运行时,
342
+ * `npm install -g` 会把插件装到 shell 会话专属前缀(如 ~/.nvm/.../bin/),
343
+ * 而该目录不在系统/用户 PATH,其他 shell(cmd/PowerShell/Git Bash 默认)
344
+ * 里 cft/opencode/sshproxy 命令不可用。此方法把该目录追加进
345
+ * HKCU\Environment\Path(REG_EXPAND_SZ),重启终端后全局生效。
346
+ *
347
+ * @param {Shell} [shellImpl] - 可注入的 Shell 实例(测试用),默认模块级 shell
348
+ * @returns {{ bin: string|null, added: boolean, inPath: boolean }}
349
+ * @private
350
+ */
351
+ _ensurePluginCommandsOnPath(shellImpl = shell) {
352
+ if (!shellImpl.isWindows) {
353
+ return { bin: null, added: false, inPath: false }
354
+ }
355
+ const bin = this._npmGlobalBin()
356
+ if (!bin) {
357
+ console.log(' [WARN] 无法获取 npm 全局目录(npm config get prefix 失败),跳过 PATH 保障')
358
+ return { bin: null, added: false, inPath: false }
359
+ }
360
+ const userPath = typeof shellImpl.getUserPath === 'function' ? shellImpl.getUserPath() : ''
361
+ const inPath = shellImpl.dirInPath(bin, userPath)
362
+ if (inPath) {
363
+ console.log(` ✓ npm 全局 bin 已在用户 PATH: ${bin}`)
364
+ return { bin, added: false, inPath: true }
365
+ }
366
+ const added = shellImpl.ensurePathEntry(bin)
367
+ console.log(
368
+ added
369
+ ? ` ✓ 已追加 npm 全局 bin 到用户 PATH(重启终端后生效): ${bin}`
370
+ : ` ✗ 追加用户 PATH 失败,请手动将以下目录加入 PATH 后重启终端:\n ${bin}`
371
+ )
372
+ return { bin, added, inPath: false }
373
+ }
374
+
375
+ /**
376
+ * 确保 mise shims 在当前进程 PATH,并在 Windows 上持久化到用户 PATH(幂等)
377
+ *
378
+ * 所有平台:把 mise 安装目录 + shims 目录前置到当前进程 PATH,让 webwork install
379
+ * 后续步骤(ww init 的 npm install -g)用 mise 管理的 node,而非系统残留旧版。
380
+ * Windows 额外把目录写入持久化用户 PATH(HKCU 注册表)并检测/修复系统 PATH 冲突;
381
+ * Linux/macOS 不写 profile(靠 mise activate 管理 shell rc),仅前置当前进程 PATH。
382
+ * mise 未安装时跳过(node 由其他方式管理,无 shims 可保障)。
383
+ *
384
+ * @param {Shell} [shellImpl] - 可注入的 Shell 实例(测试用),默认模块级 shell
385
+ * @param {Function} [isMiseOk] - 可注入的 mise 安装检测(测试用),默认 isMiseInstalled
386
+ * @returns {{ shims: string|null, added: boolean, inPath: boolean }}
387
+ * @private
388
+ */
389
+ _ensureMiseShimsOnPath(shellImpl = shell, isMiseOk = isMiseInstalled) {
390
+ // mise 未安装:所有平台跳过(无 shims 可保障)
391
+ if (!isMiseOk()) {
392
+ if (shellImpl.isWindows) {
393
+ console.log(' [SKIP] mise 未安装,跳过 mise shims PATH 保障')
394
+ }
395
+ return { shims: null, added: false, inPath: false }
396
+ }
397
+ // 需要两个目录都在 PATH:
398
+ // 1. mise 安装目录(~/.local/bin,mise 二进制本身)——node shim 内部调用 `mise x -- node`
399
+ // 2. mise shims 目录(node/npm/npx shim)——mise 为激活工具生成的 shim
400
+ // 缺任一,node 命令都不可用(shim 找不到 mise,或 PATH 无 shim)。
401
+ const dirs = [getMiseInstallDir(), getMiseShimsDir()]
402
+ // 所有平台:把 mise 目录前置到当前进程 PATH,让本进程后续命令(ww init 的 npm)用 mise node。
403
+ // 进程从旧 PATH 启动时不含 mise shims;webwork install 内部 node/npm 命令必须用 mise 的 node,
404
+ // 故显式前置(幂等:已在则移到最前)。
405
+ if (typeof shellImpl.prependProcPath === 'function') {
406
+ shellImpl.prependProcPath(dirs)
407
+ }
408
+ // Linux/macOS:不写持久化 profile(靠 mise activate 管理 shell rc),仅前置当前进程 PATH
409
+ if (!shellImpl.isWindows) {
410
+ return { shims: dirs[1], added: false, inPath: false }
411
+ }
412
+ const userPath = typeof shellImpl.getUserPath === 'function' ? shellImpl.getUserPath() : ''
413
+ const inPath = dirs.every((d) => shellImpl.dirInPath(d, userPath))
414
+ // 逐个前置到用户 PATH(幂等:已在最前 → false;在末尾/缺失 → 移到最前)。
415
+ // 前置(而非追加):系统 PATH 可能已有旧版 node(如 D:\library\nodejs),
416
+ // 只有把 mise 目录前置才能让新 shell 优先用 mise 管理的 node。
417
+ let added = false
418
+ for (const d of dirs) {
419
+ if (shellImpl.prependPathEntry(d)) added = true
420
+ }
421
+ if (inPath && !added) {
422
+ console.log(` ✓ mise 目录已在用户 PATH 最前: ${dirs.join('; ')}`)
423
+ } else {
424
+ console.log(
425
+ added
426
+ ? ` ✓ 已前置 mise 目录到用户 PATH(重启终端后 node/npm 可用): ${dirs.join('; ')}`
427
+ : ` ✗ 前置 mise 目录失败,请手动将以下目录加入 PATH 后重启终端:\n ${dirs.join('\n ')}`
428
+ )
429
+ }
430
+ // 系统 PATH 优先于用户 PATH:若系统 PATH 有旧版 node,会抢先于 mise shims。
431
+ // 自动尝试修复(管理员运行本命令时直接成功);无权限才提示用户处理。
432
+ const sysNode = this._detectSystemNodeConflict()
433
+ if (sysNode) {
434
+ if (this._tryFixSystemNode(dirs)) {
435
+ console.log(` ✓ 系统 PATH 含旧版 node(${sysNode}),已自动把 mise 目录前置到系统 PATH(重启终端后生效)`)
436
+ } else {
437
+ console.log(` [WARN] 系统 PATH 含旧版 node(${sysNode}),优先于 mise shims,修复系统 PATH 需管理员权限`)
438
+ console.log(' → 以管理员身份运行 `opm node fix-path`,或以管理员身份重跑 `opm webwork install` 自动完成')
439
+ }
440
+ }
441
+ return { shims: dirs[1], added, inPath }
442
+ }
443
+
444
+ /**
445
+ * 尝试把 mise 目录前置到系统 PATH(HKLM)——管理员运行时成功,普通权限失败
446
+ * @param {string[]} dirs - mise 安装目录 + shims 目录
447
+ * @param {Function} [spawnSyncImpl] - 可注入的 spawnSync(测试用)
448
+ * @returns {boolean} 是否修复成功
449
+ * @private
450
+ */
451
+ _tryFixSystemNode(dirs, spawnSyncImpl = spawnSync) {
452
+ return prependSystemPathDirs(dirs, spawnSyncImpl)
453
+ }
454
+
455
+ /**
456
+ * 检测系统 PATH(HKLM)中是否含 node.exe 的目录(Windows)
457
+ *
458
+ * Windows 新进程 PATH = 系统 PATH + 用户 PATH,系统 PATH 优先。若系统 PATH 有
459
+ * 旧版 node(如 D:\library\nodejs\node.exe),会抢先于用户 PATH 的 mise shims,
460
+ * 导致新终端中 node 仍是旧版。返回该目录路径;无冲突返回空串。
461
+ * @returns {string}
462
+ * @private
463
+ */
464
+ _detectSystemNodeConflict() {
465
+ return detectSystemNodeConflict()
466
+ }
467
+
468
+ /**
469
+ * 打印 CFT 环境变量状态提示(Windows)
470
+ *
471
+ * SELENIUM_CFT_* 由 @wwkit/cft postinstall 经 setx 写入注册表,但已打开的
472
+ * shell 不会刷新注册表变更 —— 需重启终端后 ww webdriver 检查才识别到 CFT。
473
+ * @private
474
+ */
475
+ _printCftEnvHint() {
476
+ if (!shell.isWindows) return
477
+ if (this._registryEnvHas('SELENIUM_CFT_DIR')) {
478
+ console.log(' ✓ SELENIUM_CFT_* 已写入注册表(由 @wwkit/cft postinstall 设置)')
479
+ console.log(' → 重启终端后 ww webdriver status 将识别到 CFT 安装')
480
+ } else {
481
+ console.log(' [WARN] SELENIUM_CFT_DIR 未写入注册表,cft postinstall 可能未执行')
482
+ console.log(' → 请运行 ww plugin init -u 重装 cft 插件')
483
+ }
484
+ }
485
+
232
486
  /**
233
487
  * 将 NPM_REGISTRY / PIP_REGISTRY / REGISTRY_PROXY 写入 shell profile,并同步到当前会话 env
234
488
  * blues-lib 的相关命令依赖镜像,写入后保持与 opm 配置一致;代理同理,ww init 用同一机制读取
@@ -284,7 +538,7 @@ export class WebworkGroup {
284
538
  /**
285
539
  * 检查镜像可达性(HEAD 请求,不可达则抛错)
286
540
  * 必查:npm / pip(blues-lib 依赖,环境必然可用)
287
- * 条件查:nvm + node(仅 env.node 不满足时)、uv(仅 env.python 不满足时)
541
+ * 条件查:node(仅 env.node 不满足时)、uv(仅 env.python 不满足时)
288
542
  * cft 不查:由 ww init 全权处理插件安装,不再单独检查。
289
543
  * @param {string} proxy
290
544
  * @param {{ node: { satisfied: boolean }, python: { satisfied: boolean } } | null} env
@@ -304,7 +558,6 @@ export class WebworkGroup {
304
558
  { name: 'npm', url: getActiveRegistry('npm') },
305
559
  { name: 'pip', url: getActiveRegistry('pip') },
306
560
  ...(needNode ? [
307
- { name: 'nvm', url: getActiveRegistry('nvm') },
308
561
  { name: 'node', url: getActiveRegistry('node') },
309
562
  ] : []),
310
563
  ...(needPython ? [
@@ -434,7 +687,7 @@ export class WebworkGroup {
434
687
 
435
688
  async _nodeVersions(proxy, num) {
436
689
  const mgr = new NodeManager()
437
- if (!isNvmInstalled()) return []
690
+ if (!isMiseInstalled()) return []
438
691
  try {
439
692
  const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
440
693
  const versions = mgr._parseRemote(out).map((v) => v.replace(/^v/, ''))
@@ -446,6 +699,9 @@ export class WebworkGroup {
446
699
 
447
700
  async _installNode(proxy) {
448
701
  const defaultVersion = getConfig().node?.defaultVersion || '24'
702
+ // 先前置 mise 目录到当前进程 PATH,让 _nodeVersion() 用 mise 管理的 node 判断,
703
+ // 而非系统残留的旧版 node(避免误判不满足而重复安装)。
704
+ if (isMiseInstalled()) this._ensureMiseShimsOnPath()
449
705
  const installed = this._nodeVersion()
450
706
  if (installed !== '-' && this._compareVersion(installed, defaultVersion) >= 0) {
451
707
  return { action: 'skipped', reason: `node ${installed} >= ${defaultVersion}`, version: installed }
@@ -455,23 +711,50 @@ export class WebworkGroup {
455
711
  } else {
456
712
  console.log(` node not found, installing ${defaultVersion}`)
457
713
  }
458
- if (!isNvmInstalled()) {
459
- console.log(' nvm not found, installing nvm...')
460
- installNvm(proxy)
714
+ if (!isMiseInstalled()) {
715
+ console.log(' mise not found, installing mise...')
716
+ await installMise(proxy)
461
717
  }
462
718
  const mgr = new NodeManager()
463
719
  mgr._execInherit(mgr.cfg.installCmd(defaultVersion), { proxy })
464
- // nvm-windows 不支持主版本别名(如 `nvm use 24`),需要完整版本号。
465
- // install 后查询 nvm current 获取实际激活的完整版本(如 24.21.0)。
466
- let useVersion = defaultVersion
720
+ // mise install node@<major> 会自动 use 最新 <major>.x;但 default 别名需要完整版本号。
721
+ // 优先 mise current 解析完整版本;失败则回退 mise ls --json 的当前激活版本;
722
+ // 都失败则跳过 use/default(install 已自动激活),绝不回退主版本别名。
723
+ const useVersion = this._resolveInstalledNodeVersion(mgr, proxy)
724
+ if (useVersion) {
725
+ mgr._execInherit(mgr.cfg.useCmd(useVersion), { proxy })
726
+ mgr._execInherit(mgr.cfg.defaultCmd(useVersion), { proxy })
727
+ } else {
728
+ console.log(' [WARN] 无法解析已安装的完整 node 版本,跳过 use/default 设置(mise install 已自动激活)')
729
+ }
730
+ // 装完 node 后立即把 mise 目录前置到当前进程 PATH,确保后续步骤(blues-lib/ww init)
731
+ // 的 node/npm 命令用 mise 管理的 node,而非系统残留的旧版 node。
732
+ this._ensureMiseShimsOnPath()
733
+ return { action: 'installed', version: useVersion || defaultVersion }
734
+ }
735
+
736
+ /**
737
+ * 解析 mise install 后实际激活的完整 node 版本
738
+ *
739
+ * 优先 mise current;失败回退 mise ls --json 的当前激活版本(带 current 标记);
740
+ * 都失败返回空串(调用方跳过 use/default,绝不回退主版本别名)。
741
+ * @param {NodeManager} mgr
742
+ * @param {string} proxy
743
+ * @returns {string}
744
+ * @private
745
+ */
746
+ _resolveInstalledNodeVersion(mgr, proxy) {
467
747
  try {
468
748
  const currentOut = mgr._exec(mgr.cfg.currentCmd, { allowNonZero: true, proxy })
469
749
  const parsed = mgr.cfg.parseCurrent(currentOut)
470
- if (parsed) useVersion = parsed.replace(/^v/, '')
750
+ if (parsed) return parsed.replace(/^v/, '')
751
+ } catch {}
752
+ try {
753
+ const lsOut = mgr._exec(mgr.cfg.listCmd, { allowNonZero: true, proxy })
754
+ const current = mgr._parseInstalled(lsOut).find((i) => i.current)
755
+ if (current) return current.version
471
756
  } catch {}
472
- mgr._execInherit(mgr.cfg.useCmd(useVersion), { proxy })
473
- mgr._execInherit(mgr.cfg.defaultCmd(useVersion), { proxy })
474
- return { action: 'installed', version: useVersion }
757
+ return ''
475
758
  }
476
759
 
477
760
  async _pipInstalled(name) {
@@ -1,36 +0,0 @@
1
- # nvm 命令对照表
2
-
3
- `@wwkit/opm` 统一命令管理 nvm 自身(安装/升级/卸载)。
4
-
5
- ## 命令对照
6
-
7
- | 统一命令 | 原始命令 | 说明 |
8
- |---------|---------|------|
9
- | `opm nvm version` | `nvm --version` | 查看 nvm 版本 |
10
- | `opm nvm registry` | 读 `~/.config/opm/config.json5` 的 `nvm.registry.active` | 查询 nvm git 镜像 |
11
- | `opm nvm registry set <url\|preset>` | 写 `~/.config/opm/config.json5` | 设置 nvm git 镜像 |
12
- | `opm nvm registry presets` | — | 列出可用镜像预设 |
13
- | `opm nvm install` | `git clone <mirror> ~/.nvm` + 写 shell profile | 安装 nvm |
14
- | `opm nvm upgrade` | `cd ~/.nvm && git pull --tags` | 更新 nvm |
15
- | `opm nvm uninstall` | `rm -rf ~/.nvm` | 卸载 nvm |
16
- | `opm nvm dir` | 返回 `~/.nvm` 安装目录 |
17
- | `opm nvm list` | `nvm ls --no-colors` | 列出已安装 Node.js 版本 |
18
-
19
- ## 镜像预设
20
-
21
- | 预设名 | 地址 |
22
- |--------|------|
23
- | `official` | `https://github.com/nvm-sh/nvm.git` |
24
- | `gitee` | `https://gitee.com/mirrors/nvm.git` |
25
-
26
- ## 示例
27
-
28
- ```bash
29
- opm nvm version
30
- opm nvm registry set gitee
31
- opm nvm install
32
- opm nvm upgrade
33
- opm nvm list
34
- opm nvm dir
35
- opm nvm uninstall
36
- ```
@@ -1,81 +0,0 @@
1
- /**
2
- * nvm 命令组 — 管理 nvm 自身
3
- *
4
- * nvm 是 shell function,通过 git clone 安装;
5
- * install = git clone + 写 shell profile;upgrade = git pull;uninstall = rm -rf ~/.nvm。
6
- * registry 是 nvm 的 git 镜像地址(official / gitee)。
7
- */
8
-
9
- import { execSync } from 'node:child_process'
10
- import fs from 'node:fs'
11
- import path from 'node:path'
12
- import os from 'node:os'
13
- import { ToolManagerGroup } from './version-manager.js'
14
- import { Shell } from '@wwkit/shared'
15
- import { getActiveRegistry } from '../../config.js'
16
- import { installNvm, parseNvmInstalled } from '../../managers/node.js'
17
-
18
- const shell = new Shell()
19
-
20
- /**
21
- * 获取 nvm 目录
22
- * @returns {string}
23
- */
24
- function getNvmDir() {
25
- return process.env.NVM_DIR || path.join(os.homedir(), '.nvm')
26
- }
27
-
28
- export class NvmGroup extends ToolManagerGroup {
29
- constructor() {
30
- super({
31
- name: 'nvm',
32
- desc: 'nvm version manager (manage nvm itself)',
33
- needsBash: true,
34
- isInstalled: () => fs.existsSync(path.join(getNvmDir(), 'nvm.sh')),
35
- getPath: () => getNvmDir(),
36
- versionCmd: () => `source "${path.join(getNvmDir(), 'nvm.sh')}" && nvm --version`,
37
- parseVersion: (out) => out.trim(),
38
- listCmd: () => `source "${path.join(getNvmDir(), 'nvm.sh')}" && nvm ls --no-colors`,
39
- parseList: parseNvmInstalled,
40
- install: (proxy) => installNvm(proxy),
41
- upgrade: () => {
42
- const nvmDir = getNvmDir()
43
- if (!fs.existsSync(path.join(nvmDir, '.git'))) {
44
- throw new Error(`${nvmDir} is not a git repository (nvm was not installed via git clone). Run \`opm nvm uninstall -y\` then \`opm nvm install\` to reinstall.`)
45
- }
46
- execSync(`cd "${nvmDir}" && git remote set-url origin "${getActiveRegistry('nvm')}" && git pull --tags`, {
47
- stdio: shell.stdioForExec(true),
48
- shell: shell.resolveBash(),
49
- })
50
- return { action: 'upgraded' }
51
- },
52
- uninstall: () => {
53
- fs.rmSync(getNvmDir(), { recursive: true, force: true })
54
- return { action: 'uninstalled', hint: 'Remove NVM_DIR/source line from shell profile manually' }
55
- },
56
- usage: 'opm nvm <action> [args] [options]',
57
- actions: [
58
- ['version', 'Show nvm version'],
59
- ['installed', 'Check if nvm is installed'],
60
- ['registry', 'Show active nvm git mirror (from opm config)'],
61
- ['registry set <url|preset>', 'Set active nvm git mirror (writes opm config)'],
62
- ['registry presets', 'List available mirror presets'],
63
- ['install', 'Install nvm (git clone + shell profile setup)'],
64
- ['upgrade', 'Update nvm (git pull)'],
65
- ['uninstall', 'Remove nvm (~/.nvm)'],
66
- ['dir', 'Show nvm install directory'],
67
- ['list', 'List installed Node.js versions (nvm ls --no-colors)'],
68
- ['help', 'Show this help'],
69
- ],
70
- examples: [
71
- 'opm nvm version',
72
- 'opm nvm installed',
73
- 'opm nvm registry set gitee',
74
- 'opm nvm install',
75
- 'opm nvm upgrade',
76
- 'opm nvm dir',
77
- 'opm nvm list',
78
- ],
79
- })
80
- }
81
- }