@wwkit/opm 1.0.1 → 1.0.3

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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * docker 命令组 — 管理 Docker 自身与 daemon
3
+ *
4
+ * 继承 ToolManagerGroup 复用 version/installed/registry/install/upgrade/uninstall/dir,
5
+ * 额外增加 status/start/stop/restart/open。
6
+ * 跨平台:Linux 用便捷脚本(URL 由 docker.registry.active 提供),macOS 用 brew cask,Windows 走 WSL2。
7
+ */
8
+
9
+ import { ToolManagerGroup } from './version-manager.js'
10
+ import { parseFlags } from '../helpers/args.js'
11
+ import { output } from '../../formatter.js'
12
+ import {
13
+ isDockerInstalled,
14
+ getDockerVersion,
15
+ installDocker,
16
+ upgradeDocker,
17
+ uninstallDocker,
18
+ getDockerDir,
19
+ getDockerStatus,
20
+ startDocker,
21
+ stopDocker,
22
+ restartDocker,
23
+ openDocker,
24
+ } from '../../managers/docker.js'
25
+
26
+ export class DockerGroup extends ToolManagerGroup {
27
+ constructor() {
28
+ super({
29
+ name: 'docker',
30
+ desc: 'Docker manager (install/upgrade/start/stop/open)',
31
+ isInstalled: () => isDockerInstalled(),
32
+ getPath: () => getDockerDir(),
33
+ versionCmd: () => 'docker --version',
34
+ parseVersion: (out) => {
35
+ const m = out.match(/Docker version\s+(\S+)/)
36
+ return m ? m[1] : out.trim()
37
+ },
38
+ install: (proxy) => installDocker(proxy),
39
+ upgrade: () => upgradeDocker(),
40
+ uninstall: () => uninstallDocker(),
41
+ usage: 'opm docker <action> [options]',
42
+ actions: [
43
+ ['version', 'Show Docker version'],
44
+ ['installed', 'Check if Docker is installed'],
45
+ ['registry', 'Show active Docker install script URL (from opm config)'],
46
+ ['registry set <url|preset>', 'Set active Docker install script URL (writes opm config)'],
47
+ ['registry presets', 'List available mirror presets'],
48
+ ['install', 'Install Docker (Linux: script from active registry, macOS: brew cask, Windows: WSL2)'],
49
+ ['upgrade', 'Upgrade Docker'],
50
+ ['uninstall', 'Uninstall Docker'],
51
+ ['dir', 'Show Docker data directory'],
52
+ ['status', 'Show Docker daemon status'],
53
+ ['start', 'Start Docker daemon'],
54
+ ['stop', 'Stop Docker daemon'],
55
+ ['restart', 'Restart Docker daemon'],
56
+ ['open', 'Open terminal with Docker ready'],
57
+ ['help', 'Show this help'],
58
+ ],
59
+ examples: [
60
+ 'opm docker version',
61
+ 'opm docker installed',
62
+ 'opm docker registry',
63
+ 'opm docker registry set official',
64
+ 'opm docker registry presets',
65
+ 'opm docker install',
66
+ 'opm docker upgrade',
67
+ 'opm docker status',
68
+ 'opm docker start',
69
+ 'opm docker stop',
70
+ 'opm docker restart',
71
+ 'opm docker open',
72
+ 'opm docker dir',
73
+ 'opm docker uninstall',
74
+ ],
75
+ })
76
+ }
77
+
78
+ async run(argv) {
79
+ const [action, ...rest] = argv
80
+
81
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
82
+ this.printHelp()
83
+ return
84
+ }
85
+
86
+ switch (action) {
87
+ case 'status':
88
+ return this._status()
89
+ case 'start':
90
+ return this._start()
91
+ case 'stop':
92
+ return this._stop()
93
+ case 'restart':
94
+ return this._restart()
95
+ case 'open':
96
+ return this._open()
97
+ }
98
+
99
+ return super.run(argv)
100
+ }
101
+
102
+ _status() {
103
+ output(getDockerStatus())
104
+ }
105
+
106
+ _start() {
107
+ output(startDocker())
108
+ }
109
+
110
+ _stop() {
111
+ output(stopDocker())
112
+ }
113
+
114
+ _restart() {
115
+ output(restartDocker())
116
+ }
117
+
118
+ _open() {
119
+ output(openDocker())
120
+ }
121
+ }
@@ -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
+ }