@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.
@@ -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
  */
@@ -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)
@@ -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')
@@ -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 {
@@ -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 })
@@ -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
- const items = []
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
  /**
@@ -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
- if (opts.allowScripts) args.push(...this._allowScriptsArgs(name))
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
- if (opts.allowScripts) args.push(...this._allowScriptsArgs(name))
498
+ args.push(...this._allowScriptsArgs(name))
493
499
 
494
500
  this._execInherit(args)
495
501
  return { name, version: 'latest', action: 'upgraded' }
@@ -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
- const items = []
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
  /**
@@ -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']
@@ -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
- const items = []
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
  /**
@@ -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'],