@wwkit/opm 1.0.8 → 1.0.10
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 +30 -20
- package/docs/managers/apt.md +2 -3
- package/docs/managers/brew.md +2 -6
- package/docs/managers/bun.md +2 -4
- package/docs/managers/composer.md +2 -6
- package/docs/managers/dnf.md +2 -3
- package/docs/managers/npm.md +2 -6
- package/docs/managers/nvm.md +2 -0
- package/docs/managers/phpenv.md +2 -0
- package/docs/managers/pip.md +2 -6
- package/docs/managers/uv.md +2 -0
- package/docs/opm.html +13 -23
- package/package.json +3 -3
- package/src/cli/groups/ensure.js +25 -4
- package/src/cli/groups/nvm.js +5 -1
- package/src/cli/groups/overlay.js +169 -0
- package/src/cli/groups/package.js +42 -18
- package/src/cli/groups/phpenv.js +5 -1
- package/src/cli/groups/uv.js +5 -1
- package/src/cli/groups/version-manager.js +27 -0
- package/src/cli/index.js +4 -0
- package/src/config.json5 +1 -0
- package/src/harnesses/base.js +88 -20
- package/src/harnesses/harness/index.js +132 -0
- package/src/harnesses/opencode/index.js +1 -1
- package/src/index.js +2 -0
- package/src/managers/apt.js +3 -12
- package/src/managers/base.js +17 -2
- package/src/managers/brew.js +3 -5
- package/src/managers/bun.js +0 -18
- package/src/managers/composer.js +65 -9
- package/src/managers/dnf.js +3 -12
- package/src/managers/node.js +17 -8
- package/src/managers/npm.js +51 -45
- package/src/managers/php.js +17 -8
- package/src/managers/pip.js +24 -18
- package/src/managers/python.js +18 -8
- package/src/managers/winget.js +3 -13
- package/src/tools/webwork/index.js +132 -463
package/src/managers/base.js
CHANGED
|
@@ -81,13 +81,27 @@ export class PackageManager {
|
|
|
81
81
|
/**
|
|
82
82
|
* 查询包是否已安装
|
|
83
83
|
* @param {string} name - 包名
|
|
84
|
-
* @param {{ global?: boolean }} [opts] -
|
|
85
|
-
* @returns {Promise<InstalledResult>}
|
|
84
|
+
* @param {{ global?: boolean }} [opts] - 选项;global: true = 全局/系统作用域,false/缺省 = 项目/当前作用域
|
|
85
|
+
* @returns {Promise<InstalledResult>} - name/installed/version?/path?;单作用域管理器对不支持的作用域返回 { name, installed: false, hint }
|
|
86
86
|
*/
|
|
87
87
|
async isInstalled(name, opts) {
|
|
88
88
|
throw new Error(`${this.name}: isInstalled not implemented`)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 查询包在全局与项目双作用域的安装状态
|
|
93
|
+
* @param {string} name - 包名
|
|
94
|
+
* @param {{ proxy?: string }} [opts] - 选项
|
|
95
|
+
* @returns {Promise<{ name: string, global: InstalledResult, project: InstalledResult }>}
|
|
96
|
+
*/
|
|
97
|
+
async scopedInstalled(name, opts = {}) {
|
|
98
|
+
const [global, project] = await Promise.all([
|
|
99
|
+
this.isInstalled(name, { ...opts, global: true }),
|
|
100
|
+
this.isInstalled(name, { ...opts, global: false }),
|
|
101
|
+
])
|
|
102
|
+
return { name, global, project }
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* 查询包详情
|
|
93
107
|
* @param {string} name - 包名
|
|
@@ -110,6 +124,7 @@ export class PackageManager {
|
|
|
110
124
|
|
|
111
125
|
/**
|
|
112
126
|
* 列出所有已安装包
|
|
127
|
+
* // 仅运行时管理器(node/php/python)实现;包管理器不再实现此方法
|
|
113
128
|
* @param {{ global?: boolean }} [opts] - 选项
|
|
114
129
|
* @returns {Promise<InstalledItem[]>}
|
|
115
130
|
*/
|
package/src/managers/brew.js
CHANGED
|
@@ -299,6 +299,9 @@ export class BrewManager extends PackageManager {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
async isInstalled(name, opts = {}) {
|
|
302
|
+
if (opts.global === false) {
|
|
303
|
+
return { name, installed: false, hint: 'no project scope (system-level manager)' }
|
|
304
|
+
}
|
|
302
305
|
const out = this._exec(['list', '--versions', name], { allowNonZero: true })
|
|
303
306
|
const items = this._parseListVersions(out)
|
|
304
307
|
const hit = items.find((i) => i.name === name)
|
|
@@ -337,11 +340,6 @@ export class BrewManager extends PackageManager {
|
|
|
337
340
|
return { name, versions, registry }
|
|
338
341
|
}
|
|
339
342
|
|
|
340
|
-
async listInstalled(opts = {}) {
|
|
341
|
-
const out = this._exec(['list', '--versions'], { allowNonZero: true })
|
|
342
|
-
return this._parseListVersions(out)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
343
|
async listOutdated(opts = {}) {
|
|
346
344
|
const out = this._exec(['outdated', '--json=v2'], { allowNonZero: true, proxy: opts.proxy })
|
|
347
345
|
return this._parseOutdatedJson(out)
|
package/src/managers/bun.js
CHANGED
|
@@ -269,24 +269,6 @@ export class BunManager extends PackageManager {
|
|
|
269
269
|
return { name, versions, registry }
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
async listInstalled(opts = {}) {
|
|
273
|
-
const args = ['pm', 'ls']
|
|
274
|
-
if (opts.global) args.push('-g')
|
|
275
|
-
try {
|
|
276
|
-
const out = this._exec(args, { allowNonZero: true })
|
|
277
|
-
const items = []
|
|
278
|
-
for (const line of out.split('\n')) {
|
|
279
|
-
const m = line.match(/([^\s]+)@([\d.]+)/)
|
|
280
|
-
if (m) {
|
|
281
|
-
items.push({ name: m[1], version: m[2], ...(opts.global ? { global: true } : {}) })
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return items
|
|
285
|
-
} catch {
|
|
286
|
-
return []
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
272
|
async listOutdated(opts = {}) {
|
|
291
273
|
const args = ['outdated']
|
|
292
274
|
if (opts.global) args.push('-g')
|
package/src/managers/composer.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { execSync, spawnSync } from 'node:child_process'
|
|
13
13
|
import fs from 'node:fs'
|
|
14
|
+
import os from 'node:os'
|
|
14
15
|
import path from 'node:path'
|
|
15
16
|
import { PackageManager } from './base.js'
|
|
16
17
|
import { fetchText } from './http.js'
|
|
@@ -197,7 +198,71 @@ export class ComposerManager extends PackageManager {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/**
|
|
202
|
+
* 获取 composer 全局 home 目录
|
|
203
|
+
*
|
|
204
|
+
* 优先级:$COMPOSER_HOME → ~/.config/composer → ~/.composer(旧版)
|
|
205
|
+
* @returns {string}
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
_getComposerHome() {
|
|
209
|
+
if (process.env.COMPOSER_HOME) return process.env.COMPOSER_HOME
|
|
210
|
+
const xdg = path.join(os.homedir(), '.config', 'composer')
|
|
211
|
+
if (fs.existsSync(xdg)) return xdg
|
|
212
|
+
return path.join(os.homedir(), '.composer')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 读取 composer 全局 installed.json
|
|
217
|
+
* @returns {object[]|null}
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_readGlobalInstalledJson() {
|
|
221
|
+
const home = this._getComposerHome()
|
|
222
|
+
const p = path.join(home, 'vendor', 'composer', 'installed.json')
|
|
223
|
+
if (!fs.existsSync(p)) return null
|
|
224
|
+
try {
|
|
225
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
226
|
+
return Array.isArray(data.packages) ? data.packages : null
|
|
227
|
+
} catch {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 通过 PATH 查找 bin 路径(Linux: which / Windows: where)
|
|
234
|
+
* @param {string} name - bin 名
|
|
235
|
+
* @returns {string|null} bin 路径或 null
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
_whichBin(name) {
|
|
239
|
+
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`
|
|
240
|
+
try {
|
|
241
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
242
|
+
const lines = out.trim().split(/\r?\n/).filter(Boolean)
|
|
243
|
+
return lines[0] || null
|
|
244
|
+
} catch {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
200
249
|
async isInstalled(name, opts = {}) {
|
|
250
|
+
if (opts.global === true) {
|
|
251
|
+
const packages = this._readGlobalInstalledJson()
|
|
252
|
+
if (packages) {
|
|
253
|
+
const pkg = packages.find((p) => p.name === name)
|
|
254
|
+
if (pkg) {
|
|
255
|
+
return { name, installed: true, version: pkg.version || 'unknown', global: true }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const binPath = this._whichBin(name)
|
|
260
|
+
if (binPath) {
|
|
261
|
+
return { name, installed: true, global: true, path: binPath }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { name, installed: false, global: true }
|
|
265
|
+
}
|
|
201
266
|
const packages = this._readInstalledJson()
|
|
202
267
|
if (packages) {
|
|
203
268
|
const pkg = packages.find((p) => p.name === name)
|
|
@@ -233,15 +298,6 @@ export class ComposerManager extends PackageManager {
|
|
|
233
298
|
return { name, versions, registry }
|
|
234
299
|
}
|
|
235
300
|
|
|
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
301
|
async listOutdated(opts = {}) {
|
|
246
302
|
const out = this._exec(['outdated', '--format=json'], { allowNonZero: true, proxy: opts.proxy })
|
|
247
303
|
try {
|
package/src/managers/dnf.js
CHANGED
|
@@ -240,6 +240,9 @@ export class DnfManager extends PackageManager {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
async isInstalled(name, opts = {}) {
|
|
243
|
+
if (opts.global === false) {
|
|
244
|
+
return { name, installed: false, hint: 'no project scope (system-level manager)' }
|
|
245
|
+
}
|
|
243
246
|
const args = ['repoquery', '--installed', '--qf', '%{VERSION}-%{RELEASE}', name]
|
|
244
247
|
const out = this._exec(args, { allowNonZero: true })
|
|
245
248
|
const versions = this._parseLines(out)
|
|
@@ -316,18 +319,6 @@ export class DnfManager extends PackageManager {
|
|
|
316
319
|
return { name, versions, registry }
|
|
317
320
|
}
|
|
318
321
|
|
|
319
|
-
async listInstalled(opts = {}) {
|
|
320
|
-
const out = this._exec(['repoquery', '--installed', '--qf', '%{NAME} %{VERSION}-%{RELEASE}'], { allowNonZero: true })
|
|
321
|
-
const items = []
|
|
322
|
-
for (const line of this._parseLines(out)) {
|
|
323
|
-
const parts = line.split(/\s+/)
|
|
324
|
-
if (parts.length >= 2) {
|
|
325
|
-
items.push({ name: parts[0], version: parts[1] })
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return items
|
|
329
|
-
}
|
|
330
|
-
|
|
331
322
|
async listOutdated(opts = {}) {
|
|
332
323
|
const args = ['list', '--upgrades', ...this._proxyArgs(opts.proxy)]
|
|
333
324
|
const out = this._exec(args, { allowNonZero: true })
|
package/src/managers/node.js
CHANGED
|
@@ -68,6 +68,22 @@ export function installNvm(proxy) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 解析 nvm ls 输出为已安装版本列表
|
|
73
|
+
* @param {string} out
|
|
74
|
+
* @returns {{ name: string, version: string, current: boolean }[]}
|
|
75
|
+
*/
|
|
76
|
+
export function parseNvmInstalled(out) {
|
|
77
|
+
const items = []
|
|
78
|
+
for (const line of out.split('\n')) {
|
|
79
|
+
const m = line.match(/^(->)?\s*v(\d+\.\d+\.\d+)\b/)
|
|
80
|
+
if (m) {
|
|
81
|
+
items.push({ name: `v${m[2]}`, version: m[2], current: !!m[1] })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return items
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
export class NodeManager extends RuntimeManager {
|
|
72
88
|
constructor() {
|
|
73
89
|
super({
|
|
@@ -127,14 +143,7 @@ export class NodeManager extends RuntimeManager {
|
|
|
127
143
|
* @private
|
|
128
144
|
*/
|
|
129
145
|
_parseInstalled(out) {
|
|
130
|
-
|
|
131
|
-
for (const line of out.split('\n')) {
|
|
132
|
-
const m = line.match(/^(->)?\s*v(\d+\.\d+\.\d+)\b/)
|
|
133
|
-
if (m) {
|
|
134
|
-
items.push({ name: `v${m[2]}`, version: m[2], current: !!m[1] })
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return items
|
|
146
|
+
return parseNvmInstalled(out)
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
/**
|
package/src/managers/npm.js
CHANGED
|
@@ -290,8 +290,31 @@ export class NpmManager extends PackageManager {
|
|
|
290
290
|
return null
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* 通过 PATH 查找 bin 路径(Linux: which / Windows: where)
|
|
295
|
+
* @param {string} name - bin 名
|
|
296
|
+
* @returns {string|null} bin 路径或 null
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
_whichBin(name) {
|
|
300
|
+
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`
|
|
301
|
+
try {
|
|
302
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
303
|
+
const lines = out.trim().split(/\r?\n/).filter(Boolean)
|
|
304
|
+
return lines[0] || null
|
|
305
|
+
} catch {
|
|
306
|
+
return null
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
293
310
|
/**
|
|
294
311
|
* 查询全局安装的包
|
|
312
|
+
*
|
|
313
|
+
* 三段式检查:
|
|
314
|
+
* 1. npm list -g <name>(当前激活 node 全局)
|
|
315
|
+
* 2. npm root -g 目录直查(同上,兜底)
|
|
316
|
+
* 3. PATH 回退:which <name> → realpath → 推导全局 node_modules
|
|
317
|
+
* 覆盖通过其他 node 版本/路径安装的包(如非 nvm 管理的独立 node)
|
|
295
318
|
* @param {string} name - 包名
|
|
296
319
|
* @returns {Promise<InstalledResult>}
|
|
297
320
|
* @private
|
|
@@ -316,6 +339,30 @@ export class NpmManager extends PackageManager {
|
|
|
316
339
|
}
|
|
317
340
|
} catch {}
|
|
318
341
|
|
|
342
|
+
// PATH 回退:which <name> → realpath → 推导全局 node_modules
|
|
343
|
+
try {
|
|
344
|
+
const binPath = this._whichBin(name)
|
|
345
|
+
if (binPath) {
|
|
346
|
+
const realBin = fs.realpathSync(binPath)
|
|
347
|
+
const binDir = path.dirname(realBin)
|
|
348
|
+
const candidates = [
|
|
349
|
+
path.join(binDir, '..', 'package.json'),
|
|
350
|
+
path.join(binDir, '..', 'lib', 'node_modules', name, 'package.json'),
|
|
351
|
+
path.join(binDir, '..', 'node_modules', name, 'package.json')
|
|
352
|
+
]
|
|
353
|
+
for (const pkgPath of candidates) {
|
|
354
|
+
try {
|
|
355
|
+
if (fs.existsSync(pkgPath)) {
|
|
356
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
357
|
+
return { name, installed: true, version: pkg.version, path: path.dirname(pkgPath), global: true }
|
|
358
|
+
}
|
|
359
|
+
} catch {}
|
|
360
|
+
}
|
|
361
|
+
// 找到 bin 但推导不到 package.json(极端情况)
|
|
362
|
+
return { name, installed: true, global: true }
|
|
363
|
+
}
|
|
364
|
+
} catch {}
|
|
365
|
+
|
|
319
366
|
return { name, installed: false, global: true }
|
|
320
367
|
}
|
|
321
368
|
|
|
@@ -375,48 +422,6 @@ export class NpmManager extends PackageManager {
|
|
|
375
422
|
return { name, versions: [], registry }
|
|
376
423
|
}
|
|
377
424
|
|
|
378
|
-
async listInstalled(opts = {}) {
|
|
379
|
-
if (opts.global) {
|
|
380
|
-
try {
|
|
381
|
-
const data = this._execJson(['list', '-g', '--depth=0'])
|
|
382
|
-
const deps = data?.dependencies || {}
|
|
383
|
-
return Object.entries(deps)
|
|
384
|
-
.filter(([, info]) => info && info.version)
|
|
385
|
-
.map(([name, info]) => ({ name, version: info.version, global: true }))
|
|
386
|
-
} catch (err) {
|
|
387
|
-
throw new Error(`npm list -g failed: ${err.message}`)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const declared = this._readDeclaredDeps()
|
|
392
|
-
const workspaces = this._readWorkspacePackages()
|
|
393
|
-
const items = []
|
|
394
|
-
const seen = new Set()
|
|
395
|
-
|
|
396
|
-
for (const { name, version, dir } of workspaces) {
|
|
397
|
-
if (seen.has(name)) continue
|
|
398
|
-
seen.add(name)
|
|
399
|
-
const resolved = this._resolveInstalled(name)
|
|
400
|
-
items.push({
|
|
401
|
-
name,
|
|
402
|
-
version: resolved ? resolved.version : version,
|
|
403
|
-
path: resolved ? resolved.path : dir
|
|
404
|
-
})
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
for (const [name, declaredVersion] of Object.entries(declared)) {
|
|
408
|
-
if (seen.has(name)) continue
|
|
409
|
-
seen.add(name)
|
|
410
|
-
const resolved = this._resolveInstalled(name)
|
|
411
|
-
items.push({
|
|
412
|
-
name,
|
|
413
|
-
version: resolved ? resolved.version : declaredVersion
|
|
414
|
-
})
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return items
|
|
418
|
-
}
|
|
419
|
-
|
|
420
425
|
async listOutdated(opts = {}) {
|
|
421
426
|
try {
|
|
422
427
|
const args = ['outdated']
|
|
@@ -455,7 +460,8 @@ export class NpmManager extends PackageManager {
|
|
|
455
460
|
|
|
456
461
|
/**
|
|
457
462
|
* 构造 --dangerously-allow-all-scripts 参数(仅 npm >= 11 支持)
|
|
458
|
-
* npm < 11 默认放行所有脚本,无需此 flag;npm >= 11
|
|
463
|
+
* npm < 11 默认放行所有脚本,无需此 flag;npm >= 11 默认阻止,需显式放行。
|
|
464
|
+
* install/upgrade 始终追加,保持与 npm < 11 一致的脚本行为(postinstall 可执行)。
|
|
459
465
|
* @param {string} name - 包名(保留签名兼容,未使用)
|
|
460
466
|
* @returns {string[]}
|
|
461
467
|
* @private
|
|
@@ -470,7 +476,7 @@ export class NpmManager extends PackageManager {
|
|
|
470
476
|
if (opts.global) args.push('-g')
|
|
471
477
|
args.push(...this._proxyArgs(opts.proxy))
|
|
472
478
|
args.push(...this._registryArgs())
|
|
473
|
-
|
|
479
|
+
args.push(...this._allowScriptsArgs(name))
|
|
474
480
|
|
|
475
481
|
this._execInherit(args)
|
|
476
482
|
return { name, version: version || 'latest', action: 'installed' }
|
|
@@ -489,7 +495,7 @@ export class NpmManager extends PackageManager {
|
|
|
489
495
|
if (opts.global) args.push('-g')
|
|
490
496
|
args.push(...this._proxyArgs(opts.proxy))
|
|
491
497
|
args.push(...this._registryArgs())
|
|
492
|
-
|
|
498
|
+
args.push(...this._allowScriptsArgs(name))
|
|
493
499
|
|
|
494
500
|
this._execInherit(args)
|
|
495
501
|
return { name, version: 'latest', action: 'upgraded' }
|
package/src/managers/php.js
CHANGED
|
@@ -70,6 +70,22 @@ export function installPhpenv() {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 解析 phpenv versions 输出为已安装版本列表
|
|
75
|
+
* @param {string} out
|
|
76
|
+
* @returns {{ name: string, version: string, current: boolean }[]}
|
|
77
|
+
*/
|
|
78
|
+
export function parsePhpenvInstalled(out) {
|
|
79
|
+
const items = []
|
|
80
|
+
for (const line of out.split('\n')) {
|
|
81
|
+
const m = line.match(/^(\*)?\s+(\d+\.\d+\.\d+)\b/)
|
|
82
|
+
if (m) {
|
|
83
|
+
items.push({ name: m[2], version: m[2], current: !!m[1] })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return items
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
export class PhpManager extends RuntimeManager {
|
|
74
90
|
constructor() {
|
|
75
91
|
super({
|
|
@@ -134,14 +150,7 @@ export class PhpManager extends RuntimeManager {
|
|
|
134
150
|
* @private
|
|
135
151
|
*/
|
|
136
152
|
_parseInstalled(out) {
|
|
137
|
-
|
|
138
|
-
for (const line of out.split('\n')) {
|
|
139
|
-
const m = line.match(/^(\*)?\s+(\d+\.\d+\.\d+)\b/)
|
|
140
|
-
if (m) {
|
|
141
|
-
items.push({ name: m[2], version: m[2], current: !!m[1] })
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return items
|
|
153
|
+
return parsePhpenvInstalled(out)
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
/**
|
package/src/managers/pip.js
CHANGED
|
@@ -429,6 +429,23 @@ export class PipManager extends PackageManager {
|
|
|
429
429
|
return null
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* 通过 PATH 查找 bin 路径(Linux: which / Windows: where)
|
|
434
|
+
* @param {string} name - bin 名
|
|
435
|
+
* @returns {string|null} bin 路径或 null
|
|
436
|
+
* @private
|
|
437
|
+
*/
|
|
438
|
+
_whichBin(name) {
|
|
439
|
+
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`
|
|
440
|
+
try {
|
|
441
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
442
|
+
const lines = out.trim().split(/\r?\n/).filter(Boolean)
|
|
443
|
+
return lines[0] || null
|
|
444
|
+
} catch {
|
|
445
|
+
return null
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
432
449
|
async isInstalled(name, opts = {}) {
|
|
433
450
|
const info = this._parsePipShow(name, opts)
|
|
434
451
|
if (info) {
|
|
@@ -441,6 +458,13 @@ export class PipManager extends PackageManager {
|
|
|
441
458
|
}
|
|
442
459
|
}
|
|
443
460
|
|
|
461
|
+
if (opts.global) {
|
|
462
|
+
const binPath = this._whichBin(name)
|
|
463
|
+
if (binPath) {
|
|
464
|
+
return { name, installed: true, global: true, path: binPath }
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
444
468
|
return { name, installed: false, ...(opts.global ? { global: true } : {}) }
|
|
445
469
|
}
|
|
446
470
|
|
|
@@ -487,24 +511,6 @@ export class PipManager extends PackageManager {
|
|
|
487
511
|
return { name, versions: [], registry }
|
|
488
512
|
}
|
|
489
513
|
|
|
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
514
|
async listOutdated(opts = {}) {
|
|
509
515
|
try {
|
|
510
516
|
const args = ['list', '--outdated', '--format=json']
|
package/src/managers/python.js
CHANGED
|
@@ -67,6 +67,23 @@ export function installUv(proxy) {
|
|
|
67
67
|
return { action: 'installed', hint: 'Restart shell or ensure ~/.local/bin is in PATH' }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 解析 uv python list --only-installed 输出为已安装版本列表
|
|
72
|
+
* 输出格式: cpython-3.12.4-linux-x86_64-gnu /path/to/python
|
|
73
|
+
* @param {string} out
|
|
74
|
+
* @returns {{ name: string, version: string, current: boolean }[]}
|
|
75
|
+
*/
|
|
76
|
+
export function parseUvInstalled(out) {
|
|
77
|
+
const items = []
|
|
78
|
+
for (const line of out.split('\n')) {
|
|
79
|
+
const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
|
|
80
|
+
if (m) {
|
|
81
|
+
items.push({ name: m[1], version: m[1], current: false })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return items
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
export class PythonManager extends RuntimeManager {
|
|
71
88
|
constructor() {
|
|
72
89
|
super({
|
|
@@ -129,14 +146,7 @@ export class PythonManager extends RuntimeManager {
|
|
|
129
146
|
* @private
|
|
130
147
|
*/
|
|
131
148
|
_parseInstalled(out) {
|
|
132
|
-
|
|
133
|
-
for (const line of out.split('\n')) {
|
|
134
|
-
const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
|
|
135
|
-
if (m) {
|
|
136
|
-
items.push({ name: m[1], version: m[1], current: false })
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return items
|
|
149
|
+
return parseUvInstalled(out)
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
/**
|
package/src/managers/winget.js
CHANGED
|
@@ -175,6 +175,9 @@ export class WingetManager extends PackageManager {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
async isInstalled(name, opts = {}) {
|
|
178
|
+
if (opts.global === false) {
|
|
179
|
+
return { name, installed: false, hint: 'no project scope (system-level manager)' }
|
|
180
|
+
}
|
|
178
181
|
const out = this._exec(
|
|
179
182
|
['list', name, '--accept-source-agreements', '--disable-interactivity'],
|
|
180
183
|
{ allowNonZero: true }
|
|
@@ -218,19 +221,6 @@ export class WingetManager extends PackageManager {
|
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
async listInstalled(opts = {}) {
|
|
222
|
-
const out = this._exec(
|
|
223
|
-
['list', '--accept-source-agreements', '--disable-interactivity'],
|
|
224
|
-
{ allowNonZero: true }
|
|
225
|
-
)
|
|
226
|
-
return this._parseTable(out).map((r) => ({
|
|
227
|
-
name: r.Name || r.Id,
|
|
228
|
-
id: r.Id,
|
|
229
|
-
version: r.Version || 'unknown',
|
|
230
|
-
source: r.Source || '',
|
|
231
|
-
}))
|
|
232
|
-
}
|
|
233
|
-
|
|
234
224
|
async listOutdated(opts = {}) {
|
|
235
225
|
const out = this._exec(
|
|
236
226
|
['list', '--upgrade-available', '--accept-source-agreements', '--disable-interactivity'],
|