@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnf 包管理器实现
|
|
3
|
+
*
|
|
4
|
+
* 通过执行 dnf CLI 命令实现统一接口(Fedora / RHEL / 华为云 EulerOS 等 rpm 系发行版)。
|
|
5
|
+
* dnf 是系统级包管理器,所有操作均作用于系统(无用户级/local 概念)。
|
|
6
|
+
*
|
|
7
|
+
* 源查询使用 dnf repoquery,本地查询使用 dnf repoquery --installed / dnf info。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { PackageManager } from './base.js'
|
|
14
|
+
import { getActiveRegistry, setActiveRegistry } from '../config.js'
|
|
15
|
+
|
|
16
|
+
const REPO_DIR = '/etc/yum.repos.d'
|
|
17
|
+
|
|
18
|
+
const ARCH_SUFFIX = /\.(x86_64|noarch|aarch64|i686|armv7hl|armv7hnl|ppc64le|s390x|riscv64|src)$/
|
|
19
|
+
|
|
20
|
+
const INFO_KEYS = new Set([
|
|
21
|
+
'name', 'epoch', 'version', 'release', 'architecture', 'size', 'source',
|
|
22
|
+
'repository', 'from repo', 'summary', 'url', 'license', 'description'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export class DnfManager extends PackageManager {
|
|
26
|
+
constructor() {
|
|
27
|
+
super()
|
|
28
|
+
this.name = 'dnf'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 对包含特殊字符的参数进行 shell 转义(单引号包裹)
|
|
33
|
+
* @param {string} arg
|
|
34
|
+
* @returns {string}
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
_quoteArg(arg) {
|
|
38
|
+
if (arg && /[\s'"$`\\]/.test(arg)) {
|
|
39
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`
|
|
40
|
+
}
|
|
41
|
+
return arg
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 执行 dnf 命令,返回 stdout
|
|
46
|
+
* @param {string[]} args
|
|
47
|
+
* @param {object} [opts]
|
|
48
|
+
* @returns {string}
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
_exec(args, opts = {}) {
|
|
52
|
+
const cmd = ['dnf', ...args].map((a) => this._quoteArg(a)).join(' ')
|
|
53
|
+
try {
|
|
54
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts })
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (opts.allowNonZero) {
|
|
57
|
+
return err.stdout ? err.stdout.toString() : ''
|
|
58
|
+
}
|
|
59
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : ''
|
|
60
|
+
if (stderr && !stderr.startsWith('Last metadata')) {
|
|
61
|
+
throw new Error(stderr)
|
|
62
|
+
}
|
|
63
|
+
return err.stdout ? err.stdout.toString() : ''
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 执行 dnf 命令,输出直接继承到终端(用于 install/remove/upgrade)
|
|
69
|
+
* @param {string[]} args
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
_execInherit(args) {
|
|
73
|
+
const result = spawnSync('dnf', args, { stdio: 'inherit', shell: true })
|
|
74
|
+
if (result.error) {
|
|
75
|
+
throw new Error(result.error.message)
|
|
76
|
+
}
|
|
77
|
+
if (result.status !== 0) {
|
|
78
|
+
throw new Error(`dnf ${args.join(' ')} exited with code ${result.status}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 构造 dnf proxy 参数
|
|
84
|
+
* dnf 支持全局 --proxy 选项
|
|
85
|
+
* @param {string} [proxy] - 代理地址
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
_proxyArgs(proxy) {
|
|
90
|
+
return proxy ? ['--proxy', proxy] : []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 过滤 dnf 输出中的噪音行,返回有意义的行
|
|
95
|
+
* @param {string} out
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
_parseLines(out) {
|
|
100
|
+
const lines = []
|
|
101
|
+
for (const line of out.split('\n')) {
|
|
102
|
+
const trimmed = line.trim()
|
|
103
|
+
if (!trimmed) continue
|
|
104
|
+
if (trimmed.startsWith('Last metadata')) continue
|
|
105
|
+
if (trimmed === 'Installed Packages' || trimmed === 'Available Packages' || trimmed === 'Upgraded Packages') continue
|
|
106
|
+
lines.push(trimmed)
|
|
107
|
+
}
|
|
108
|
+
return lines
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 读取启用的 yum/dnf 仓库(不调用 dnf,纯读配置文件)
|
|
113
|
+
*
|
|
114
|
+
* 解析 /etc/yum.repos.d/*.repo,提取 [id] + enabled + baseurl/metalink/mirrorlist。
|
|
115
|
+
*
|
|
116
|
+
* @param {string} [dir] - 仓库目录(默认 /etc/yum.repos.d,测试可注入临时目录)
|
|
117
|
+
* @returns {{ id: string, url: string }[]}
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_readEnabledRepos(dir = REPO_DIR) {
|
|
121
|
+
const repos = []
|
|
122
|
+
let files = []
|
|
123
|
+
try {
|
|
124
|
+
files = fs.readdirSync(dir)
|
|
125
|
+
} catch {
|
|
126
|
+
return repos
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
if (!file.endsWith('.repo')) continue
|
|
131
|
+
let text
|
|
132
|
+
try {
|
|
133
|
+
text = fs.readFileSync(path.join(dir, file), 'utf8')
|
|
134
|
+
} catch {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let current = null
|
|
139
|
+
for (const rawLine of text.split('\n')) {
|
|
140
|
+
const line = rawLine.trim()
|
|
141
|
+
if (line.startsWith('[') && line.endsWith(']')) {
|
|
142
|
+
current = { id: line.slice(1, -1), url: '' }
|
|
143
|
+
repos.push(current)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (!current) continue
|
|
147
|
+
const eq = line.indexOf('=')
|
|
148
|
+
if (eq < 0) continue
|
|
149
|
+
const key = line.slice(0, eq).trim().toLowerCase()
|
|
150
|
+
const val = line.slice(eq + 1).trim()
|
|
151
|
+
if (key === 'enabled' && val === '0') {
|
|
152
|
+
current.enabled = false
|
|
153
|
+
} else if (key === 'baseurl') {
|
|
154
|
+
current.url = val
|
|
155
|
+
} else if (key === 'metalink' && !current.url) {
|
|
156
|
+
current.url = `metalink:${val}`
|
|
157
|
+
} else if (key === 'mirrorlist' && !current.url) {
|
|
158
|
+
current.url = `mirrorlist:${val}`
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return repos.filter((r) => r.enabled !== false && r.url)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 查询某个包在所有启用仓库中的可用版本(升序)
|
|
168
|
+
* @param {string} name - 包名
|
|
169
|
+
* @param {{ proxy?: string }} [opts]
|
|
170
|
+
* @returns {string[]}
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
_availableVersions(name, opts = {}) {
|
|
174
|
+
const args = ['repoquery', '--available', '--qf', '%{VERSION}-%{RELEASE}', name, ...this._proxyArgs(opts.proxy)]
|
|
175
|
+
const out = this._exec(args, { allowNonZero: true })
|
|
176
|
+
return this._parseLines(out)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getVersion() {
|
|
180
|
+
try {
|
|
181
|
+
const out = this._exec(['--version'], { allowNonZero: true })
|
|
182
|
+
const firstLine = out.trim().split('\n')[0] || ''
|
|
183
|
+
const match = firstLine.match(/(\d+(?:\.\d+)+)/)
|
|
184
|
+
if (match) {
|
|
185
|
+
return { name: 'dnf', installed: true, version: match[1] }
|
|
186
|
+
}
|
|
187
|
+
if (firstLine) {
|
|
188
|
+
return { name: 'dnf', installed: true, version: firstLine }
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
return { name: 'dnf', version: '', installed: false, hint: 'dnf is the package manager for Fedora / RHEL-based systems' }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getRegistry() {
|
|
195
|
+
const registry = getActiveRegistry('dnf') || '(no active registry in opm config)'
|
|
196
|
+
return { registry }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async setRegistry(url) {
|
|
200
|
+
const resolved = setActiveRegistry('dnf', url)
|
|
201
|
+
const repoFile = path.join(REPO_DIR, 'opm.repo')
|
|
202
|
+
const content = [
|
|
203
|
+
'[opm]',
|
|
204
|
+
'name=opm mirror',
|
|
205
|
+
`baseurl=${resolved}`,
|
|
206
|
+
'enabled=1',
|
|
207
|
+
'gpgcheck=0',
|
|
208
|
+
''
|
|
209
|
+
].join('\n')
|
|
210
|
+
try {
|
|
211
|
+
fs.writeFileSync(repoFile, content, 'utf8')
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw new Error(`failed to write ${repoFile}: ${err.message}`)
|
|
214
|
+
}
|
|
215
|
+
return { registry: resolved }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async searchPackage(name, version, opts = {}) {
|
|
219
|
+
const { registry } = await this.getRegistry()
|
|
220
|
+
const versions = this._availableVersions(name, opts)
|
|
221
|
+
|
|
222
|
+
if (versions.length === 0) {
|
|
223
|
+
if (version) {
|
|
224
|
+
return { name, version, exists: false, available: false, registry }
|
|
225
|
+
}
|
|
226
|
+
return { name, exists: false, registry }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (version) {
|
|
230
|
+
return { name, version, exists: true, available: versions.includes(version), registry }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
name,
|
|
235
|
+
exists: true,
|
|
236
|
+
latest: versions[versions.length - 1],
|
|
237
|
+
versions,
|
|
238
|
+
registry
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async isInstalled(name, opts = {}) {
|
|
243
|
+
const args = ['repoquery', '--installed', '--qf', '%{VERSION}-%{RELEASE}', name]
|
|
244
|
+
const out = this._exec(args, { allowNonZero: true })
|
|
245
|
+
const versions = this._parseLines(out)
|
|
246
|
+
if (versions.length > 0) {
|
|
247
|
+
return { name, installed: true, version: versions[versions.length - 1] }
|
|
248
|
+
}
|
|
249
|
+
return { name, installed: false }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 解析 dnf info 输出为字段映射
|
|
254
|
+
* @param {string} out
|
|
255
|
+
* @returns {Record<string, string>|null}
|
|
256
|
+
* @private
|
|
257
|
+
*/
|
|
258
|
+
_parseInfo(out) {
|
|
259
|
+
const info = {}
|
|
260
|
+
let descriptionLines = []
|
|
261
|
+
let inDescription = false
|
|
262
|
+
|
|
263
|
+
for (const rawLine of out.split('\n')) {
|
|
264
|
+
const line = rawLine.trim()
|
|
265
|
+
const idx = line.indexOf(':')
|
|
266
|
+
if (idx > 0) {
|
|
267
|
+
const key = line.slice(0, idx).trim().toLowerCase()
|
|
268
|
+
if (INFO_KEYS.has(key)) {
|
|
269
|
+
const val = line.slice(idx + 1).trim()
|
|
270
|
+
if (key === 'description') {
|
|
271
|
+
descriptionLines = [val]
|
|
272
|
+
inDescription = true
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
info[key] = val
|
|
276
|
+
inDescription = false
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (inDescription && line) {
|
|
281
|
+
descriptionLines.push(line)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (descriptionLines.length > 0) {
|
|
286
|
+
info.description = descriptionLines.join(' ')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return info.name ? info : null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async packageInfo(name, opts = {}) {
|
|
293
|
+
const out = this._exec(['info', name, ...this._proxyArgs(opts.proxy)], { allowNonZero: true })
|
|
294
|
+
const info = this._parseInfo(out)
|
|
295
|
+
if (!info) {
|
|
296
|
+
return { name, description: '(not found)' }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const epoch = info.epoch && info.epoch !== '0' ? `${info.epoch}:` : ''
|
|
300
|
+
const latest = `${epoch}${info.version || ''}${info.release ? '-' + info.release : ''}`
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
name,
|
|
304
|
+
description: info.summary || info.description || '',
|
|
305
|
+
latest,
|
|
306
|
+
homepage: info.url || '',
|
|
307
|
+
license: info.license || '',
|
|
308
|
+
author: '',
|
|
309
|
+
repository: info.repository || ''
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async packageVersions(name, opts = {}) {
|
|
314
|
+
const { registry } = await this.getRegistry()
|
|
315
|
+
const versions = this._availableVersions(name, opts)
|
|
316
|
+
return { name, versions, registry }
|
|
317
|
+
}
|
|
318
|
+
|
|
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
|
+
async listOutdated(opts = {}) {
|
|
332
|
+
const args = ['list', '--upgrades', ...this._proxyArgs(opts.proxy)]
|
|
333
|
+
const out = this._exec(args, { allowNonZero: true })
|
|
334
|
+
const items = []
|
|
335
|
+
for (const line of this._parseLines(out)) {
|
|
336
|
+
const parts = line.split(/\s+/)
|
|
337
|
+
if (parts.length >= 3 && parts[1].includes('-')) {
|
|
338
|
+
items.push({
|
|
339
|
+
name: parts[0].replace(ARCH_SUFFIX, ''),
|
|
340
|
+
current: parts[1],
|
|
341
|
+
latest: parts[2]
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return items
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async install(name, version, opts = {}) {
|
|
349
|
+
const spec = version ? `${name}-${version}` : name
|
|
350
|
+
const args = ['install', '-y', spec, ...this._proxyArgs(opts.proxy)]
|
|
351
|
+
this._execInherit(args)
|
|
352
|
+
return { name, version: version || 'latest', action: 'installed' }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async uninstall(name, opts = {}) {
|
|
356
|
+
this._execInherit(['remove', '-y', name])
|
|
357
|
+
return { name, action: 'uninstalled' }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async upgrade(name, opts = {}) {
|
|
361
|
+
const args = ['upgrade', '-y', name, ...this._proxyArgs(opts.proxy)]
|
|
362
|
+
this._execInherit(args)
|
|
363
|
+
return { name, version: 'latest', action: 'upgraded' }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async cleanCache() {
|
|
367
|
+
this._execInherit(['clean', 'all'])
|
|
368
|
+
return { action: 'cache cleaned' }
|
|
369
|
+
}
|
|
370
|
+
}
|