@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.
Files changed (62) hide show
  1. package/README.md +381 -0
  2. package/bin/index.js +9 -0
  3. package/docs/managers/apt.md +45 -0
  4. package/docs/managers/brew.md +92 -0
  5. package/docs/managers/bun.md +50 -0
  6. package/docs/managers/cft.md +57 -0
  7. package/docs/managers/composer.md +96 -0
  8. package/docs/managers/config.md +32 -0
  9. package/docs/managers/dnf.md +46 -0
  10. package/docs/managers/node.md +49 -0
  11. package/docs/managers/npm.md +100 -0
  12. package/docs/managers/nvm.md +34 -0
  13. package/docs/managers/php.md +48 -0
  14. package/docs/managers/phpenv.md +34 -0
  15. package/docs/managers/pip.md +98 -0
  16. package/docs/managers/python.md +53 -0
  17. package/docs/managers/uv.md +27 -0
  18. package/docs/opm.html +570 -0
  19. package/docs/packages/opencode.md +41 -0
  20. package/package.json +51 -0
  21. package/scripts/postinstall.js +7 -0
  22. package/src/binaries/base.js +508 -0
  23. package/src/binaries/cft/index.js +35 -0
  24. package/src/binaries/depchecker.js +172 -0
  25. package/src/binaries/exefinder.js +110 -0
  26. package/src/binaries/platform.js +17 -0
  27. package/src/cli/doc.js +335 -0
  28. package/src/cli/groups/config.js +78 -0
  29. package/src/cli/groups/ensure.js +96 -0
  30. package/src/cli/groups/homebrew.js +73 -0
  31. package/src/cli/groups/nvm.js +73 -0
  32. package/src/cli/groups/package.js +514 -0
  33. package/src/cli/groups/phpenv.js +89 -0
  34. package/src/cli/groups/uv.js +73 -0
  35. package/src/cli/groups/version-manager.js +186 -0
  36. package/src/cli/helpers/args.js +49 -0
  37. package/src/cli/index.js +129 -0
  38. package/src/config.js +133 -0
  39. package/src/config.json5 +158 -0
  40. package/src/formatter.js +13 -0
  41. package/src/harnesses/base.js +205 -0
  42. package/src/harnesses/opencode/clear.js +239 -0
  43. package/src/harnesses/opencode/db.js +39 -0
  44. package/src/harnesses/opencode/index.js +46 -0
  45. package/src/harnesses/opencode/sqlite.js +200 -0
  46. package/src/index.js +46 -0
  47. package/src/managers/apt.js +377 -0
  48. package/src/managers/base.js +167 -0
  49. package/src/managers/brew.js +374 -0
  50. package/src/managers/bun.js +342 -0
  51. package/src/managers/composer.js +280 -0
  52. package/src/managers/dnf.js +370 -0
  53. package/src/managers/http.js +432 -0
  54. package/src/managers/node.js +150 -0
  55. package/src/managers/npm.js +473 -0
  56. package/src/managers/php.js +157 -0
  57. package/src/managers/pip.js +555 -0
  58. package/src/managers/python.js +180 -0
  59. package/src/managers/runtime.js +309 -0
  60. package/src/managers/winget.js +268 -0
  61. package/src/presets.js +24 -0
  62. 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
+ }