@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.
- package/README.md +381 -0
- package/bin/index.js +9 -0
- package/docs/managers/apt.md +45 -0
- package/docs/managers/brew.md +92 -0
- package/docs/managers/bun.md +50 -0
- package/docs/managers/cft.md +57 -0
- package/docs/managers/composer.md +96 -0
- package/docs/managers/config.md +32 -0
- package/docs/managers/dnf.md +46 -0
- package/docs/managers/node.md +49 -0
- package/docs/managers/npm.md +100 -0
- package/docs/managers/nvm.md +34 -0
- package/docs/managers/php.md +48 -0
- package/docs/managers/phpenv.md +34 -0
- package/docs/managers/pip.md +98 -0
- package/docs/managers/python.md +53 -0
- package/docs/managers/uv.md +27 -0
- package/docs/opm.html +570 -0
- package/docs/packages/opencode.md +41 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +7 -0
- package/src/binaries/base.js +508 -0
- package/src/binaries/cft/index.js +35 -0
- package/src/binaries/depchecker.js +172 -0
- package/src/binaries/exefinder.js +110 -0
- package/src/binaries/platform.js +17 -0
- package/src/cli/doc.js +335 -0
- package/src/cli/groups/config.js +78 -0
- package/src/cli/groups/ensure.js +96 -0
- package/src/cli/groups/homebrew.js +73 -0
- package/src/cli/groups/nvm.js +73 -0
- package/src/cli/groups/package.js +514 -0
- package/src/cli/groups/phpenv.js +89 -0
- package/src/cli/groups/uv.js +73 -0
- package/src/cli/groups/version-manager.js +186 -0
- package/src/cli/helpers/args.js +49 -0
- package/src/cli/index.js +129 -0
- package/src/config.js +133 -0
- package/src/config.json5 +158 -0
- package/src/formatter.js +13 -0
- package/src/harnesses/base.js +205 -0
- package/src/harnesses/opencode/clear.js +239 -0
- package/src/harnesses/opencode/db.js +39 -0
- package/src/harnesses/opencode/index.js +46 -0
- package/src/harnesses/opencode/sqlite.js +200 -0
- package/src/index.js +46 -0
- package/src/managers/apt.js +377 -0
- package/src/managers/base.js +167 -0
- package/src/managers/brew.js +374 -0
- package/src/managers/bun.js +342 -0
- package/src/managers/composer.js +280 -0
- package/src/managers/dnf.js +370 -0
- package/src/managers/http.js +432 -0
- package/src/managers/node.js +150 -0
- package/src/managers/npm.js +473 -0
- package/src/managers/php.js +157 -0
- package/src/managers/pip.js +555 -0
- package/src/managers/python.js +180 -0
- package/src/managers/runtime.js +309 -0
- package/src/managers/winget.js +268 -0
- package/src/presets.js +24 -0
- 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
|
+
}
|