@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,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
|
+
}
|