@wwkit/opm 1.0.8 → 1.0.9
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 +10 -17
- 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/pip.md +2 -6
- package/docs/opm.html +7 -20
- package/package.json +3 -3
- package/src/cli/groups/overlay.js +169 -0
- package/src/cli/groups/package.js +42 -18
- package/src/cli/index.js +2 -0
- package/src/config.json5 +1 -0
- package/src/harnesses/opencode/index.js +1 -1
- package/src/index.js +1 -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/npm.js +51 -45
- package/src/managers/pip.js +24 -18
- package/src/managers/winget.js +3 -13
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overlay 命令组 — 将本地开发源码覆盖到全局 npm 安装位置
|
|
3
|
+
*
|
|
4
|
+
* 用途:跳过发布流程,把最近修改的代码放到实际包安装位置用于实际测试。
|
|
5
|
+
*
|
|
6
|
+
* opm overlay 覆盖 cwd 包到全局安装
|
|
7
|
+
* opm overlay -f /path/to/pkg 指定源目录
|
|
8
|
+
* opm overlay -t /custom/node_modules 指定目标目录
|
|
9
|
+
* opm overlay --dry-run 预览将要复制的文件
|
|
10
|
+
*
|
|
11
|
+
* 逻辑:
|
|
12
|
+
* 1. 读取源目录 package.json 获取包名
|
|
13
|
+
* 2. 默认目标 = npm root -g,拼接 <globalRoot>/<pkgName>
|
|
14
|
+
* 3. 排除 node_modules/.git 等,逐文件复制(保留权限)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { execSync } from 'node:child_process'
|
|
20
|
+
import { parseFlags } from '../helpers/args.js'
|
|
21
|
+
import { output } from '../../formatter.js'
|
|
22
|
+
|
|
23
|
+
const EXCLUDE = new Set(['node_modules', '.git', '.DS_Store', 'Thumbs.db'])
|
|
24
|
+
|
|
25
|
+
export class OverlayGroup {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.name = 'overlay'
|
|
28
|
+
this.desc = 'Overlay local package source onto global npm install (bypass publish)'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async run(argv) {
|
|
32
|
+
const parsed = parseFlags(argv)
|
|
33
|
+
|
|
34
|
+
if (parsed.flags.help === 'true' || parsed.flags.h === 'true' || parsed.positional[0] === 'help') {
|
|
35
|
+
this.printHelp()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dryRun = parsed.flags['dry-run'] === 'true'
|
|
40
|
+
|
|
41
|
+
const from = path.resolve(parsed.flags.f || parsed.flags.from || process.cwd())
|
|
42
|
+
if (!fs.existsSync(from) || !fs.statSync(from).isDirectory()) {
|
|
43
|
+
output({ error: 'Source directory does not exist or is not a directory', from })
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
const srcPkgPath = path.join(from, 'package.json')
|
|
47
|
+
if (!fs.existsSync(srcPkgPath)) {
|
|
48
|
+
output({ error: 'No package.json in source directory root (not an npm project)', from, hint: 'Use -f/--to to specify a directory containing package.json' })
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pkg = JSON.parse(fs.readFileSync(srcPkgPath, 'utf8'))
|
|
53
|
+
const pkgName = pkg.name
|
|
54
|
+
if (!pkgName) {
|
|
55
|
+
output({ error: 'package.json has no name field', from })
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const to = parsed.flags.t || parsed.flags.to
|
|
60
|
+
let globalRoot
|
|
61
|
+
if (to) {
|
|
62
|
+
globalRoot = path.resolve(to)
|
|
63
|
+
if (path.basename(globalRoot) !== 'node_modules') {
|
|
64
|
+
output({ error: 'Target directory must end with "node_modules"', to: globalRoot, hint: 'Example: /usr/local/lib/node_modules' })
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
if (!fs.existsSync(globalRoot) || !fs.statSync(globalRoot).isDirectory()) {
|
|
68
|
+
output({ error: 'Target node_modules directory does not exist', to: globalRoot })
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
74
|
+
} catch {
|
|
75
|
+
output({ error: 'Failed to determine npm global root', hint: 'Use -t/--to to specify target directory' })
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const targetDir = path.join(globalRoot, pkgName)
|
|
81
|
+
if (!fs.existsSync(targetDir)) {
|
|
82
|
+
output({
|
|
83
|
+
error: 'Target package not found in global install',
|
|
84
|
+
package: pkgName,
|
|
85
|
+
target: targetDir,
|
|
86
|
+
hint: `Install first: npm install -g ${pkgName}`,
|
|
87
|
+
})
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const files = this._collectFiles(from)
|
|
92
|
+
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
output({
|
|
95
|
+
package: pkgName,
|
|
96
|
+
from,
|
|
97
|
+
to: targetDir,
|
|
98
|
+
dryRun: true,
|
|
99
|
+
fileCount: files.length,
|
|
100
|
+
files: files.map((f) => path.relative(from, f)),
|
|
101
|
+
})
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let copied = 0
|
|
106
|
+
for (const srcPath of files) {
|
|
107
|
+
const rel = path.relative(from, srcPath)
|
|
108
|
+
const destPath = path.join(targetDir, rel)
|
|
109
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
|
110
|
+
const stat = fs.statSync(srcPath)
|
|
111
|
+
fs.copyFileSync(srcPath, destPath)
|
|
112
|
+
fs.chmodSync(destPath, stat.mode)
|
|
113
|
+
copied++
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
output({
|
|
117
|
+
package: pkgName,
|
|
118
|
+
from,
|
|
119
|
+
to: targetDir,
|
|
120
|
+
fileCount: copied,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 递归收集源目录下所有文件(排除 EXCLUDE 集合)
|
|
126
|
+
* @param {string} dir
|
|
127
|
+
* @returns {string[]}
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
_collectFiles(dir) {
|
|
131
|
+
const result = []
|
|
132
|
+
const walk = (d) => {
|
|
133
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
134
|
+
if (EXCLUDE.has(entry.name)) continue
|
|
135
|
+
const full = path.join(d, entry.name)
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
walk(full)
|
|
138
|
+
} else {
|
|
139
|
+
result.push(full)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
walk(dir)
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
printHelp() {
|
|
148
|
+
console.log(`
|
|
149
|
+
Usage: opm overlay [options]
|
|
150
|
+
|
|
151
|
+
Overlay local package source onto the global npm install location.
|
|
152
|
+
Copies all files (except node_modules/.git) from source to target,
|
|
153
|
+
overwriting existing files. Useful for testing local changes without
|
|
154
|
+
publishing.
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
-f, --from <dir> Source package directory (default: current directory)
|
|
158
|
+
-t, --to <dir> Target node_modules directory (default: npm root -g)
|
|
159
|
+
--dry-run Preview files to copy without writing
|
|
160
|
+
-h, --help Show this help
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
opm overlay Overlay cwd package to global install
|
|
164
|
+
opm overlay -f /path/to/package Overlay specific package
|
|
165
|
+
opm overlay -t /custom/node_modules Overlay to custom target
|
|
166
|
+
opm overlay --dry-run Preview what would be copied
|
|
167
|
+
`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -50,14 +50,13 @@ const MANAGER_DESC = {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const ACTIONS = {
|
|
53
|
-
version: { desc: 'Show manager version' },
|
|
53
|
+
version: { desc: 'Show manager or package version' },
|
|
54
54
|
installed: { desc: 'Check if the manager itself is installed' },
|
|
55
55
|
registry: { desc: 'Query or set registry/mirror address' },
|
|
56
56
|
search: { desc: 'Check if a package is available in the source' },
|
|
57
|
-
view: { desc: 'Check
|
|
57
|
+
view: { desc: 'Check package in global and project scopes' },
|
|
58
58
|
info: { desc: 'Show package details' },
|
|
59
59
|
versions: { desc: 'List all available versions of a package' },
|
|
60
|
-
list: { desc: 'List all installed packages' },
|
|
61
60
|
outdated: { desc: 'List outdated packages' },
|
|
62
61
|
install: { desc: 'Install a package' },
|
|
63
62
|
uninstall: { desc: 'Uninstall a package' },
|
|
@@ -105,8 +104,18 @@ export class PackageGroup {
|
|
|
105
104
|
|
|
106
105
|
switch (action) {
|
|
107
106
|
case 'version': {
|
|
108
|
-
const
|
|
109
|
-
|
|
107
|
+
const name = positional[0]
|
|
108
|
+
if (name) {
|
|
109
|
+
const result = await manager.scopedInstalled(name, { proxy: opts.proxy })
|
|
110
|
+
output({
|
|
111
|
+
name: result.name,
|
|
112
|
+
global: { installed: result.global.installed, ...(result.global.version ? { version: result.global.version } : {}) },
|
|
113
|
+
project: { installed: result.project.installed, ...(result.project.version ? { version: result.project.version } : {}) },
|
|
114
|
+
})
|
|
115
|
+
} else {
|
|
116
|
+
const result = await manager.getVersion()
|
|
117
|
+
output((result.version || '').replace(/^v/, ''))
|
|
118
|
+
}
|
|
110
119
|
break
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -151,10 +160,10 @@ export class PackageGroup {
|
|
|
151
160
|
case 'view': {
|
|
152
161
|
const name = positional[0]
|
|
153
162
|
if (!name) {
|
|
154
|
-
console.error(`Usage: opm ${this.name} view <name
|
|
163
|
+
console.error(`Usage: opm ${this.name} view <name>`)
|
|
155
164
|
process.exit(1)
|
|
156
165
|
}
|
|
157
|
-
const result = await manager.
|
|
166
|
+
const result = await manager.scopedInstalled(name, { proxy: opts.proxy })
|
|
158
167
|
output(result)
|
|
159
168
|
break
|
|
160
169
|
}
|
|
@@ -181,12 +190,6 @@ export class PackageGroup {
|
|
|
181
190
|
break
|
|
182
191
|
}
|
|
183
192
|
|
|
184
|
-
case 'list': {
|
|
185
|
-
const result = await manager.listInstalled({ global: !!opts.global })
|
|
186
|
-
output(result)
|
|
187
|
-
break
|
|
188
|
-
}
|
|
189
|
-
|
|
190
193
|
case 'outdated': {
|
|
191
194
|
const result = await manager.listOutdated({ global: !!opts.global, proxy: opts.proxy })
|
|
192
195
|
output(result)
|
|
@@ -254,16 +257,15 @@ export class PackageGroup {
|
|
|
254
257
|
Usage: opm ${this.name} <action> [args] [options]
|
|
255
258
|
|
|
256
259
|
Actions:
|
|
257
|
-
version
|
|
260
|
+
version [name] Show manager version, or package version in global+project scopes
|
|
258
261
|
installed Check if ${this.name} itself is installed
|
|
259
262
|
registry Show active registry (from opm config)
|
|
260
263
|
registry set <url|preset> Set active registry (writes opm config)
|
|
261
264
|
registry presets List available mirror presets
|
|
262
265
|
search <name> [version] Check package availability in source
|
|
263
|
-
view <name>
|
|
266
|
+
view <name> Show package in global and project scopes
|
|
264
267
|
info <name> Show package details
|
|
265
268
|
versions <name> List all available versions
|
|
266
|
-
list [-g] List all installed packages
|
|
267
269
|
outdated [-g] List outdated packages
|
|
268
270
|
install <name> [version] Install a package
|
|
269
271
|
uninstall <name> Uninstall a package
|
|
@@ -285,7 +287,6 @@ Examples:
|
|
|
285
287
|
opm ${this.name} view <name>
|
|
286
288
|
opm ${this.name} info <name>
|
|
287
289
|
opm ${this.name} versions <name>
|
|
288
|
-
opm ${this.name} list
|
|
289
290
|
opm ${this.name} outdated
|
|
290
291
|
opm ${this.name} install <name>
|
|
291
292
|
opm ${this.name} uninstall <name>
|
|
@@ -363,7 +364,7 @@ export class RuntimeGroup extends PackageGroup {
|
|
|
363
364
|
return
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
const runtimeActions = new Set(['use', 'current', 'default', 'installed', 'dir', 'install', 'upgrade', 'search', 'versions'])
|
|
367
|
+
const runtimeActions = new Set(['use', 'current', 'default', 'installed', 'dir', 'install', 'upgrade', 'search', 'versions', 'version', 'view', 'list'])
|
|
367
368
|
if (runtimeActions.has(action)) {
|
|
368
369
|
const manager = MANAGERS[this.name]()
|
|
369
370
|
const parsed = parseFlags(rest)
|
|
@@ -465,6 +466,29 @@ export class RuntimeGroup extends PackageGroup {
|
|
|
465
466
|
break
|
|
466
467
|
}
|
|
467
468
|
|
|
469
|
+
case 'version': {
|
|
470
|
+
const result = await manager.getVersion()
|
|
471
|
+
output((result.version || '').replace(/^v/, ''))
|
|
472
|
+
break
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
case 'view': {
|
|
476
|
+
const version = positional[0]
|
|
477
|
+
if (!version) {
|
|
478
|
+
console.error(`Usage: opm ${this.name} view <version>`)
|
|
479
|
+
process.exit(1)
|
|
480
|
+
}
|
|
481
|
+
const result = await manager.isInstalled(version, { global: false })
|
|
482
|
+
output(result)
|
|
483
|
+
break
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
case 'list': {
|
|
487
|
+
const result = await manager.listInstalled({ global: false })
|
|
488
|
+
output(result)
|
|
489
|
+
break
|
|
490
|
+
}
|
|
491
|
+
|
|
468
492
|
default:
|
|
469
493
|
return super._dispatch(manager, action, parsed, opts)
|
|
470
494
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { DockerGroup } from './groups/docker.js'
|
|
|
20
20
|
import { EnsureGroup } from './groups/ensure.js'
|
|
21
21
|
import { PingGroup } from './groups/ping.js'
|
|
22
22
|
import { ProcGroup } from './groups/proc.js'
|
|
23
|
+
import { OverlayGroup } from './groups/overlay.js'
|
|
23
24
|
import { FtpGroup } from '../tools/ftp/index.js'
|
|
24
25
|
import { GitGroup } from '../tools/git/index.js'
|
|
25
26
|
import { ShareGroup } from '../tools/share/index.js'
|
|
@@ -62,6 +63,7 @@ const GROUPS = {
|
|
|
62
63
|
ensure: new EnsureGroup(),
|
|
63
64
|
ping: new PingGroup(),
|
|
64
65
|
proc: new ProcGroup(),
|
|
66
|
+
overlay: new OverlayGroup(),
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
class CLI {
|
package/src/config.json5
CHANGED
|
@@ -21,7 +21,7 @@ const shell = new Shell()
|
|
|
21
21
|
/**
|
|
22
22
|
* opencode 专属环境变量名
|
|
23
23
|
*/
|
|
24
|
-
const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG_CONTENT']
|
|
24
|
+
const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG_CONTENT', 'OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS']
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* 查找 opencode 配置文件路径(优先 .jsonc,其次 .json)
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { WslGroup } from './cli/groups/wsl.js'
|
|
|
21
21
|
export { DockerGroup } from './cli/groups/docker.js'
|
|
22
22
|
export { PingGroup } from './cli/groups/ping.js'
|
|
23
23
|
export { ProcGroup } from './cli/groups/proc.js'
|
|
24
|
+
export { OverlayGroup } from './cli/groups/overlay.js'
|
|
24
25
|
export { FtpGroup } from './tools/ftp/index.js'
|
|
25
26
|
export { GitGroup } from './tools/git/index.js'
|
|
26
27
|
export { ShareGroup } from './tools/share/index.js'
|
package/src/managers/apt.js
CHANGED
|
@@ -249,6 +249,9 @@ export class AptManager extends PackageManager {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
async isInstalled(name, opts = {}) {
|
|
252
|
+
if (opts.global === false) {
|
|
253
|
+
return { name, installed: false, hint: 'no project scope (system-level manager)' }
|
|
254
|
+
}
|
|
252
255
|
const args = ['query', '-W', '-f=${Status} ${Version}', name]
|
|
253
256
|
const out = this._exec(args, { allowNonZero: true })
|
|
254
257
|
const m = out.trim().match(/^install ok installed\s+(\S+)$/)
|
|
@@ -323,18 +326,6 @@ export class AptManager extends PackageManager {
|
|
|
323
326
|
return { name, versions, registry }
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
async listInstalled(opts = {}) {
|
|
327
|
-
const out = this._exec(['list', '--installed'], { allowNonZero: true })
|
|
328
|
-
const items = []
|
|
329
|
-
for (const line of this._parseLines(out)) {
|
|
330
|
-
const m = line.match(/^(\S+?)\/(?:\S+,)?now\s+(\S+)\s+(\S+)\s+\[/)
|
|
331
|
-
if (m) {
|
|
332
|
-
items.push({ name: m[1], version: m[2] })
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return items
|
|
336
|
-
}
|
|
337
|
-
|
|
338
329
|
async listOutdated(opts = {}) {
|
|
339
330
|
const out = this._exec(['list', '--upgradable', ...this._proxyArgs(opts.proxy)], { allowNonZero: true })
|
|
340
331
|
const items = []
|
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 })
|