@wwkit/opm 1.0.21 → 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.
- package/docs/managers/mise.md +38 -0
- package/docs/managers/node.md +16 -16
- package/docs/opm.html +21 -20
- package/package.json +6 -4
- package/src/cli/groups/mise.js +128 -0
- package/src/cli/groups/package.js +54 -10
- package/src/cli/groups/version-manager.js +2 -2
- package/src/cli/index.js +2 -2
- package/src/config.json5 +15 -11
- package/src/index.js +4 -4
- package/src/managers/node.js +263 -78
- package/src/managers/runtime.js +4 -4
- package/src/presets.js +0 -1
- package/src/tools/webwork/index.js +325 -26
- package/docs/managers/nvm.md +0 -36
- package/src/cli/groups/nvm.js +0 -81
|
@@ -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 流程(
|
|
8
|
-
* 镜像检查(必查 npm/pip + 按需
|
|
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,
|
|
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' },
|
|
@@ -55,7 +56,7 @@ export class WebworkGroup {
|
|
|
55
56
|
return
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
const parsed = parseFlags(rest)
|
|
59
|
+
const parsed = parseFlags(rest, { booleans: ['f', 'force'] })
|
|
59
60
|
|
|
60
61
|
switch (action) {
|
|
61
62
|
case 'version':
|
|
@@ -117,9 +118,9 @@ export class WebworkGroup {
|
|
|
117
118
|
output(result)
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
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,14 +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)
|
|
229
|
+
const force = !!(parsed.flags.force || parsed.flags.f)
|
|
165
230
|
|
|
166
|
-
console.log('\n========== 步骤 1/
|
|
231
|
+
console.log('\n========== 步骤 1/6: 镜像可达检查 ==========')
|
|
167
232
|
const env = await this._checkEnv()
|
|
168
233
|
await this._checkMirrors(proxy, env)
|
|
169
234
|
|
|
170
|
-
console.log('\n========== 步骤 2/
|
|
235
|
+
console.log('\n========== 步骤 2/6: Ensure node / python ==========')
|
|
171
236
|
const ensured = []
|
|
172
237
|
if (!env.node.satisfied) {
|
|
173
238
|
console.log(` [ENSURE] node >= ${env.node.required}...`)
|
|
@@ -184,21 +249,28 @@ export class WebworkGroup {
|
|
|
184
249
|
console.log(` → python ${env.python.provided} 已满足`)
|
|
185
250
|
}
|
|
186
251
|
|
|
187
|
-
console.log('\n========== 步骤 3/
|
|
252
|
+
console.log('\n========== 步骤 3/6: 写入 NPM_REGISTRY / PIP_REGISTRY / REGISTRY_PROXY ==========')
|
|
188
253
|
this._writeRegistryEnv(proxy)
|
|
189
254
|
|
|
190
|
-
console.log('\n========== 步骤 4/
|
|
191
|
-
const blue = await this._installPipPackage('blues-lib', proxy)
|
|
255
|
+
console.log('\n========== 步骤 4/6: 安装 blues-lib ==========')
|
|
256
|
+
const blue = await this._installPipPackage('blues-lib', proxy, force)
|
|
192
257
|
if (blue.action === 'skipped') {
|
|
193
258
|
console.log(` → blues-lib ${blue.version} 已安装,跳过`)
|
|
259
|
+
} else if (blue.action === 'upgraded') {
|
|
260
|
+
console.log(` ✓ blues-lib 已升级到最新(提供 ww 命令)`)
|
|
194
261
|
} else {
|
|
195
262
|
console.log(` ✓ blues-lib 安装完成(提供 ww 命令)`)
|
|
196
263
|
}
|
|
197
264
|
|
|
198
|
-
console.log('\n========== 步骤 5/
|
|
265
|
+
console.log('\n========== 步骤 5/6: ww init ==========')
|
|
199
266
|
this._runWwInit('init')
|
|
200
267
|
|
|
201
|
-
|
|
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 })
|
|
202
274
|
}
|
|
203
275
|
|
|
204
276
|
async _upgrade(parsed) {
|
|
@@ -226,6 +298,191 @@ export class WebworkGroup {
|
|
|
226
298
|
execSync(`ww ${action}`, { encoding: 'utf8', stdio: 'inherit' })
|
|
227
299
|
}
|
|
228
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
|
+
|
|
229
486
|
/**
|
|
230
487
|
* 将 NPM_REGISTRY / PIP_REGISTRY / REGISTRY_PROXY 写入 shell profile,并同步到当前会话 env
|
|
231
488
|
* blues-lib 的相关命令依赖镜像,写入后保持与 opm 配置一致;代理同理,ww init 用同一机制读取
|
|
@@ -281,7 +538,7 @@ export class WebworkGroup {
|
|
|
281
538
|
/**
|
|
282
539
|
* 检查镜像可达性(HEAD 请求,不可达则抛错)
|
|
283
540
|
* 必查:npm / pip(blues-lib 依赖,环境必然可用)
|
|
284
|
-
* 条件查:
|
|
541
|
+
* 条件查:node(仅 env.node 不满足时)、uv(仅 env.python 不满足时)
|
|
285
542
|
* cft 不查:由 ww init 全权处理插件安装,不再单独检查。
|
|
286
543
|
* @param {string} proxy
|
|
287
544
|
* @param {{ node: { satisfied: boolean }, python: { satisfied: boolean } } | null} env
|
|
@@ -301,7 +558,6 @@ export class WebworkGroup {
|
|
|
301
558
|
{ name: 'npm', url: getActiveRegistry('npm') },
|
|
302
559
|
{ name: 'pip', url: getActiveRegistry('pip') },
|
|
303
560
|
...(needNode ? [
|
|
304
|
-
{ name: 'nvm', url: getActiveRegistry('nvm') },
|
|
305
561
|
{ name: 'node', url: getActiveRegistry('node') },
|
|
306
562
|
] : []),
|
|
307
563
|
...(needPython ? [
|
|
@@ -431,7 +687,7 @@ export class WebworkGroup {
|
|
|
431
687
|
|
|
432
688
|
async _nodeVersions(proxy, num) {
|
|
433
689
|
const mgr = new NodeManager()
|
|
434
|
-
if (!
|
|
690
|
+
if (!isMiseInstalled()) return []
|
|
435
691
|
try {
|
|
436
692
|
const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
|
|
437
693
|
const versions = mgr._parseRemote(out).map((v) => v.replace(/^v/, ''))
|
|
@@ -443,6 +699,9 @@ export class WebworkGroup {
|
|
|
443
699
|
|
|
444
700
|
async _installNode(proxy) {
|
|
445
701
|
const defaultVersion = getConfig().node?.defaultVersion || '24'
|
|
702
|
+
// 先前置 mise 目录到当前进程 PATH,让 _nodeVersion() 用 mise 管理的 node 判断,
|
|
703
|
+
// 而非系统残留的旧版 node(避免误判不满足而重复安装)。
|
|
704
|
+
if (isMiseInstalled()) this._ensureMiseShimsOnPath()
|
|
446
705
|
const installed = this._nodeVersion()
|
|
447
706
|
if (installed !== '-' && this._compareVersion(installed, defaultVersion) >= 0) {
|
|
448
707
|
return { action: 'skipped', reason: `node ${installed} >= ${defaultVersion}`, version: installed }
|
|
@@ -452,15 +711,50 @@ export class WebworkGroup {
|
|
|
452
711
|
} else {
|
|
453
712
|
console.log(` node not found, installing ${defaultVersion}`)
|
|
454
713
|
}
|
|
455
|
-
if (!
|
|
456
|
-
console.log('
|
|
457
|
-
|
|
714
|
+
if (!isMiseInstalled()) {
|
|
715
|
+
console.log(' mise not found, installing mise...')
|
|
716
|
+
await installMise(proxy)
|
|
458
717
|
}
|
|
459
718
|
const mgr = new NodeManager()
|
|
460
719
|
mgr._execInherit(mgr.cfg.installCmd(defaultVersion), { proxy })
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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) {
|
|
747
|
+
try {
|
|
748
|
+
const currentOut = mgr._exec(mgr.cfg.currentCmd, { allowNonZero: true, proxy })
|
|
749
|
+
const parsed = mgr.cfg.parseCurrent(currentOut)
|
|
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
|
|
756
|
+
} catch {}
|
|
757
|
+
return ''
|
|
464
758
|
}
|
|
465
759
|
|
|
466
760
|
async _pipInstalled(name) {
|
|
@@ -497,10 +791,13 @@ export class WebworkGroup {
|
|
|
497
791
|
.slice(0, num)
|
|
498
792
|
}
|
|
499
793
|
|
|
500
|
-
async _installPipPackage(name, proxy) {
|
|
794
|
+
async _installPipPackage(name, proxy, force = false) {
|
|
501
795
|
const pip = new PipManager()
|
|
502
796
|
const existing = await pip.isInstalled(name, { global: true })
|
|
503
797
|
if (existing.installed) {
|
|
798
|
+
if (force) {
|
|
799
|
+
return pip.upgrade(name, { global: true, proxy })
|
|
800
|
+
}
|
|
504
801
|
return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
|
|
505
802
|
}
|
|
506
803
|
return pip.install(name, null, { global: true, proxy })
|
|
@@ -531,6 +828,7 @@ ${actionLines}
|
|
|
531
828
|
|
|
532
829
|
Options:
|
|
533
830
|
-p, --proxy <url> Proxy for install/upgrade/versions (overrides config proxy.active)
|
|
831
|
+
-f, --force Force upgrade blues-lib even if already installed (install only)
|
|
534
832
|
-h, --help Show this help
|
|
535
833
|
|
|
536
834
|
Examples:
|
|
@@ -538,6 +836,7 @@ Examples:
|
|
|
538
836
|
opm webwork versions Show latest available versions (default 10, -n <num>)
|
|
539
837
|
opm webwork installed Show install status of all components
|
|
540
838
|
opm webwork install Mirror check + ensure runtime + registry env + blues-lib + ww init
|
|
839
|
+
opm webwork install -f Same as install, but force upgrade blues-lib to latest
|
|
541
840
|
opm webwork uninstall Uninstall all components in reverse order
|
|
542
841
|
opm webwork upgrade Mirror check + write registry env + ww upgrade
|
|
543
842
|
`)
|
package/docs/managers/nvm.md
DELETED
|
@@ -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
|
-
```
|
package/src/cli/groups/nvm.js
DELETED
|
@@ -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
|
-
}
|