@wwkit/opm 1.0.1

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.
Files changed (62) hide show
  1. package/README.md +381 -0
  2. package/bin/index.js +9 -0
  3. package/docs/managers/apt.md +45 -0
  4. package/docs/managers/brew.md +92 -0
  5. package/docs/managers/bun.md +50 -0
  6. package/docs/managers/cft.md +57 -0
  7. package/docs/managers/composer.md +96 -0
  8. package/docs/managers/config.md +32 -0
  9. package/docs/managers/dnf.md +46 -0
  10. package/docs/managers/node.md +49 -0
  11. package/docs/managers/npm.md +100 -0
  12. package/docs/managers/nvm.md +34 -0
  13. package/docs/managers/php.md +48 -0
  14. package/docs/managers/phpenv.md +34 -0
  15. package/docs/managers/pip.md +98 -0
  16. package/docs/managers/python.md +53 -0
  17. package/docs/managers/uv.md +27 -0
  18. package/docs/opm.html +570 -0
  19. package/docs/packages/opencode.md +41 -0
  20. package/package.json +51 -0
  21. package/scripts/postinstall.js +7 -0
  22. package/src/binaries/base.js +508 -0
  23. package/src/binaries/cft/index.js +35 -0
  24. package/src/binaries/depchecker.js +172 -0
  25. package/src/binaries/exefinder.js +110 -0
  26. package/src/binaries/platform.js +17 -0
  27. package/src/cli/doc.js +335 -0
  28. package/src/cli/groups/config.js +78 -0
  29. package/src/cli/groups/ensure.js +96 -0
  30. package/src/cli/groups/homebrew.js +73 -0
  31. package/src/cli/groups/nvm.js +73 -0
  32. package/src/cli/groups/package.js +514 -0
  33. package/src/cli/groups/phpenv.js +89 -0
  34. package/src/cli/groups/uv.js +73 -0
  35. package/src/cli/groups/version-manager.js +186 -0
  36. package/src/cli/helpers/args.js +49 -0
  37. package/src/cli/index.js +129 -0
  38. package/src/config.js +133 -0
  39. package/src/config.json5 +158 -0
  40. package/src/formatter.js +13 -0
  41. package/src/harnesses/base.js +205 -0
  42. package/src/harnesses/opencode/clear.js +239 -0
  43. package/src/harnesses/opencode/db.js +39 -0
  44. package/src/harnesses/opencode/index.js +46 -0
  45. package/src/harnesses/opencode/sqlite.js +200 -0
  46. package/src/index.js +46 -0
  47. package/src/managers/apt.js +377 -0
  48. package/src/managers/base.js +167 -0
  49. package/src/managers/brew.js +374 -0
  50. package/src/managers/bun.js +342 -0
  51. package/src/managers/composer.js +280 -0
  52. package/src/managers/dnf.js +370 -0
  53. package/src/managers/http.js +432 -0
  54. package/src/managers/node.js +150 -0
  55. package/src/managers/npm.js +473 -0
  56. package/src/managers/php.js +157 -0
  57. package/src/managers/pip.js +555 -0
  58. package/src/managers/python.js +180 -0
  59. package/src/managers/runtime.js +309 -0
  60. package/src/managers/winget.js +268 -0
  61. package/src/presets.js +24 -0
  62. package/src/proxy-presets.js +12 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * bun 包管理器实现
3
+ *
4
+ * Bun 是全栈 JS 工具链(runtime + 包管理器 + 打包器)。
5
+ * 包管理命令通过 bun CLI 实现,源查询通过 HTTP 直接查 npm registry。
6
+ * 镜像通过 BUN_CONFIG_REGISTRY 环境变量传递给 bun 命令。
7
+ */
8
+
9
+ import { execSync, spawnSync } from 'node:child_process'
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { PackageManager } from './base.js'
13
+ import { getActiveRegistry, setActiveRegistry } from '../config.js'
14
+
15
+ export class BunManager extends PackageManager {
16
+ constructor() {
17
+ super()
18
+ this.name = 'bun'
19
+ }
20
+
21
+ /**
22
+ * 构造 bun 调用环境(含 registry 镜像 + proxy)
23
+ * @param {string} [proxy]
24
+ * @returns {Record<string,string>}
25
+ * @private
26
+ */
27
+ _buildEnv(proxy) {
28
+ const env = { ...process.env }
29
+ const registry = getActiveRegistry('bun')
30
+ if (registry) {
31
+ env.BUN_CONFIG_REGISTRY = registry
32
+ }
33
+ if (proxy) {
34
+ Object.assign(env, {
35
+ HTTP_PROXY: proxy,
36
+ HTTPS_PROXY: proxy,
37
+ ALL_PROXY: proxy,
38
+ http_proxy: proxy,
39
+ https_proxy: proxy,
40
+ all_proxy: proxy,
41
+ })
42
+ }
43
+ return env
44
+ }
45
+
46
+ /**
47
+ * 执行 bun 命令,返回 stdout
48
+ * @param {string[]} args
49
+ * @param {{ allowNonZero?: boolean, proxy?: string }} [opts]
50
+ * @returns {string}
51
+ * @private
52
+ */
53
+ _exec(args, opts = {}) {
54
+ try {
55
+ return execSync(['bun', ...args].join(' '), {
56
+ encoding: 'utf8',
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ env: this._buildEnv(opts.proxy),
59
+ })
60
+ } catch (err) {
61
+ if (opts.allowNonZero) {
62
+ return err.stdout ? err.stdout.toString() : ''
63
+ }
64
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
65
+ if (stderr) {
66
+ throw new Error(stderr)
67
+ }
68
+ return err.stdout ? err.stdout.toString() : ''
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 执行 bun 命令并解析 JSON 输出
74
+ * @param {string[]} args
75
+ * @param {{ proxy?: string }} [opts]
76
+ * @returns {*}
77
+ * @private
78
+ */
79
+ _execJson(args, opts = {}) {
80
+ const out = this._exec([...args], { allowNonZero: true, ...opts })
81
+ try {
82
+ return JSON.parse(out)
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 执行 bun 命令,输出直接继承到终端
90
+ * @param {string[]} args
91
+ * @param {{ proxy?: string }} [opts]
92
+ * @private
93
+ */
94
+ _execInherit(args, opts = {}) {
95
+ const result = spawnSync('bun', args, {
96
+ stdio: 'inherit',
97
+ shell: true,
98
+ env: this._buildEnv(opts.proxy),
99
+ })
100
+ if (result.error) {
101
+ throw new Error(result.error.message)
102
+ }
103
+ if (result.status !== 0) {
104
+ throw new Error(`bun ${args.join(' ')} exited with code ${result.status}`)
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 通过 HTTP 查询 npm registry 获取包元数据
110
+ * @param {string} pkgName
111
+ * @param {{ proxy?: string }} [opts]
112
+ * @returns {Promise<object|null>}
113
+ * @private
114
+ */
115
+ async _fetchRegistryMetadata(pkgName, opts = {}) {
116
+ const registry = getActiveRegistry('bun').replace(/\/$/, '')
117
+ const url = `${registry}/${encodeURIComponent(pkgName).replace('%40', '@')}`
118
+ try {
119
+ const { default: http } = await import('node:http')
120
+ const { default: https } = await import('node:https')
121
+ const client = url.startsWith('https') ? https : http
122
+ const proxyAgent = opts.proxy ? await this._getProxyAgent(opts.proxy) : null
123
+ return new Promise((resolve) => {
124
+ const req = client.get(url, { agent: proxyAgent }, (res) => {
125
+ let data = ''
126
+ res.on('data', (chunk) => (data += chunk))
127
+ res.on('end', () => {
128
+ try {
129
+ resolve(JSON.parse(data))
130
+ } catch {
131
+ resolve(null)
132
+ }
133
+ })
134
+ })
135
+ req.on('error', () => resolve(null))
136
+ req.setTimeout(15000, () => {
137
+ req.destroy()
138
+ resolve(null)
139
+ })
140
+ })
141
+ } catch {
142
+ return null
143
+ }
144
+ }
145
+
146
+ /**
147
+ * 获取 proxy agent(简化实现,仅支持 http proxy)
148
+ * @param {string} proxy
149
+ * @returns {Promise<undefined>}
150
+ * @private
151
+ */
152
+ async _getProxyAgent(proxy) {
153
+ return undefined
154
+ }
155
+
156
+ async getVersion() {
157
+ try {
158
+ const version = this._exec(['--version'], { allowNonZero: true }).trim()
159
+ if (version) {
160
+ return { name: 'bun', installed: true, version }
161
+ }
162
+ } catch {}
163
+ return { name: 'bun', version: '', installed: false, hint: 'Install Bun from https://bun.sh/ (curl -fsSL https://bun.sh/install | bash)' }
164
+ }
165
+
166
+ async getRegistry() {
167
+ return { registry: getActiveRegistry('bun') }
168
+ }
169
+
170
+ async setRegistry(url) {
171
+ const resolved = setActiveRegistry('bun', url)
172
+ return { registry: resolved }
173
+ }
174
+
175
+ async searchPackage(name, version, opts = {}) {
176
+ const { registry } = await this.getRegistry()
177
+ const data = await this._fetchRegistryMetadata(name, opts)
178
+ if (!data) {
179
+ if (version) return { name, version, exists: false, available: false, registry }
180
+ return { name, exists: false, registry }
181
+ }
182
+ const versions = data.versions ? Object.keys(data.versions) : []
183
+ if (version) {
184
+ return { name, version, exists: true, available: versions.includes(version), registry }
185
+ }
186
+ return {
187
+ name,
188
+ exists: true,
189
+ latest: data['dist-tags']?.latest || versions[versions.length - 1] || 'unknown',
190
+ versions,
191
+ registry,
192
+ }
193
+ }
194
+
195
+ async isInstalled(name, opts = {}) {
196
+ const pkgPath = path.join(process.cwd(), 'node_modules', name, 'package.json')
197
+ if (fs.existsSync(pkgPath)) {
198
+ try {
199
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
200
+ return { name, installed: true, version: pkg.version, path: path.dirname(pkgPath) }
201
+ } catch {}
202
+ }
203
+
204
+ if (opts.global) {
205
+ try {
206
+ const out = this._exec(['pm', 'ls', '-g'], { allowNonZero: true })
207
+ if (out.includes(name)) {
208
+ const m = out.match(new RegExp(`${name}@([\\d.]+)`))
209
+ return { name, installed: true, version: m ? m[1] : 'unknown', global: true }
210
+ }
211
+ } catch {}
212
+ }
213
+
214
+ const declared = this._readDeclaredDeps()
215
+ if (declared[name]) {
216
+ return { name, installed: true, version: declared[name] }
217
+ }
218
+
219
+ return { name, installed: false }
220
+ }
221
+
222
+ /**
223
+ * 读取 package.json 中的依赖声明
224
+ * @returns {Record<string, string>}
225
+ * @private
226
+ */
227
+ _readDeclaredDeps() {
228
+ const pkgPath = path.join(process.cwd(), 'package.json')
229
+ if (!fs.existsSync(pkgPath)) return {}
230
+ try {
231
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
232
+ return {
233
+ ...(pkg.dependencies || {}),
234
+ ...(pkg.devDependencies || {}),
235
+ ...(pkg.peerDependencies || {}),
236
+ ...(pkg.optionalDependencies || {}),
237
+ }
238
+ } catch {
239
+ return {}
240
+ }
241
+ }
242
+
243
+ async packageInfo(name, opts = {}) {
244
+ const data = await this._fetchRegistryMetadata(name, opts)
245
+ if (!data) {
246
+ return { name, description: '(not found)' }
247
+ }
248
+ const latest = data['dist-tags']?.latest || ''
249
+ const latestData = latest && data.versions?.[latest] ? data.versions[latest] : data
250
+ return {
251
+ name,
252
+ description: latestData.description || data.description || '',
253
+ latest,
254
+ homepage: latestData.homepage || data.homepage || '',
255
+ license: latestData.license || data.license || '',
256
+ author: typeof latestData.author === 'object' ? latestData.author?.name || '' : (latestData.author || ''),
257
+ repository: typeof latestData.repository === 'object' ? latestData.repository?.url || '' : (latestData.repository || ''),
258
+ }
259
+ }
260
+
261
+ async packageVersions(name, opts = {}) {
262
+ const { registry } = await this.getRegistry()
263
+ const data = await this._fetchRegistryMetadata(name, opts)
264
+ if (!data) {
265
+ return { name, versions: [], registry }
266
+ }
267
+ const versions = data.versions ? Object.keys(data.versions) : []
268
+ return { name, versions, registry }
269
+ }
270
+
271
+ async listInstalled(opts = {}) {
272
+ const args = ['pm', 'ls']
273
+ if (opts.global) args.push('-g')
274
+ try {
275
+ const out = this._exec(args, { allowNonZero: true })
276
+ const items = []
277
+ for (const line of out.split('\n')) {
278
+ const m = line.match(/([^\s]+)@([\d.]+)/)
279
+ if (m) {
280
+ items.push({ name: m[1], version: m[2], ...(opts.global ? { global: true } : {}) })
281
+ }
282
+ }
283
+ return items
284
+ } catch {
285
+ return []
286
+ }
287
+ }
288
+
289
+ async listOutdated(opts = {}) {
290
+ const args = ['outdated']
291
+ if (opts.global) args.push('-g')
292
+ try {
293
+ const out = this._exec(args, { allowNonZero: true, proxy: opts.proxy })
294
+ const items = []
295
+ const lines = out.split('\n')
296
+ for (const line of lines) {
297
+ const parts = line.trim().split(/\s+/)
298
+ if (parts.length >= 3 && parts[0] !== 'package' && parts[0] !== 'name') {
299
+ items.push({
300
+ name: parts[0],
301
+ current: parts[1] || 'unknown',
302
+ wanted: parts[1] || '',
303
+ latest: parts[2] || 'unknown',
304
+ })
305
+ }
306
+ }
307
+ return items
308
+ } catch {
309
+ return []
310
+ }
311
+ }
312
+
313
+ async install(name, version, opts = {}) {
314
+ const spec = version ? `${name}@${version}` : name
315
+ const args = ['add', spec]
316
+ if (opts.global) args.push('-g')
317
+
318
+ this._execInherit(args, { proxy: opts.proxy })
319
+ return { name, version: version || 'latest', action: 'installed' }
320
+ }
321
+
322
+ async uninstall(name, opts = {}) {
323
+ const args = ['remove', name]
324
+ if (opts.global) args.push('-g')
325
+
326
+ this._execInherit(args)
327
+ return { name, action: 'uninstalled' }
328
+ }
329
+
330
+ async upgrade(name, opts = {}) {
331
+ const args = ['update', name]
332
+ if (opts.global) args.push('-g')
333
+
334
+ this._execInherit(args, { proxy: opts.proxy })
335
+ return { name, version: 'latest', action: 'upgraded' }
336
+ }
337
+
338
+ async cleanCache() {
339
+ this._execInherit(['pm', 'cache', 'rm'])
340
+ return { action: 'cache cleaned' }
341
+ }
342
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * composer(PHP)包管理器实现
3
+ *
4
+ * 通过执行 composer CLI 命令实现统一接口。
5
+ * 源查询使用 Packagist P2 元数据端点({registry}/p2/{vendor}/{package}.json),
6
+ * 本地查询读取 vendor/composer/installed.json 或 composer CLI。
7
+ *
8
+ * composer 无 per-command registry 标记,镜像仅作用于源查询(P2 元数据拉取);
9
+ * install/upgrade 使用 composer CLI 自身的仓库配置(项目 composer.json / 全局 config)。
10
+ */
11
+
12
+ import { execSync, spawnSync } from 'node:child_process'
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+ import { PackageManager } from './base.js'
16
+ import { fetchText } from './http.js'
17
+ import { getActiveRegistry, setActiveRegistry } from '../config.js'
18
+
19
+ export class ComposerManager extends PackageManager {
20
+ constructor() {
21
+ super()
22
+ this.name = 'composer'
23
+ }
24
+
25
+ /**
26
+ * 执行 composer 命令,返回 stdout
27
+ * @param {string[]} args
28
+ * @param {{ allowNonZero?: boolean, proxy?: string }} [opts]
29
+ * @returns {string}
30
+ * @private
31
+ */
32
+ _exec(args, opts = {}) {
33
+ const cmd = ['composer', ...args].join(' ')
34
+ try {
35
+ return execSync(cmd, {
36
+ encoding: 'utf8',
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ env: this._buildEnv(opts.proxy),
39
+ })
40
+ } catch (err) {
41
+ if (opts.allowNonZero) {
42
+ return err.stdout ? err.stdout.toString() : ''
43
+ }
44
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
45
+ if (stderr) {
46
+ throw new Error(stderr)
47
+ }
48
+ return err.stdout ? err.stdout.toString() : ''
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 执行 composer 命令,输出直接继承到终端
54
+ * @param {string[]} args
55
+ * @param {{ proxy?: string }} [opts]
56
+ * @private
57
+ */
58
+ _execInherit(args, opts = {}) {
59
+ const result = spawnSync('composer', args, {
60
+ stdio: 'inherit',
61
+ shell: true,
62
+ env: this._buildEnv(opts.proxy),
63
+ })
64
+ if (result.error) {
65
+ throw new Error(result.error.message)
66
+ }
67
+ if (result.status !== 0) {
68
+ throw new Error(`composer ${args.join(' ')} exited with code ${result.status}`)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 构造 composer 命令环境(代理 via 标准 env vars)
74
+ * @param {string} [proxy]
75
+ * @returns {Record<string,string>}
76
+ * @private
77
+ */
78
+ _buildEnv(proxy) {
79
+ const env = { ...process.env }
80
+ if (proxy) {
81
+ Object.assign(env, {
82
+ HTTP_PROXY: proxy,
83
+ HTTPS_PROXY: proxy,
84
+ ALL_PROXY: proxy,
85
+ http_proxy: proxy,
86
+ https_proxy: proxy,
87
+ all_proxy: proxy,
88
+ })
89
+ }
90
+ return env
91
+ }
92
+
93
+ /**
94
+ * 获取激活镜像 base URL(P2 元数据端点基础)
95
+ * @returns {string}
96
+ * @private
97
+ */
98
+ _getBaseRegistry() {
99
+ return getActiveRegistry('composer') || 'https://repo.packagist.org'
100
+ }
101
+
102
+ /**
103
+ * 构造 Packagist P2 元数据 URL
104
+ * @param {string} name - 包名(vendor/package)
105
+ * @returns {string}
106
+ * @private
107
+ */
108
+ _buildP2Url(name) {
109
+ const base = this._getBaseRegistry().replace(/\/+$/, '')
110
+ return `${base}/p2/${name}.json`
111
+ }
112
+
113
+ /**
114
+ * 通过 P2 元数据端点查询包信息
115
+ * @param {string} name
116
+ * @param {{ proxy?: string }} [opts]
117
+ * @returns {Promise<object|null>} 包条目数组或 null
118
+ * @private
119
+ */
120
+ async _fetchMetadata(name, opts = {}) {
121
+ if (!name.includes('/')) return null
122
+ const url = this._buildP2Url(name)
123
+ const text = await fetchText(url, { proxy: opts.proxy })
124
+ if (!text) return null
125
+ try {
126
+ const data = JSON.parse(text)
127
+ const entries = data?.packages?.[name]
128
+ if (Array.isArray(entries) && entries.length > 0) return entries
129
+ } catch {}
130
+ return null
131
+ }
132
+
133
+ /**
134
+ * 从 P2 元数据条目中提取稳定最新版本
135
+ * @param {object[]} entries
136
+ * @returns {string}
137
+ * @private
138
+ */
139
+ _pickLatest(entries) {
140
+ for (const entry of entries) {
141
+ const v = entry.version || ''
142
+ if (v && !v.includes('dev') && !v.includes('-beta') && !v.includes('-RC') && !v.includes('-alpha')) {
143
+ return v
144
+ }
145
+ }
146
+ return entries[0]?.version || ''
147
+ }
148
+
149
+ async getVersion() {
150
+ try {
151
+ const out = this._exec(['--version'], { allowNonZero: true })
152
+ const match = out.match(/Composer version\s+(\S+)/)
153
+ if (match) {
154
+ return { name: 'composer', installed: true, version: match[1] }
155
+ }
156
+ } catch {}
157
+ return { name: 'composer', version: '', installed: false, hint: 'Install Composer from https://getcomposer.org/' }
158
+ }
159
+
160
+ async getRegistry() {
161
+ return { registry: getActiveRegistry('composer') }
162
+ }
163
+
164
+ async setRegistry(url) {
165
+ const resolved = setActiveRegistry('composer', url)
166
+ return { registry: resolved }
167
+ }
168
+
169
+ async searchPackage(name, version, opts = {}) {
170
+ const { registry } = await this.getRegistry()
171
+ const entries = await this._fetchMetadata(name, opts)
172
+ if (!entries) {
173
+ if (version) return { name, version, exists: false, available: false, registry }
174
+ return { name, exists: false, registry }
175
+ }
176
+ const versions = entries.map((e) => e.version).filter(Boolean)
177
+ const latest = this._pickLatest(entries)
178
+ if (version) {
179
+ return { name, version, exists: true, available: versions.includes(version), registry }
180
+ }
181
+ return { name, exists: true, latest, versions, registry }
182
+ }
183
+
184
+ /**
185
+ * 读取项目本地 vendor/composer/installed.json
186
+ * @returns {object[]|null}
187
+ * @private
188
+ */
189
+ _readInstalledJson() {
190
+ const p = path.join(process.cwd(), 'vendor', 'composer', 'installed.json')
191
+ if (!fs.existsSync(p)) return null
192
+ try {
193
+ const data = JSON.parse(fs.readFileSync(p, 'utf8'))
194
+ return Array.isArray(data.packages) ? data.packages : null
195
+ } catch {
196
+ return null
197
+ }
198
+ }
199
+
200
+ async isInstalled(name, opts = {}) {
201
+ const packages = this._readInstalledJson()
202
+ if (packages) {
203
+ const pkg = packages.find((p) => p.name === name)
204
+ if (pkg) {
205
+ return { name, installed: true, version: pkg.version || 'unknown' }
206
+ }
207
+ return { name, installed: false }
208
+ }
209
+ return { name, installed: false }
210
+ }
211
+
212
+ async packageInfo(name, opts = {}) {
213
+ const entries = await this._fetchMetadata(name, opts)
214
+ if (!entries) {
215
+ return { name, description: '(not found)' }
216
+ }
217
+ const entry = entries[0]
218
+ return {
219
+ name,
220
+ description: entry.description || '',
221
+ latest: this._pickLatest(entries),
222
+ homepage: entry.homepage || '',
223
+ license: Array.isArray(entry.license) ? entry.license.join(', ') : (entry.license || ''),
224
+ author: Array.isArray(entry.authors) ? (entry.authors[0]?.name || '') : '',
225
+ repository: entry.source?.url || entry.homepage || '',
226
+ }
227
+ }
228
+
229
+ async packageVersions(name, opts = {}) {
230
+ const { registry } = await this.getRegistry()
231
+ const entries = await this._fetchMetadata(name, opts)
232
+ const versions = entries ? entries.map((e) => e.version).filter(Boolean) : []
233
+ return { name, versions, registry }
234
+ }
235
+
236
+ async listInstalled(opts = {}) {
237
+ const packages = this._readInstalledJson()
238
+ if (!packages) return []
239
+ return packages.map((p) => ({
240
+ name: p.name,
241
+ version: p.version || 'unknown',
242
+ }))
243
+ }
244
+
245
+ async listOutdated(opts = {}) {
246
+ const out = this._exec(['outdated', '--format=json'], { allowNonZero: true, proxy: opts.proxy })
247
+ try {
248
+ const data = JSON.parse(out)
249
+ const installed = Array.isArray(data.installed) ? data.installed : []
250
+ return installed.map((item) => ({
251
+ name: item.name,
252
+ current: item.version || '',
253
+ latest: item.latest || '',
254
+ }))
255
+ } catch {
256
+ return []
257
+ }
258
+ }
259
+
260
+ async install(name, version, opts = {}) {
261
+ const spec = version ? `${name}:${version}` : name
262
+ this._execInherit(['require', spec, '--no-interaction'], { proxy: opts.proxy })
263
+ return { name, version: version || 'latest', action: 'installed' }
264
+ }
265
+
266
+ async uninstall(name, opts = {}) {
267
+ this._execInherit(['remove', name, '--no-interaction'])
268
+ return { name, action: 'uninstalled' }
269
+ }
270
+
271
+ async upgrade(name, opts = {}) {
272
+ this._execInherit(['update', name, '--no-interaction'], { proxy: opts.proxy })
273
+ return { name, version: 'latest', action: 'upgraded' }
274
+ }
275
+
276
+ async cleanCache() {
277
+ this._execInherit(['clear-cache'])
278
+ return { action: 'cache cleaned' }
279
+ }
280
+ }