@wwkit/opm 1.0.2 → 1.0.4

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.
@@ -2,9 +2,8 @@
2
2
  * homebrew 命令组 — 管理 Homebrew(brew)自身
3
3
  *
4
4
  * Homebrew 无独立版本管理器,通过官方 shell 脚本安装/卸载。
5
- * 安装方式:curl 管道执行 install.sh(与 uv 同模式);
5
+ * 安装方式:curl 管道执行 install.sh(URL 由 homebrew.registry.active 提供);
6
6
  * upgrade = brew update;uninstall = curl 管道执行 uninstall.sh。
7
- * brew 自身无镜像源(包镜像走 opm brew registry),故无 registry action。
8
7
  * 仅支持 macOS / Linux,Windows 不支持。
9
8
  */
10
9
 
@@ -30,7 +29,6 @@ export class HomebrewGroup extends ToolManagerGroup {
30
29
  super({
31
30
  name: 'homebrew',
32
31
  desc: 'Homebrew manager (install/upgrade/uninstall brew itself)',
33
- hasRegistry: false,
34
32
  isInstalled: () => isHomebrewInstalled(),
35
33
  getPath: () => getBrewRoot(),
36
34
  versionCmd: () => `${getBrewBin()} --version`,
@@ -38,7 +36,7 @@ export class HomebrewGroup extends ToolManagerGroup {
38
36
  const m = out.trim().match(/^Homebrew\s+(\S+)/)
39
37
  return m ? m[1] : out.trim()
40
38
  },
41
- install: () => installHomebrew(),
39
+ install: (proxy) => installHomebrew(proxy),
42
40
  upgrade: () => {
43
41
  execSync(`${getBrewBin()} update`, { stdio: 'inherit' })
44
42
  return { action: 'upgraded' }
@@ -54,7 +52,10 @@ export class HomebrewGroup extends ToolManagerGroup {
54
52
  actions: [
55
53
  ['version', 'Show Homebrew version'],
56
54
  ['installed', 'Check if Homebrew is installed'],
57
- ['install', 'Install Homebrew (curl installer from GitHub)'],
55
+ ['registry', 'Show active install script URL (from opm config)'],
56
+ ['registry set <url|preset>', 'Set active install script URL (writes opm config)'],
57
+ ['registry presets', 'List available mirror presets'],
58
+ ['install', 'Install Homebrew (curl installer from active registry)'],
58
59
  ['upgrade', 'Update Homebrew (brew update)'],
59
60
  ['uninstall', 'Remove Homebrew (curl uninstaller from GitHub)'],
60
61
  ['dir', 'Show Homebrew install directory'],
@@ -63,6 +64,9 @@ export class HomebrewGroup extends ToolManagerGroup {
63
64
  examples: [
64
65
  'opm homebrew version',
65
66
  'opm homebrew installed',
67
+ 'opm homebrew registry',
68
+ 'opm homebrew registry set official',
69
+ 'opm homebrew registry presets',
66
70
  'opm homebrew install',
67
71
  'opm homebrew upgrade',
68
72
  'opm homebrew dir',
@@ -35,9 +35,13 @@ export class NvmGroup extends ToolManagerGroup {
35
35
  getPath: () => getNvmDir(),
36
36
  versionCmd: () => `source "${path.join(getNvmDir(), 'nvm.sh')}" && nvm --version`,
37
37
  parseVersion: (out) => out.trim(),
38
- install: () => installNvm(),
38
+ install: (proxy) => installNvm(proxy),
39
39
  upgrade: () => {
40
- execSync(`cd "${getNvmDir()}" && git remote set-url origin "${getActiveRegistry('nvm')}" && git pull --tags`, {
40
+ const nvmDir = getNvmDir()
41
+ if (!fs.existsSync(path.join(nvmDir, '.git'))) {
42
+ 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.`)
43
+ }
44
+ execSync(`cd "${nvmDir}" && git remote set-url origin "${getActiveRegistry('nvm')}" && git pull --tags`, {
41
45
  stdio: 'inherit',
42
46
  shell: shell.resolveBash(),
43
47
  })
@@ -0,0 +1,461 @@
1
+ /**
2
+ * ping 命令组 — 验证主机、代理或 registry 的可访问性
3
+ *
4
+ * 用法:
5
+ * opm ping <host> Ping 指定主机/IP/URL
6
+ * opm ping proxy Ping 当前 active 代理地址的主机
7
+ * opm ping registry Ping 所有工具 registry active 地址的主机
8
+ *
9
+ * 自动检测可用工具(ping > curl > wget),无工具时提示并退出。
10
+ * 默认输出精简数组 [{tool, address, proxy, reachable}];-d/--detail 输出完整 JSON。
11
+ */
12
+
13
+ import { spawn } from 'node:child_process'
14
+ import { Shell } from '@wwkit/shared'
15
+ import { parseFlags } from '../helpers/args.js'
16
+ import { output } from '../../formatter.js'
17
+ import { getActiveProxy, getActiveRegistry, getConfig } from '../../config.js'
18
+
19
+ const shell = new Shell()
20
+
21
+ /**
22
+ * 从 URL 或 deb 行中提取主机名和端口(用于 ping 目标)
23
+ * @param {string} url
24
+ * @returns {{ host: string, port: string }}
25
+ */
26
+ function extractHostPort(url) {
27
+ let cleaned = url
28
+ const debMatch = url.match(/^deb\s+(https?:\/\/\S+)/)
29
+ if (debMatch) cleaned = debMatch[1]
30
+ try {
31
+ const u = new URL(cleaned)
32
+ return { host: u.hostname, port: u.port || (u.protocol === 'https:' ? '443' : '80') }
33
+ } catch {
34
+ const m = cleaned.match(/^https?:\/\/([^:/]+)(?::(\d+))?/)
35
+ if (m) return { host: m[1], port: m[2] || '80' }
36
+ return { host: '', port: '' }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 判断地址是否为 URL(含协议前缀)
42
+ * @param {string} addr
43
+ * @returns {boolean}
44
+ */
45
+ function isUrl(addr) {
46
+ return /^https?:\/\//i.test(addr)
47
+ }
48
+
49
+ /**
50
+ * 获取所有含 registry 配置的工具名列表
51
+ * @returns {string[]}
52
+ */
53
+ function getRegistryTools() {
54
+ const config = getConfig()
55
+ return Object.keys(config).filter((k) => config[k] && typeof config[k] === 'object' && config[k].registry)
56
+ }
57
+
58
+ /**
59
+ * 从完整结果中提取精简版(仅 tool, address, proxy, reachable)
60
+ * @param {object} item
61
+ * @returns {{tool: string, address: string, proxy: string, reachable: boolean}}
62
+ */
63
+ function compact(item) {
64
+ return { tool: item.tool, address: item.address, proxy: item.proxy, reachable: item.reachable }
65
+ }
66
+
67
+ /**
68
+ * 用 ICMP ping 检查主机可访问性
69
+ * @param {string} host - 主机名或 IP
70
+ * @param {number} count - 发送包数量
71
+ * @returns {Promise<object>}
72
+ */
73
+ function checkByPing(host, count) {
74
+ return new Promise((resolve) => {
75
+ const isWin = shell.isWindows
76
+ const args = isWin
77
+ ? ['-n', String(count), '-w', '2000', host]
78
+ : ['-c', String(count), '-W', '2', host]
79
+
80
+ const child = spawn('ping', args, { stdio: ['pipe', 'pipe', 'pipe'] })
81
+ let stdout = ''
82
+ let stderr = ''
83
+ child.stdout.on('data', (d) => { stdout += d })
84
+ child.stderr.on('data', (d) => { stderr += d })
85
+ child.on('close', () => {
86
+ const raw = stdout + stderr
87
+ let received = 0
88
+ let rttMin = ''
89
+ let rttAvg = ''
90
+ let rttMax = ''
91
+
92
+ const recvMatch = raw.match(/(\d+)\s+packets?\s+received/)
93
+ if (recvMatch) received = parseInt(recvMatch[1], 10)
94
+
95
+ if (!isWin) {
96
+ const rttMatch = raw.match(/rtt min\/avg\/max(?:\/mdev)?\s*=\s*([\d.]+)\/([\d.]+)\/([\d.]+)/)
97
+ if (rttMatch) {
98
+ rttMin = rttMatch[1]
99
+ rttAvg = rttMatch[2]
100
+ rttMax = rttMatch[3]
101
+ }
102
+ } else {
103
+ const msMatch = raw.match(/Minimum\s*=\s*(\d+)ms.*?Maximum\s*=\s*(\d+)ms.*?Average\s*=\s*(\d+)ms/s)
104
+ if (msMatch) {
105
+ rttMin = msMatch[1]
106
+ rttAvg = msMatch[3]
107
+ rttMax = msMatch[2]
108
+ }
109
+ }
110
+
111
+ resolve({
112
+ reachable: received > 0,
113
+ sent: count,
114
+ received,
115
+ loss: count - received,
116
+ lossPercent: Math.round(((count - received) / count) * 100),
117
+ rttMin,
118
+ rttAvg,
119
+ rttMax,
120
+ raw: raw.trim(),
121
+ })
122
+ })
123
+ child.on('error', () => {
124
+ resolve({
125
+ reachable: false, sent: count, received: 0, loss: count,
126
+ lossPercent: 100, rttMin: '', rttAvg: '', rttMax: '', raw: 'ping command failed to execute',
127
+ })
128
+ })
129
+ })
130
+ }
131
+
132
+ /**
133
+ * 用 curl 检查 URL 可访问性(HTTP,跳过 SSL 校验,跟随跳转)
134
+ * @param {string} url - 完整 URL
135
+ * @param {string} [proxy] - 代理地址,设置时通过 -x 路由
136
+ * @returns {Promise<object>}
137
+ */
138
+ function checkByCurl(url, proxy) {
139
+ return new Promise((resolve) => {
140
+ const args = ['-k', '-L', '-s', '-o', '/dev/null', '-w', '%{http_code} %{time_total}', '--connect-timeout', '2', '--max-time', '5']
141
+ if (proxy) args.push('-x', proxy)
142
+ args.push(url)
143
+ const child = spawn('curl', args, { stdio: ['pipe', 'pipe', 'pipe'] })
144
+ let stdout = ''
145
+ let stderr = ''
146
+ child.stdout.on('data', (d) => { stdout += d })
147
+ child.stderr.on('data', (d) => { stderr += d })
148
+ child.on('close', (code) => {
149
+ const parts = stdout.trim().split(/\s+/)
150
+ const httpCode = parseInt(parts[0], 10) || 0
151
+ const timeTotal = parseFloat(parts[1]) || 0
152
+ const proxyAuthRequired = httpCode === 407
153
+ const allowlist = [404]
154
+ const reachable = (httpCode >= 200 && httpCode < 400) || allowlist.includes(httpCode)
155
+ const rttMs = reachable ? String(Math.round(timeTotal * 1000)) : ''
156
+ resolve({
157
+ reachable,
158
+ proxyAuthRequired,
159
+ sent: 1,
160
+ received: reachable ? 1 : 0,
161
+ loss: reachable ? 0 : 1,
162
+ lossPercent: reachable ? 0 : 100,
163
+ rttMin: rttMs,
164
+ rttAvg: rttMs,
165
+ rttMax: rttMs,
166
+ raw: `HTTP ${httpCode} (${timeTotal}s)`,
167
+ })
168
+ })
169
+ child.on('error', () => {
170
+ resolve({
171
+ reachable: false, sent: 1, received: 0, loss: 1,
172
+ lossPercent: 100, rttMin: '', rttAvg: '', rttMax: '', raw: 'curl failed to execute',
173
+ })
174
+ })
175
+ })
176
+ }
177
+
178
+ /**
179
+ * 用 wget --spider 检查 URL 可访问性
180
+ * @param {string} url - 完整 URL
181
+ * @param {string} [proxy] - 代理地址,设置时通过 -e 路由
182
+ * @returns {Promise<object>}
183
+ */
184
+ function checkByWget(url, proxy) {
185
+ return new Promise((resolve) => {
186
+ const args = ['-q', '--spider', '--timeout=5']
187
+ if (proxy) {
188
+ args.push('-e', `use_proxy=yes`, '-e', `https_proxy=${proxy}`, '-e', `http_proxy=${proxy}`)
189
+ }
190
+ args.push(url)
191
+ const child = spawn('wget', args, { stdio: ['pipe', 'pipe', 'pipe'] })
192
+ let stdout = ''
193
+ let stderr = ''
194
+ child.stdout.on('data', (d) => { stdout += d })
195
+ child.stderr.on('data', (d) => { stderr += d })
196
+ child.on('close', (code) => {
197
+ const reachable = code === 0
198
+ resolve({
199
+ reachable,
200
+ sent: 1,
201
+ received: reachable ? 1 : 0,
202
+ loss: reachable ? 0 : 1,
203
+ lossPercent: reachable ? 0 : 100,
204
+ rttMin: '',
205
+ rttAvg: '',
206
+ rttMax: '',
207
+ raw: stderr.trim() || (reachable ? 'OK' : 'Failed'),
208
+ })
209
+ })
210
+ child.on('error', () => {
211
+ resolve({
212
+ reachable: false, sent: 1, received: 0, loss: 1,
213
+ lossPercent: 100, rttMin: '', rttAvg: '', rttMax: '', raw: 'wget failed to execute',
214
+ })
215
+ })
216
+ })
217
+ }
218
+
219
+ /**
220
+ * 统一检查入口:根据可用工具选择检查方式
221
+ * @param {string} address - 完整地址(URL 或纯主机名/IP)
222
+ * @param {string} tool - 检测到的工具 ('ping' | 'curl' | 'wget')
223
+ * @param {number} count - ping 包数量(仅 ping 用)
224
+ * @param {string} [proxy] - 代理地址,设置时通过代理路由请求(仅 curl/wget)
225
+ * @returns {Promise<object>}
226
+ */
227
+ async function checkReachability(address, tool, count, proxy) {
228
+ if (tool === 'ping') {
229
+ let host = address
230
+ if (isUrl(address)) {
231
+ const extracted = extractHostPort(address)
232
+ host = extracted.host
233
+ }
234
+ if (!host) {
235
+ return { reachable: false, sent: count, received: 0, loss: count, lossPercent: 100, rttMin: '', rttAvg: '', rttMax: '', raw: 'Could not parse host from address' }
236
+ }
237
+ return checkByPing(host, count)
238
+ }
239
+
240
+ let url = address
241
+ const debMatch = address.match(/^deb\s+(https?:\/\/\S+)/)
242
+ if (debMatch) {
243
+ url = debMatch[1]
244
+ } else if (!isUrl(url)) {
245
+ url = `http://${url}`
246
+ }
247
+
248
+ if (tool === 'curl') return checkByCurl(url, proxy)
249
+ if (tool === 'wget') return checkByWget(url, proxy)
250
+ return { reachable: false, sent: 0, received: 0, loss: 0, lossPercent: 0, rttMin: '', rttAvg: '', rttMax: '', raw: 'No tool available' }
251
+ }
252
+
253
+ export class PingGroup {
254
+ constructor() {
255
+ this.name = 'ping'
256
+ this.desc = 'Ping host, active proxy, or all registries to verify accessibility'
257
+ }
258
+
259
+ async run(argv) {
260
+ const parsed = parseFlags(argv)
261
+ const [target] = parsed.positional
262
+
263
+ if (!target || target === '-h' || target === '--help' || target === 'help') {
264
+ this.printHelp()
265
+ return
266
+ }
267
+
268
+ const tool = shell.getNetProbeTool()
269
+ if (!tool) {
270
+ console.error('[opm] No network check tool available (ping/curl/wget). Please install one.')
271
+ process.exit(1)
272
+ }
273
+
274
+ const count = parseInt(parsed.flags.c || parsed.flags.count || '4', 10)
275
+ const n = (isNaN(count) || count < 1) ? 4 : count
276
+ const detail = !!(parsed.flags.d || parsed.flags.detail)
277
+
278
+ let proxy = ''
279
+ if ('p' in parsed.flags || 'proxy' in parsed.flags) {
280
+ let raw = parsed.flags.proxy || parsed.flags.p || ''
281
+ if (raw === 'true') raw = ''
282
+ proxy = raw || getActiveProxy()
283
+ }
284
+
285
+ if (target === 'proxy') {
286
+ return this._pingProxy(n, detail, tool)
287
+ }
288
+
289
+ if (target === 'registry') {
290
+ return this._pingRegistry(n, detail, proxy, tool)
291
+ }
292
+
293
+ if (target === 'source') {
294
+ return this._pingSource()
295
+ }
296
+
297
+ if (proxy) {
298
+ const usable = await this._checkProxyUsable(proxy, tool, n)
299
+ if (!usable.ok) {
300
+ console.error(`[opm] Proxy not usable: ${usable.reason}`)
301
+ process.exit(1)
302
+ }
303
+ }
304
+
305
+ const result = await checkReachability(target, tool, n, proxy)
306
+ const item = { tool: 'host', address: target, proxy, ...result }
307
+ output(detail ? [item] : [compact(item)])
308
+ }
309
+
310
+ /**
311
+ * 检查代理是否可用:通过代理访问 npm active registry 测试
312
+ * @returns {{ ok: boolean, reason: string }}
313
+ */
314
+ async _checkProxyUsable(proxyUrl, tool, count) {
315
+ const testUrl = getActiveRegistry('npm')
316
+ const result = await checkReachability(testUrl, tool, count, proxyUrl)
317
+ if (result.proxyAuthRequired) {
318
+ return { ok: false, reason: 'authentication required (HTTP 407)' }
319
+ }
320
+ if (!result.reachable) {
321
+ return { ok: false, reason: result.raw }
322
+ }
323
+ return { ok: true, reason: 'OK' }
324
+ }
325
+
326
+ async _pingProxy(count, detail, tool) {
327
+ const proxyUrl = getActiveProxy()
328
+ if (!proxyUrl) {
329
+ const item = { tool: 'proxy', address: '(no active proxy)', proxy: '', reachable: false, sent: 0, received: 0, loss: 0, lossPercent: 0, rttMin: '', rttAvg: '', rttMax: '', raw: 'No active proxy configured (proxy.active is empty)' }
330
+ output(detail ? [item] : [compact(item)])
331
+ return
332
+ }
333
+
334
+ const usable = await this._checkProxyUsable(proxyUrl, tool, count)
335
+ const item = {
336
+ tool: 'proxy',
337
+ address: proxyUrl,
338
+ proxy: '',
339
+ reachable: usable.ok,
340
+ sent: 1,
341
+ received: usable.ok ? 1 : 0,
342
+ loss: usable.ok ? 0 : 1,
343
+ lossPercent: usable.ok ? 0 : 100,
344
+ rttMin: '',
345
+ rttAvg: '',
346
+ rttMax: '',
347
+ raw: usable.reason,
348
+ }
349
+ output(detail ? [item] : [compact(item)])
350
+ }
351
+
352
+ async _pingRegistry(count, detail, proxy, tool) {
353
+ const tools = getRegistryTools()
354
+
355
+ if (proxy) {
356
+ const usable = await this._checkProxyUsable(proxy, tool, 1)
357
+ if (!usable.ok) {
358
+ console.error(`[opm] Proxy not usable: ${usable.reason}`)
359
+ process.exit(1)
360
+ }
361
+ }
362
+
363
+ const items = await Promise.all(tools.map(async (t) => {
364
+ const url = getActiveRegistry(t)
365
+ if (!url) {
366
+ return { tool: t, address: '', proxy, reachable: false, sent: 0, received: 0, loss: 0, lossPercent: 0, rttMin: '', rttAvg: '', rttMax: '', raw: 'No active registry' }
367
+ }
368
+ const result = await checkReachability(url, tool, count, proxy)
369
+ return { tool: t, address: url, proxy, ...result }
370
+ }))
371
+
372
+ output(detail ? items : items.map(compact))
373
+ }
374
+
375
+ async _pingSource() {
376
+ const url = getActiveRegistry('npm')
377
+ const activeProxy = getActiveProxy()
378
+ const tool = shell.getNetProbeTool()
379
+
380
+ if (!tool) {
381
+ console.error('[opm] No network check tool available (ping/curl/wget).')
382
+ return
383
+ }
384
+
385
+ const cleanProxy = activeProxy.replace(/^(https?:\/\/)[^@]*@/, '$1')
386
+ const authProxy = cleanProxy.replace(/^(https?:\/\/)/, '$1user:password@')
387
+
388
+ if (tool === 'curl') {
389
+ console.log(`# tool: ${tool}
390
+ # Windows (PowerShell)
391
+ curl.exe -k --max-time 10 ${url}
392
+ curl.exe -k --max-time 10 --proxy ${cleanProxy} ${url}
393
+ curl.exe -k --max-time 10 --proxy ${authProxy} ${url}
394
+
395
+ # Linux / macOS
396
+ curl -k --max-time 10 ${url}
397
+ curl -k --max-time 10 --proxy ${cleanProxy} ${url}
398
+ curl -k --max-time 10 --proxy ${authProxy} ${url}`)
399
+ return
400
+ }
401
+
402
+ if (tool === 'wget') {
403
+ console.log(`# tool: ${tool}
404
+ # Windows (PowerShell)
405
+ wget.exe -q --spider --timeout=10 ${url}
406
+ wget.exe -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${cleanProxy} ${url}
407
+ wget.exe -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${authProxy} ${url}
408
+
409
+ # Linux / macOS
410
+ wget -q --spider --timeout=10 ${url}
411
+ wget -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${cleanProxy} ${url}
412
+ wget -q --spider --timeout=10 -e use_proxy=yes -e https_proxy=${authProxy} ${url}`)
413
+ return
414
+ }
415
+
416
+ if (tool === 'ping') {
417
+ const { host } = extractHostPort(url)
418
+ console.log(`# tool: ${tool}
419
+ # Windows
420
+ ping -n 4 -w 10000 ${host}
421
+
422
+ # Linux / macOS
423
+ ping -c 4 -W 10 ${host}`)
424
+ }
425
+ }
426
+
427
+ printHelp() {
428
+ console.log(`
429
+ Usage: opm ping <host|proxy|registry|source> [options]
430
+
431
+ Actions:
432
+ ping <host> Check a host, IP, or URL for accessibility
433
+ ping proxy Check the active proxy address (from config)
434
+ ping registry Check all tools' active registry addresses in parallel
435
+ ping source Print example system commands (npm active registry)
436
+
437
+ Options:
438
+ -c, --count <n> Number of packets to send (ping only; default: 4)
439
+ -p, --proxy <url> Record proxy used for the check (informational)
440
+ -d, --detail Output full JSON details (default: compact [{tool, address, proxy, reachable}])
441
+ -h, --help Show this help
442
+
443
+ Tool detection: ping > curl > wget (auto-detected)
444
+
445
+ Output:
446
+ default (no -d) [{ "tool": "npm", "address": "https://...", "proxy": "", "reachable": true }]
447
+ -d / --detail [{ "tool": "npm", "address": "https://...", "proxy": "", "reachable": true, "sent": 1, "received": 1, "loss": 0, "lossPercent": 0, "rttMin": "...", "rttAvg": "...", "rttMax": "...", "raw": "..." }]
448
+
449
+ Examples:
450
+ opm ping 8.8.8.8
451
+ opm ping https://mirrors.tools.huawei.com/npm/
452
+ opm ping proxy
453
+ opm ping registry
454
+ opm ping source
455
+ opm ping 8.8.8.8 -c 2
456
+ opm ping proxy -d
457
+ opm ping registry -d
458
+ opm ping sample -d
459
+ `)
460
+ }
461
+ }