@wwkit/opm 1.0.14 → 1.0.16

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,451 @@
1
+ /**
2
+ * env 命令组 — 跨平台环境变量便捷管理
3
+ *
4
+ * 用法:
5
+ * opm env list [--all] 列出 opm 管理的环境变量(含配置文件路径);--all 追加 process.env 全量
6
+ * opm env get <key> 根据 key 取值(先查 opm 管理的配置源,回退 process.env)
7
+ * opm env set <key> <value> 设置值(替换已存在的)
8
+ * opm env help 通用介绍
9
+ *
10
+ * 跨平台策略(复用 @wwkit/shared 的 Shell):
11
+ * Unix: 读写 shell profile(bash/zsh/fish),用 `# >>> wwkit NAME >>>` 标记块管理
12
+ * Windows: setx 写 user 级注册表;list 用 reg query 读取;用 manifest 文件
13
+ * (~/.config/opm/env-managed.json)识别哪些 user 级变量由 opm 管理
14
+ * (setx 不留标记,无法从注册表本身区分)
15
+ */
16
+
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { Shell } from '@wwkit/shared'
21
+ import { parseFlags } from '../helpers/args.js'
22
+ import { output } from '../../formatter.js'
23
+ import { getUserConfigDir } from '../../config.js'
24
+
25
+ /** Windows 注册表环境变量键路径 */
26
+ const WIN_USER_KEY = 'HKCU\\Environment'
27
+
28
+ /** manifest 文件名(Windows 记录 opm 管理的 user 级变量 key) */
29
+ const MANIFEST_FILE = 'env-managed.json'
30
+
31
+ /**
32
+ * 反转 Shell.setEnvVar 的转义([$`\\"] → \\$&)
33
+ * @param {string} s
34
+ * @returns {string}
35
+ */
36
+ function unescapeEnvValue(s) {
37
+ return s.replace(/\\([$`\\"])/g, '$1')
38
+ }
39
+
40
+ /**
41
+ * 从一行 export/set 语句中提取变量值
42
+ * @param {string} line
43
+ * @param {string} key
44
+ * @returns {string|null}
45
+ */
46
+ function parseEnvLine(line, key) {
47
+ const exportQuoted = line.match(new RegExp(`^export\\s+${key}="(.*)"\\s*$`))
48
+ if (exportQuoted) return unescapeEnvValue(exportQuoted[1])
49
+ const fishQuoted = line.match(new RegExp(`^set -gx\\s+${key}\\s+"(.*)"\\s*$`))
50
+ if (fishQuoted) return unescapeEnvValue(fishQuoted[1])
51
+ const exportBare = line.match(new RegExp(`^export\\s+${key}=(\\S+)\\s*$`))
52
+ if (exportBare) return exportBare[1]
53
+ const fishBare = line.match(new RegExp(`^set -gx\\s+${key}\\s+(\\S+)\\s*$`))
54
+ if (fishBare) return fishBare[1]
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * 解析 profile 文件内容中的 wwkit 标记块,提取 opm 管理的环境变量
60
+ * @param {string} content
61
+ * @returns {{ key: string, value: string }[]}
62
+ */
63
+ export function parseEnvBlocks(content) {
64
+ if (!content) return []
65
+ const lines = content.split('\n')
66
+ const result = []
67
+ let i = 0
68
+ while (i < lines.length) {
69
+ const marker = lines[i].match(/^# >>> wwkit (\S+) >>>/)
70
+ if (marker) {
71
+ const key = marker[1]
72
+ // 块体:标记行之后到下一个 wwkit 标记或 EOF 之间的非空非注释行
73
+ let j = i + 1
74
+ while (j < lines.length && lines[j].trim() === '') j++
75
+ if (j < lines.length && !lines[j].startsWith('# >>> wwkit')) {
76
+ const value = parseEnvLine(lines[j], key)
77
+ if (value !== null) result.push({ key, value })
78
+ }
79
+ i = j + 1
80
+ } else {
81
+ i++
82
+ }
83
+ }
84
+ return result
85
+ }
86
+
87
+ /**
88
+ * 解析 Windows `reg query` 输出,提取环境变量条目
89
+ * @param {string} output
90
+ * @returns {{ key: string, type: string, value: string }[]}
91
+ */
92
+ export function parseRegOutput(output) {
93
+ if (!output) return []
94
+ const result = []
95
+ for (const line of output.split(/\r?\n/)) {
96
+ const m = line.match(/^\s+(\S+)\s+(REG_\S+)\s+(.*)$/)
97
+ if (m) result.push({ key: m[1], type: m[2], value: m[3] })
98
+ }
99
+ return result
100
+ }
101
+
102
+ /**
103
+ * 校验环境变量名合法性(字母/下划线开头,后接字母数字下划线)
104
+ * @param {string} key
105
+ * @returns {boolean}
106
+ */
107
+ export function isValidEnvKey(key) {
108
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
109
+ }
110
+
111
+ export class EnvGroup {
112
+ /**
113
+ * @param {{ shell?: Shell, configDir?: string }} [opts] - shell/configDir 注入用于测试
114
+ */
115
+ constructor(opts = {}) {
116
+ this.name = 'env'
117
+ this.desc = 'Manage environment variables (list/get/set) across platforms'
118
+ this.shell = opts.shell || new Shell()
119
+ this.configDir = opts.configDir || ''
120
+ }
121
+
122
+ /** @returns {string} manifest 文件绝对路径 */
123
+ _manifestPath() {
124
+ const dir = this.configDir || getUserConfigDir()
125
+ return path.join(dir, MANIFEST_FILE)
126
+ }
127
+
128
+ /**
129
+ * 读取 manifest 中的 opm 管理 key 集合(Windows 用)
130
+ * @returns {Set<string>}
131
+ */
132
+ _readManifest() {
133
+ try {
134
+ const raw = fs.readFileSync(this._manifestPath(), 'utf8')
135
+ const data = JSON.parse(raw)
136
+ return new Set(Array.isArray(data.keys) ? data.keys : [])
137
+ } catch {
138
+ return new Set()
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 将 key 登记到 manifest(Windows set 时调用)
144
+ * @param {string} key
145
+ */
146
+ _addManagedKey(key) {
147
+ const keys = this._readManifest()
148
+ keys.add(key)
149
+ const dir = this.configDir || getUserConfigDir()
150
+ fs.mkdirSync(dir, { recursive: true })
151
+ fs.writeFileSync(this._manifestPath(), JSON.stringify({ keys: [...keys] }, null, 2), 'utf8')
152
+ }
153
+
154
+ /**
155
+ * 列出 opm 管理的环境变量(默认 list 的数据源)
156
+ * Unix: 解析所有 profile 的 wwkit 标记块
157
+ * Windows: 读 manifest → 查 user 级注册表当前值
158
+ * @returns {object[]}
159
+ */
160
+ _managedItems() {
161
+ return this.shell.isWindows ? this._listWindowsManaged() : this._listUnix()
162
+ }
163
+
164
+ _listUnix() {
165
+ const items = []
166
+ for (const { path: profilePath, syntax } of this.shell.getProfiles()) {
167
+ let content = ''
168
+ try {
169
+ content = fs.readFileSync(profilePath, 'utf8')
170
+ } catch {}
171
+ for (const { key, value } of parseEnvBlocks(content)) {
172
+ items.push({ key, value, scope: 'user', source: profilePath, syntax })
173
+ }
174
+ }
175
+ return items
176
+ }
177
+
178
+ _listWindowsManaged() {
179
+ const managedKeys = this._readManifest()
180
+ if (managedKeys.size === 0) return []
181
+ const userVars = this._regQuery(WIN_USER_KEY)
182
+ const byKey = new Map(userVars.map((v) => [v.key, v]))
183
+ const items = []
184
+ for (const key of managedKeys) {
185
+ const v = byKey.get(key)
186
+ if (v) {
187
+ items.push({ key, value: v.value, scope: 'user', source: 'registry (user)', type: v.type })
188
+ } else {
189
+ // manifest 登记过但注册表中已不存在(外部删除)——标注缺失
190
+ items.push({ key, value: null, scope: 'user', source: 'registry (user)', missing: true })
191
+ }
192
+ }
193
+ return items
194
+ }
195
+
196
+ _regQuery(keyPath) {
197
+ try {
198
+ const result = spawnSync('reg', ['query', keyPath], {
199
+ encoding: 'utf8',
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ })
202
+ if (result.status !== 0) return []
203
+ return parseRegOutput(result.stdout || '')
204
+ } catch {
205
+ return []
206
+ }
207
+ }
208
+
209
+ /**
210
+ * --all:opm 管理的 ∪ process.env 全量,每条标注 managed
211
+ * @returns {object[]}
212
+ */
213
+ _listAll() {
214
+ const managedItems = this._managedItems()
215
+ const seen = new Set()
216
+ const items = []
217
+ for (const m of managedItems) {
218
+ items.push({ key: m.key, value: m.value, managed: true, source: m.source, scope: m.scope })
219
+ seen.add(m.key)
220
+ }
221
+ for (const [key, value] of Object.entries(process.env)) {
222
+ if (!seen.has(key)) {
223
+ items.push({ key, value, managed: false, source: 'process.env' })
224
+ seen.add(key)
225
+ }
226
+ }
227
+ return items
228
+ }
229
+
230
+ async run(argv) {
231
+ const [action, ...rest] = argv
232
+
233
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
234
+ this.printHelp()
235
+ return
236
+ }
237
+
238
+ const parsed = parseFlags(rest)
239
+
240
+ switch (action) {
241
+ case 'list':
242
+ return this._list(parsed)
243
+ case 'get':
244
+ return this._get(parsed)
245
+ case 'set':
246
+ return this._set(parsed)
247
+ case 'search':
248
+ return this._search(parsed)
249
+ default:
250
+ console.error(`Unknown action: ${action}`)
251
+ this.printHelp()
252
+ process.exit(1)
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 是否输出 JSON(--json / -j)
258
+ * @param {{ flags: object, positional: string[] }} parsed
259
+ * @returns {boolean}
260
+ */
261
+ _useJson(parsed) {
262
+ return parsed.flags.json === 'true' || parsed.flags.j === 'true'
263
+ }
264
+
265
+ /**
266
+ * 统一输出:--json 走 JSON,否则走 plain 格式化函数
267
+ * @param {{ flags: object, positional: string[] }} parsed
268
+ * @param {*} data - JSON 输出数据
269
+ * @param {() => string} plain - 简化字符串输出函数
270
+ */
271
+ _out(parsed, data, plain) {
272
+ if (this._useJson(parsed)) {
273
+ output(data)
274
+ } else {
275
+ console.log(plain())
276
+ }
277
+ }
278
+
279
+ /**
280
+ * list 简化格式:按 source 分组,每组标题为来源,下面 key = value 行
281
+ * @param {object[]} items
282
+ * @returns {string}
283
+ */
284
+ _formatListPlain(items) {
285
+ const groups = []
286
+ const groupMap = new Map()
287
+ for (const item of items) {
288
+ const source = item.source || 'unknown'
289
+ if (!groupMap.has(source)) {
290
+ const g = { source, entries: [] }
291
+ groupMap.set(source, g)
292
+ groups.push(g)
293
+ }
294
+ groupMap.get(source).entries.push(item)
295
+ }
296
+ return groups
297
+ .map((g) => [
298
+ g.source,
299
+ ...g.entries.map((e) => `${e.key} = ${e.value === null || e.value === undefined ? '(not set)' : e.value}`),
300
+ ].join('\n'))
301
+ .join('\n\n')
302
+ }
303
+
304
+ /**
305
+ * 列出环境变量
306
+ * 默认: 只显示 opm 管理的(Unix 标记块 / Windows manifest)
307
+ * --all: 追加 process.env 全量,每条标注 managed: true/false
308
+ * 输出: 默认简化文本(按 source 分组),--json 输出 JSON
309
+ * @param {{ flags: object, positional: string[] }} parsed
310
+ */
311
+ async _list(parsed) {
312
+ const all = parsed.flags.all === 'true' || parsed.flags.a === 'true'
313
+ const items = all ? this._listAll() : this._managedItems()
314
+ this._out(parsed, items, () => this._formatListPlain(items))
315
+ }
316
+
317
+ /**
318
+ * 根据 key 取值:先查 opm 管理的配置源,回退 process.env
319
+ * 输出: 默认 `key = value`(未找到 `key = (not found)`),--json 输出 JSON
320
+ * @param {{ flags: object, positional: string[] }} parsed
321
+ */
322
+ async _get(parsed) {
323
+ const key = parsed.positional[0] || parsed.flags.k || parsed.flags.key || ''
324
+ if (!key) {
325
+ console.error('Usage: opm env get <key>')
326
+ console.error('Missing required <key>')
327
+ process.exit(1)
328
+ }
329
+
330
+ const configured = this._managedItems()
331
+ const hit = configured.find((e) => e.key === key)
332
+ if (hit) {
333
+ this._out(parsed, { key, value: hit.value, found: true, source: hit.source, scope: hit.scope }, () => `${key} = ${hit.value}`)
334
+ return
335
+ }
336
+
337
+ if (process.env[key] !== undefined) {
338
+ const value = process.env[key]
339
+ this._out(parsed, { key, value, found: true, source: 'process.env' }, () => `${key} = ${value}`)
340
+ return
341
+ }
342
+
343
+ this._out(parsed, { key, value: null, found: false }, () => `${key} = (not found)`)
344
+ }
345
+
346
+ /**
347
+ * 设置环境变量(替换已存在的)
348
+ * Unix: 写入 shell profile 标记块;Windows: setx 写 user 级注册表 + manifest 登记
349
+ * @param {{ flags: object, positional: string[] }} parsed
350
+ */
351
+ async _set(parsed) {
352
+ const key = parsed.positional[0] || parsed.flags.k || parsed.flags.key || ''
353
+ const value = parsed.positional[1] ?? parsed.flags.v ?? parsed.flags.value ?? ''
354
+
355
+ if (!key) {
356
+ console.error('Usage: opm env set <key> <value>')
357
+ console.error('Missing required <key>')
358
+ process.exit(1)
359
+ }
360
+ if (!isValidEnvKey(key)) {
361
+ console.error(`Invalid env key: "${key}" (must start with letter/underscore, only letters/digits/underscore)`)
362
+ process.exit(1)
363
+ }
364
+ if (parsed.positional.length < 2 && !(parsed.flags.v || parsed.flags.value)) {
365
+ console.error('Usage: opm env set <key> <value>')
366
+ console.error('Missing required <value>')
367
+ process.exit(1)
368
+ }
369
+
370
+ const written = this.shell.setEnvVar(key, value)
371
+ if (this.shell.isWindows) {
372
+ this._addManagedKey(key)
373
+ }
374
+ const source = this.shell.isWindows
375
+ ? 'registry (user)'
376
+ : this.shell.getProfiles().map((p) => p.path)
377
+ const result = { key, value, written, source }
378
+ this._out(parsed, result, () => `${key} = ${value}${written ? '' : ' (no change)'}`)
379
+ }
380
+
381
+ /**
382
+ * 按关键词搜索环境变量(匹配 key 或 value,忽略大小写)
383
+ * 默认: 在 opm 管理的变量中搜索;--all 扩展至 process.env 全量
384
+ * 输出: 默认简化文本(按 source 分组),--json 输出 JSON
385
+ * @param {{ flags: object, positional: string[] }} parsed
386
+ */
387
+ async _search(parsed) {
388
+ const keyword = parsed.positional[0] || parsed.flags.q || parsed.flags.query || ''
389
+ if (!keyword) {
390
+ console.error('Usage: opm env search <keyword>')
391
+ console.error('Missing required <keyword>')
392
+ process.exit(1)
393
+ }
394
+
395
+ const all = parsed.flags.all === 'true' || parsed.flags.a === 'true'
396
+ const source = all ? this._listAll() : this._managedItems()
397
+ const kw = keyword.toLowerCase()
398
+ const matches = source.filter((e) => {
399
+ const k = String(e.key).toLowerCase()
400
+ const v = e.value === null || e.value === undefined ? '' : String(e.value).toLowerCase()
401
+ return k.includes(kw) || v.includes(kw)
402
+ })
403
+ this._out(parsed, matches, () => this._formatListPlain(matches))
404
+ }
405
+
406
+ printHelp() {
407
+ console.log(`
408
+ Usage: opm env <action> [args] [options]
409
+
410
+ Actions:
411
+ list [--all] List opm-managed env vars with their config file path
412
+ (default: only vars set via "opm env set";
413
+ --all appends all process.env vars, each tagged managed: true/false)
414
+ get <key> Get value by key (checks opm-managed sources first, then process.env)
415
+ set <key> <value> Set value, replacing any existing one
416
+ (Unix: shell profile marker block; Windows: setx user-level registry)
417
+ search <keyword> [--all] Search env vars whose key or value contains keyword (case-insensitive)
418
+ (default: opm-managed vars; --all also searches process.env)
419
+ help Show this help
420
+
421
+ Options:
422
+ -k, --key <name> Key (alternative to positional for get/set)
423
+ -v, --value <val> Value (alternative to positional for set)
424
+ -q, --query <kw> Keyword (alternative to positional for search)
425
+ -a, --all Also list all process.env vars (tagged managed: true/false)
426
+ -j, --json Output JSON (default: plain text)
427
+ -h, --help Show this help
428
+
429
+ Output:
430
+ default (no -j) Plain text: list groups by source then "key = value" lines;
431
+ get/set print a single "key = value" line
432
+ -j / --json JSON object/array with full metadata
433
+
434
+ Platform behavior:
435
+ Unix read/write shell profiles (.zshrc/.bashrc/.profile/fish config)
436
+ via "# >>> wwkit NAME >>>" marker blocks
437
+ Windows read/write user-level registry (HKCU\\Environment); writes via setx;
438
+ opm-managed keys tracked in ~/.config/opm/env-managed.json
439
+
440
+ Examples:
441
+ opm env list
442
+ opm env list --all # all process.env vars, tagged managed
443
+ opm env get PATH
444
+ opm env set FOO bar
445
+ opm env set MY_API_KEY secret123
446
+ opm env search api # match key or value containing "api" (case-insensitive)
447
+ opm env search api --all # also search process.env
448
+ opm env help
449
+ `)
450
+ }
451
+ }
@@ -57,7 +57,6 @@ const ACTIONS = {
57
57
  view: { desc: 'Check package in global and project scopes' },
58
58
  info: { desc: 'Show package details' },
59
59
  versions: { desc: 'List all available versions of a package' },
60
- outdated: { desc: 'List outdated packages' },
61
60
  install: { desc: 'Install a package' },
62
61
  uninstall: { desc: 'Uninstall a package' },
63
62
  upgrade: { desc: 'Upgrade a package' },
@@ -99,6 +98,26 @@ export class PackageGroup {
99
98
  await this._dispatch(manager, action, parsed, { global, proxy })
100
99
  }
101
100
 
101
+ /**
102
+ * 打印当前 registry 和 proxy(诊断信息,输出到 stderr)
103
+ * 在涉及网络请求的命令执行前调用,便于连接失败时定位。
104
+ * 输出到 stderr 不污染 JSON stdout。
105
+ * @param {PackageManager} manager
106
+ * @param {string} [proxy]
107
+ * @private
108
+ */
109
+ async _printEndpoint(manager, proxy) {
110
+ try {
111
+ const { registry } = await manager.getRegistry()
112
+ if (registry) {
113
+ console.error(`[opm] registry: ${registry}`)
114
+ }
115
+ } catch {}
116
+ if (proxy) {
117
+ console.error(`[opm] proxy: ${proxy}`)
118
+ }
119
+ }
120
+
102
121
  async _dispatch(manager, action, parsed, opts) {
103
122
  const { positional } = parsed
104
123
 
@@ -152,6 +171,7 @@ export class PackageGroup {
152
171
  console.error(`Usage: opm ${this.name} search <name> [version]`)
153
172
  process.exit(1)
154
173
  }
174
+ await this._printEndpoint(manager, opts.proxy)
155
175
  const result = await manager.searchPackage(name, version, { proxy: opts.proxy })
156
176
  output(result)
157
177
  break
@@ -174,6 +194,7 @@ export class PackageGroup {
174
194
  console.error(`Usage: opm ${this.name} info <name>`)
175
195
  process.exit(1)
176
196
  }
197
+ await this._printEndpoint(manager, opts.proxy)
177
198
  const result = await manager.packageInfo(name, { proxy: opts.proxy })
178
199
  output(result)
179
200
  break
@@ -185,17 +206,12 @@ export class PackageGroup {
185
206
  console.error(`Usage: opm ${this.name} versions <name>`)
186
207
  process.exit(1)
187
208
  }
209
+ await this._printEndpoint(manager, opts.proxy)
188
210
  const result = await manager.packageVersions(name, { proxy: opts.proxy })
189
211
  output(result)
190
212
  break
191
213
  }
192
214
 
193
- case 'outdated': {
194
- const result = await manager.listOutdated({ global: !!opts.global, proxy: opts.proxy })
195
- output(result)
196
- break
197
- }
198
-
199
215
  case 'install': {
200
216
  const name = positional[0]
201
217
  const version = positional[1]
@@ -203,6 +219,7 @@ export class PackageGroup {
203
219
  console.error(`Usage: opm ${this.name} install <name> [version]`)
204
220
  process.exit(1)
205
221
  }
222
+ await this._printEndpoint(manager, opts.proxy)
206
223
  const result = await manager.install(name, version, { global: !!opts.global, proxy: opts.proxy })
207
224
  output(result)
208
225
  break
@@ -225,6 +242,7 @@ export class PackageGroup {
225
242
  console.error(`Usage: opm ${this.name} upgrade <name>`)
226
243
  process.exit(1)
227
244
  }
245
+ await this._printEndpoint(manager, opts.proxy)
228
246
  const result = await manager.upgrade(name, { global: !!opts.global, proxy: opts.proxy })
229
247
  output(result)
230
248
  break
@@ -266,7 +284,6 @@ Actions:
266
284
  view <name> Show package in global and project scopes
267
285
  info <name> Show package details
268
286
  versions <name> List all available versions
269
- outdated [-g] List outdated packages
270
287
  install <name> [version] Install a package
271
288
  uninstall <name> Uninstall a package
272
289
  upgrade <name> Upgrade a package to latest
@@ -276,7 +293,7 @@ Actions:
276
293
 
277
294
  Options:
278
295
  -g, --global Global operation (npm: global node_modules; pip: system Python)
279
- -p, --proxy <url> Proxy address (search/info/versions/outdated/install/upgrade);
296
+ -p, --proxy <url> Proxy address (search/info/versions/install/upgrade);
280
297
  overrides config proxy.active when provided
281
298
  -h, --help Show this help
282
299
 
@@ -287,7 +304,6 @@ Examples:
287
304
  opm ${this.name} view <name>
288
305
  opm ${this.name} info <name>
289
306
  opm ${this.name} versions <name>
290
- opm ${this.name} outdated
291
307
  opm ${this.name} install <name>
292
308
  opm ${this.name} uninstall <name>
293
309
  opm ${this.name} upgrade <name>
@@ -438,12 +454,14 @@ export class RuntimeGroup extends PackageGroup {
438
454
  console.error(`Usage: opm ${this.name} install <version>`)
439
455
  process.exit(1)
440
456
  }
457
+ await this._printEndpoint(manager, opts.proxy)
441
458
  const result = await manager.install(this.name, version, { proxy: opts.proxy })
442
459
  output(result)
443
460
  break
444
461
  }
445
462
 
446
463
  case 'upgrade': {
464
+ await this._printEndpoint(manager, opts.proxy)
447
465
  const result = await manager.upgrade(this.name, { proxy: opts.proxy })
448
466
  output(result)
449
467
  break
@@ -455,12 +473,14 @@ export class RuntimeGroup extends PackageGroup {
455
473
  console.error(`Usage: opm ${this.name} search <version>`)
456
474
  process.exit(1)
457
475
  }
476
+ await this._printEndpoint(manager, opts.proxy)
458
477
  const result = await manager.searchPackage(this.name, version, { proxy: opts.proxy })
459
478
  output(result)
460
479
  break
461
480
  }
462
481
 
463
482
  case 'versions': {
483
+ await this._printEndpoint(manager, opts.proxy)
464
484
  const result = await manager.packageVersions(this.name, { proxy: opts.proxy })
465
485
  output(result)
466
486
  break