@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,555 @@
1
+ /**
2
+ * pip 包管理器实现
3
+ *
4
+ * 通过执行 pip CLI 命令实现统一接口。
5
+ * 源查询使用 PyPI/Index JSON API,本地查询使用 pip show/list。
6
+ */
7
+
8
+ import { execSync, spawnSync } from 'node:child_process'
9
+ import path from 'node:path'
10
+ import { URL } from 'node:url'
11
+ import { PackageManager } from './base.js'
12
+ import { fetchText } from './http.js'
13
+ import { getActiveRegistry, setActiveRegistry } from '../config.js'
14
+
15
+ export class PipManager extends PackageManager {
16
+ constructor() {
17
+ super()
18
+ this.name = 'pip'
19
+ }
20
+
21
+ /**
22
+ * 执行 pip 命令,返回 stdout
23
+ * execSync 默认通过 shell 执行,兼容 Windows 的 pip.exe
24
+ * @param {string[]} args
25
+ * @param {object} [opts]
26
+ * @returns {string}
27
+ * @private
28
+ */
29
+ _exec(args, opts = {}) {
30
+ const cmd = ['pip', ...args].join(' ')
31
+ try {
32
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts })
33
+ } catch (err) {
34
+ if (opts.allowNonZero) {
35
+ return err.stdout ? err.stdout.toString() : ''
36
+ }
37
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
38
+ if (stderr) {
39
+ throw new Error(stderr)
40
+ }
41
+ return err.stdout ? err.stdout.toString() : ''
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 执行 pip 命令,输出直接继承到终端
47
+ * @param {string[]} args
48
+ * @private
49
+ */
50
+ _execInherit(args) {
51
+ const result = spawnSync('pip', args, { stdio: 'inherit', shell: true })
52
+ if (result.error) {
53
+ throw new Error(result.error.message)
54
+ }
55
+ if (result.status !== 0) {
56
+ throw new Error(`pip ${args.join(' ')} exited with code ${result.status}`)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 获取系统级 Python 可执行文件路径(绕过 venv/conda)
62
+ *
63
+ * 通过 sys.base_prefix 定位系统 Python:
64
+ * - venv 激活时: base_prefix 指向创建 venv 的系统 Python
65
+ * - conda 激活时: base_prefix 指向 conda base 环境
66
+ * - 未激活任何环境: base_prefix === sys.prefix,直接用当前 python
67
+ *
68
+ * 兼容 uv 创建的 venv(标准 venv 结构,sys.base_prefix 正确)
69
+ *
70
+ * @returns {string} 系统 Python 可执行文件路径
71
+ * @private
72
+ */
73
+ _getBasePython() {
74
+ const script = 'import sys; print(sys.base_prefix)'
75
+ let basePrefix = ''
76
+
77
+ try {
78
+ basePrefix = execSync(`python -c "${script}"`, {
79
+ encoding: 'utf8',
80
+ stdio: ['pipe', 'pipe', 'pipe']
81
+ }).trim()
82
+ } catch {}
83
+
84
+ if (!basePrefix) {
85
+ return 'python'
86
+ }
87
+
88
+ const currentPrefix = process.env.VIRTUAL_ENV
89
+ || process.env.CONDA_PREFIX
90
+ || ''
91
+
92
+ if (!currentPrefix) {
93
+ return 'python'
94
+ }
95
+
96
+ const isWindows = process.platform === 'win32'
97
+ const pythonExe = isWindows ? 'python.exe' : 'python'
98
+ return path.join(basePrefix, pythonExe)
99
+ }
100
+
101
+ /**
102
+ * 按作用域执行 pip 命令,返回 stdout
103
+ *
104
+ * global=true 时通过系统 Python 的 -m pip 绕过当前 venv/conda 环境
105
+ * global=false 时直接用当前环境 pip(venv/conda/系统)
106
+ *
107
+ * @param {string[]} args - pip 参数
108
+ * @param {object} [opts] - 选项
109
+ * @param {boolean} [opts.global] - 是否查询系统级
110
+ * @returns {string}
111
+ * @private
112
+ */
113
+ _execPip(args, opts = {}) {
114
+ if (opts.global) {
115
+ const basePython = this._getBasePython()
116
+ const cmd = [basePython, '-m', 'pip', ...args].join(' ')
117
+ try {
118
+ return execSync(cmd, {
119
+ encoding: 'utf8',
120
+ stdio: ['pipe', 'pipe', 'pipe'],
121
+ ...(opts.execOpts || {})
122
+ })
123
+ } catch (err) {
124
+ if (opts.allowNonZero) {
125
+ return err.stdout ? err.stdout.toString() : ''
126
+ }
127
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
128
+ if (stderr) {
129
+ throw new Error(stderr)
130
+ }
131
+ return err.stdout ? err.stdout.toString() : ''
132
+ }
133
+ }
134
+ return this._exec(args, opts)
135
+ }
136
+
137
+ /**
138
+ * 按作用域执行 pip 命令,输出直接继承到终端
139
+ * @param {string[]} args - pip 参数
140
+ * @param {object} [opts] - 选项
141
+ * @param {boolean} [opts.global] - 是否操作系统级
142
+ * @private
143
+ */
144
+ _execPipInherit(args, opts = {}) {
145
+ if (opts.global) {
146
+ const basePython = this._getBasePython()
147
+ const result = spawnSync(basePython, ['-m', 'pip', ...args], {
148
+ stdio: 'inherit',
149
+ shell: true
150
+ })
151
+ if (result.error) {
152
+ throw new Error(result.error.message)
153
+ }
154
+ if (result.status !== 0) {
155
+ throw new Error(`pip ${args.join(' ')} exited with code ${result.status}`)
156
+ }
157
+ return
158
+ }
159
+ this._execInherit(args)
160
+ }
161
+
162
+ /**
163
+ * 获取当前 index-url(来自 opm 配置的 active 镜像,key 已解析为 URL)
164
+ * @returns {string}
165
+ * @private
166
+ */
167
+ _getIndexUrl() {
168
+ return getActiveRegistry('pip') || 'https://pypi.org/simple'
169
+ }
170
+
171
+ /**
172
+ * 从 index-url 构造 JSON API URL
173
+ * pip 的 index-url 格式为 https://pypi.org/simple
174
+ * JSON API 为 https://pypi.org/pypi/<name>/json
175
+ * @param {string} indexUrl
176
+ * @returns {string}
177
+ * @private
178
+ */
179
+ _buildApiUrl(indexUrl) {
180
+ let base = indexUrl.replace(/\/+$/, '')
181
+ if (base.endsWith('/simple')) {
182
+ base = base.slice(0, -7)
183
+ }
184
+ return base
185
+ }
186
+
187
+ /**
188
+ * 构造 simple index URL
189
+ * 格式为 <index-url>/<name>/
190
+ * @param {string} indexUrl
191
+ * @param {string} name
192
+ * @returns {string}
193
+ * @private
194
+ */
195
+ _buildSimpleUrl(indexUrl, name) {
196
+ const base = indexUrl.replace(/\/+$/, '')
197
+ return `${base}/${encodeURIComponent(name)}/`
198
+ }
199
+
200
+ /**
201
+ * 构造 pip index-url 参数(使用配置中 active 镜像,key 已解析为 URL)
202
+ * @returns {string[]}
203
+ * @private
204
+ */
205
+ _indexUrlArgs() {
206
+ const url = this._getIndexUrl()
207
+ return url ? ['--index-url', url] : []
208
+ }
209
+
210
+ /**
211
+ * 通过代理或直连获取 URL 文本内容(复用共享隧道实现)
212
+ * @param {string} url - 请求 URL
213
+ * @param {{ proxy?: string, timeout?: number, maxRedirects?: number }} [opts] - 选项
214
+ * @returns {Promise<string|null>}
215
+ * @private
216
+ */
217
+ async _fetchText(url, opts = {}) {
218
+ return fetchText(url, opts)
219
+ }
220
+
221
+
222
+ /**
223
+ * 通过 JSON API 查询包信息
224
+ * @param {string} name
225
+ * @param {{ proxy?: string }} [opts] - 选项
226
+ * @returns {Promise<object|null>}
227
+ * @private
228
+ */
229
+ async _fetchApiData(name, opts = {}) {
230
+ const indexUrl = this._getIndexUrl()
231
+ const apiBase = this._buildApiUrl(indexUrl)
232
+ const url = `${apiBase}/pypi/${encodeURIComponent(name)}/json`
233
+
234
+ try {
235
+ const text = await this._fetchText(url, opts)
236
+ if (!text) return null
237
+ return JSON.parse(text)
238
+ } catch {
239
+ return null
240
+ }
241
+ }
242
+
243
+ /**
244
+ * 通过 simple index 解析包版本列表
245
+ * 当 JSON API 不可用时,fallback 到 simple index HTML 解析
246
+ * @param {string} name
247
+ * @param {{ proxy?: string }} [opts] - 选项
248
+ * @returns {Promise<string[]|null>}
249
+ * @private
250
+ */
251
+ async _fetchVersionsFromSimple(name, opts = {}) {
252
+ const indexUrl = this._getIndexUrl()
253
+ const url = this._buildSimpleUrl(indexUrl, name)
254
+
255
+ try {
256
+ const html = await this._fetchText(url, opts)
257
+ if (!html) return null
258
+ const versions = new Set()
259
+ const normalizedName = name.replace(/[-_.]/g, '[-_.]')
260
+ const sdistRegex = new RegExp(`${normalizedName}-(.+?)\\.tar\\.gz`, 'g')
261
+ const wheelRegex = new RegExp(`${normalizedName}-([^-]+)-[^-]+-[^-]+\\.whl`, 'g')
262
+ let match
263
+ while ((match = sdistRegex.exec(html)) !== null) {
264
+ versions.add(match[1])
265
+ }
266
+ while ((match = wheelRegex.exec(html)) !== null) {
267
+ versions.add(match[1])
268
+ }
269
+ return versions.size > 0 ? [...versions] : null
270
+ } catch {
271
+ return null
272
+ }
273
+ }
274
+
275
+ async getRegistry() {
276
+ const registry = this._getIndexUrl()
277
+ return { registry }
278
+ }
279
+
280
+ async getVersion() {
281
+ try {
282
+ const out = this._exec(['--version'], { allowNonZero: true }).trim()
283
+ const match = out.match(/pip\s+([\d.]+)/i)
284
+ if (match) {
285
+ return { name: 'pip', installed: true, version: match[1] }
286
+ }
287
+ if (out) {
288
+ return { name: 'pip', installed: true, version: out }
289
+ }
290
+ } catch {}
291
+ return { name: 'pip', version: '', installed: false, hint: 'Install pip from https://pip.pypa.io/en/stable/installation/' }
292
+ }
293
+
294
+ async setRegistry(url) {
295
+ const resolved = setActiveRegistry('pip', url)
296
+ return { registry: resolved }
297
+ }
298
+
299
+ /**
300
+ * 比较两个版本号,返回 -1/0/1
301
+ * 支持预发布版本(如 2.34.0.dev1)
302
+ * @param {string} a
303
+ * @param {string} b
304
+ * @returns {number}
305
+ * @private
306
+ */
307
+ _compareVersions(a, b) {
308
+ const parseVer = (v) => {
309
+ const parts = v.split('.')
310
+ return parts.map((p) => {
311
+ const n = parseInt(p, 10)
312
+ return isNaN(n) ? p : n
313
+ })
314
+ }
315
+ const pa = parseVer(a)
316
+ const pb = parseVer(b)
317
+ const len = Math.max(pa.length, pb.length)
318
+ for (let i = 0; i < len; i++) {
319
+ const va = pa[i] ?? 0
320
+ const vb = pb[i] ?? 0
321
+ if (typeof va === 'number' && typeof vb === 'number') {
322
+ if (va < vb) return -1
323
+ if (va > vb) return 1
324
+ } else if (typeof va === 'string' && typeof vb === 'number') {
325
+ return -1
326
+ } else if (typeof va === 'number' && typeof vb === 'string') {
327
+ return 1
328
+ } else {
329
+ const sa = String(va)
330
+ const sb = String(vb)
331
+ if (sa < sb) return -1
332
+ if (sa > sb) return 1
333
+ }
334
+ }
335
+ return 0
336
+ }
337
+
338
+ /**
339
+ * 从版本列表中找出最新的稳定版本(排除 dev/rc/alpha/beta 等预发布)
340
+ * @param {string[]} versions
341
+ * @returns {string}
342
+ * @private
343
+ */
344
+ _pickLatest(versions) {
345
+ const stable = versions.filter((v) => !/dev|rc|alpha|beta|a\d|b\d/i.test(v))
346
+ const pool = stable.length > 0 ? stable : versions
347
+ const sorted = [...pool].sort((a, b) => this._compareVersions(a, b))
348
+ return sorted[sorted.length - 1] || 'unknown'
349
+ }
350
+
351
+ async searchPackage(name, version, opts = {}) {
352
+ const { registry } = await this.getRegistry()
353
+ const data = await this._fetchApiData(name, opts)
354
+
355
+ if (data) {
356
+ const allVersions = Object.keys(data.releases || {})
357
+ const latest = data.info?.version || allVersions[allVersions.length - 1] || 'unknown'
358
+
359
+ if (version) {
360
+ return {
361
+ name,
362
+ version,
363
+ exists: true,
364
+ available: allVersions.includes(version),
365
+ registry
366
+ }
367
+ }
368
+
369
+ return {
370
+ name,
371
+ exists: true,
372
+ latest,
373
+ versions: allVersions,
374
+ registry
375
+ }
376
+ }
377
+
378
+ const simpleVersions = await this._fetchVersionsFromSimple(name, opts)
379
+ if (simpleVersions) {
380
+ if (version) {
381
+ return {
382
+ name,
383
+ version,
384
+ exists: true,
385
+ available: simpleVersions.includes(version),
386
+ registry
387
+ }
388
+ }
389
+ return {
390
+ name,
391
+ exists: true,
392
+ latest: this._pickLatest(simpleVersions),
393
+ versions: simpleVersions,
394
+ registry
395
+ }
396
+ }
397
+
398
+ if (version) {
399
+ return { name, version, exists: false, available: false, registry }
400
+ }
401
+ return { name, exists: false, registry }
402
+ }
403
+
404
+ /**
405
+ * 解析 pip show 输出为键值对
406
+ * @param {string} name - 包名
407
+ * @param {object} [opts] - 选项
408
+ * @param {boolean} [opts.global] - 是否查询系统级
409
+ * @returns {Record<string, string>|null}
410
+ * @private
411
+ */
412
+ _parsePipShow(name, opts = {}) {
413
+ try {
414
+ const out = this._execPip(['show', name], opts)
415
+ const lines = out.split('\n')
416
+ const info = {}
417
+ for (const line of lines) {
418
+ const idx = line.indexOf(':')
419
+ if (idx > 0) {
420
+ const key = line.slice(0, idx).trim().toLowerCase()
421
+ const val = line.slice(idx + 1).trim()
422
+ info[key] = val
423
+ }
424
+ }
425
+ if (info.version) {
426
+ return info
427
+ }
428
+ } catch {}
429
+ return null
430
+ }
431
+
432
+ async isInstalled(name, opts = {}) {
433
+ const info = this._parsePipShow(name, opts)
434
+ if (info) {
435
+ return {
436
+ name,
437
+ installed: true,
438
+ version: info.version,
439
+ path: info.location || '',
440
+ ...(opts.global ? { global: true } : {})
441
+ }
442
+ }
443
+
444
+ return { name, installed: false, ...(opts.global ? { global: true } : {}) }
445
+ }
446
+
447
+ async packageInfo(name, opts = {}) {
448
+ const data = await this._fetchApiData(name, opts)
449
+ if (data) {
450
+ const info = data.info || {}
451
+ return {
452
+ name,
453
+ description: info.summary || '',
454
+ latest: info.version || '',
455
+ homepage: info.home_page || info.project_url || '',
456
+ license: info.license || '',
457
+ author: info.author || '',
458
+ repository: info.project_urls?.Source || info.project_urls?.Repository || ''
459
+ }
460
+ }
461
+ const info = this._parsePipShow(name)
462
+ if (info) {
463
+ return {
464
+ name,
465
+ description: info.summary || '',
466
+ latest: info.version || '',
467
+ homepage: info['home-page'] || info['project-url'] || '',
468
+ license: info.license || '',
469
+ author: info.author || '',
470
+ repository: ''
471
+ }
472
+ }
473
+ return { name, description: '(not found)' }
474
+ }
475
+
476
+ async packageVersions(name, opts = {}) {
477
+ const { registry } = await this.getRegistry()
478
+ const data = await this._fetchApiData(name, opts)
479
+ if (data) {
480
+ const versions = Object.keys(data.releases || {})
481
+ return { name, versions, registry }
482
+ }
483
+ const simpleVersions = await this._fetchVersionsFromSimple(name, opts)
484
+ if (simpleVersions) {
485
+ return { name, versions: simpleVersions, registry }
486
+ }
487
+ return { name, versions: [], registry }
488
+ }
489
+
490
+ async listInstalled(opts = {}) {
491
+ try {
492
+ const out = this._execPip(['list', '--format=freeze'], opts)
493
+ const items = []
494
+ for (const line of out.split('\n')) {
495
+ const trimmed = line.trim()
496
+ if (!trimmed) continue
497
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)==(.+)$/)
498
+ if (match) {
499
+ items.push({ name: match[1], version: match[2], ...(opts.global ? { global: true } : {}) })
500
+ }
501
+ }
502
+ return items
503
+ } catch (err) {
504
+ throw new Error(`pip list failed: ${err.message}`)
505
+ }
506
+ }
507
+
508
+ async listOutdated(opts = {}) {
509
+ try {
510
+ const args = ['list', '--outdated', '--format=json']
511
+ if (opts.proxy) args.push('--proxy', opts.proxy)
512
+ const jsonOut = this._execPip(args, { ...opts, allowNonZero: true })
513
+ const parsed = JSON.parse(jsonOut)
514
+ if (!Array.isArray(parsed)) return []
515
+ return parsed.map((item) => ({
516
+ name: item.name || '',
517
+ current: item.version || '',
518
+ latest: item.latest_version || '',
519
+ ...(opts.global ? { global: true } : {})
520
+ }))
521
+ } catch (err) {
522
+ if (err.message.includes('No packages') || err.message.includes('No broken')) {
523
+ return []
524
+ }
525
+ throw new Error(`pip list --outdated failed: ${err.message}`)
526
+ }
527
+ }
528
+
529
+ async install(name, version, opts = {}) {
530
+ const spec = version ? `${name}==${version}` : name
531
+ const args = ['install', spec]
532
+ args.push(...this._indexUrlArgs())
533
+ if (opts.proxy) args.push('--proxy', opts.proxy)
534
+ this._execPipInherit(args, opts)
535
+ return { name, version: version || 'latest', action: 'installed' }
536
+ }
537
+
538
+ async uninstall(name, opts = {}) {
539
+ this._execPipInherit(['uninstall', '-y', name], opts)
540
+ return { name, action: 'uninstalled' }
541
+ }
542
+
543
+ async upgrade(name, opts = {}) {
544
+ const args = ['install', '--upgrade', name]
545
+ args.push(...this._indexUrlArgs())
546
+ if (opts.proxy) args.push('--proxy', opts.proxy)
547
+ this._execPipInherit(args, opts)
548
+ return { name, version: 'latest', action: 'upgraded' }
549
+ }
550
+
551
+ async cleanCache() {
552
+ this._execInherit(['cache', 'purge'])
553
+ return { action: 'cache cleaned' }
554
+ }
555
+ }