@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,167 @@
1
+ /**
2
+ * 包管理器基类
3
+ *
4
+ * 定义统一的包管理操作接口,各具体管理器(npm/pip 等)继承此类并实现具体逻辑。
5
+ *
6
+ * @typedef {Object} SearchResult
7
+ * @property {string} name - 包名
8
+ * @property {boolean} exists - 是否存在
9
+ * @property {string} [latest] - 最新版本(无版本查询时)
10
+ * @property {string} [version] - 查询的版本(有版本查询时)
11
+ * @property {boolean} [available] - 指定版本是否可用(有版本查询时)
12
+ * @property {string[]} [versions] - 所有可用版本列表
13
+ * @property {string} registry - 查询的镜像地址
14
+ *
15
+ * @typedef {Object} InstalledResult
16
+ * @property {string} name - 包名
17
+ * @property {boolean} installed - 是否已安装
18
+ * @property {string} [version] - 已安装版本
19
+ * @property {string} [path] - 安装路径
20
+ *
21
+ * @typedef {Object} PackageInfo
22
+ * @property {string} name - 包名
23
+ * @property {string} [description] - 描述
24
+ * @property {string} [latest] - 最新版本
25
+ * @property {string} [homepage] - 主页
26
+ * @property {string} [license] - 许可证
27
+ * @property {string} [author] - 作者
28
+ * @property {string} [repository] - 仓库地址
29
+ *
30
+ * @typedef {Object} InstalledItem
31
+ * @property {string} name - 包名
32
+ * @property {string} version - 版本
33
+ *
34
+ * @typedef {Object} OutdatedItem
35
+ * @property {string} name - 包名
36
+ * @property {string} current - 当前版本
37
+ * @property {string} [wanted] - 期望版本
38
+ * @property {string} latest - 最新版本
39
+ */
40
+ export class PackageManager {
41
+ constructor() {
42
+ this.name = 'base'
43
+ }
44
+
45
+ /**
46
+ * 查询管理器本身版本
47
+ * @returns {Promise<{ name: string, installed: boolean, version?: string, hint?: string }>}
48
+ */
49
+ async getVersion() {
50
+ throw new Error(`${this.name}: getVersion not implemented`)
51
+ }
52
+
53
+ /**
54
+ * 查询当前镜像地址
55
+ * @returns {Promise<{ registry: string }>}
56
+ */
57
+ async getRegistry() {
58
+ throw new Error(`${this.name}: getRegistry not implemented`)
59
+ }
60
+
61
+ /**
62
+ * 设置镜像地址
63
+ * @param {string} url - 镜像 URL 或预设名
64
+ * @returns {Promise<{ registry: string }>}
65
+ */
66
+ async setRegistry(url) {
67
+ throw new Error(`${this.name}: setRegistry not implemented`)
68
+ }
69
+
70
+ /**
71
+ * 查询包在源是否可用
72
+ * @param {string} name - 包名
73
+ * @param {string} [version] - 版本号(可选)
74
+ * @param {{ proxy?: string }} [opts] - 选项
75
+ * @returns {Promise<SearchResult>}
76
+ */
77
+ async searchPackage(name, version, opts) {
78
+ throw new Error(`${this.name}: searchPackage not implemented`)
79
+ }
80
+
81
+ /**
82
+ * 查询包是否已安装
83
+ * @param {string} name - 包名
84
+ * @param {{ global?: boolean }} [opts] - 选项
85
+ * @returns {Promise<InstalledResult>}
86
+ */
87
+ async isInstalled(name, opts) {
88
+ throw new Error(`${this.name}: isInstalled not implemented`)
89
+ }
90
+
91
+ /**
92
+ * 查询包详情
93
+ * @param {string} name - 包名
94
+ * @param {{ proxy?: string }} [opts] - 选项
95
+ * @returns {Promise<PackageInfo>}
96
+ */
97
+ async packageInfo(name, opts) {
98
+ throw new Error(`${this.name}: packageInfo not implemented`)
99
+ }
100
+
101
+ /**
102
+ * 列出包的所有可用版本
103
+ * @param {string} name - 包名
104
+ * @param {{ proxy?: string }} [opts] - 选项
105
+ * @returns {Promise<{ name: string, versions: string[], registry: string }>}
106
+ */
107
+ async packageVersions(name, opts) {
108
+ throw new Error(`${this.name}: packageVersions not implemented`)
109
+ }
110
+
111
+ /**
112
+ * 列出所有已安装包
113
+ * @param {{ global?: boolean }} [opts] - 选项
114
+ * @returns {Promise<InstalledItem[]>}
115
+ */
116
+ async listInstalled(opts) {
117
+ throw new Error(`${this.name}: listInstalled not implemented`)
118
+ }
119
+
120
+ /**
121
+ * 列出过期包
122
+ * @param {{ global?: boolean, proxy?: string }} [opts] - 选项
123
+ * @returns {Promise<OutdatedItem[]>}
124
+ */
125
+ async listOutdated(opts) {
126
+ throw new Error(`${this.name}: listOutdated not implemented`)
127
+ }
128
+
129
+ /**
130
+ * 安装包
131
+ * @param {string} name - 包名
132
+ * @param {string} [version] - 版本号
133
+ * @param {{ global?: boolean, proxy?: string }} [opts] - 选项
134
+ * @returns {Promise<{ name: string, version: string, action: string }>}
135
+ */
136
+ async install(name, version, opts) {
137
+ throw new Error(`${this.name}: install not implemented`)
138
+ }
139
+
140
+ /**
141
+ * 卸载包
142
+ * @param {string} name - 包名
143
+ * @param {{ global?: boolean }} [opts] - 选项
144
+ * @returns {Promise<{ name: string, action: string }>}
145
+ */
146
+ async uninstall(name, opts) {
147
+ throw new Error(`${this.name}: uninstall not implemented`)
148
+ }
149
+
150
+ /**
151
+ * 升级包
152
+ * @param {string} name - 包名
153
+ * @param {{ global?: boolean, proxy?: string }} [opts] - 选项
154
+ * @returns {Promise<{ name: string, version: string, action: string }>}
155
+ */
156
+ async upgrade(name, opts) {
157
+ throw new Error(`${this.name}: upgrade not implemented`)
158
+ }
159
+
160
+ /**
161
+ * 清理缓存
162
+ * @returns {Promise<{ action: string }>}
163
+ */
164
+ async cleanCache() {
165
+ throw new Error(`${this.name}: cleanCache not implemented`)
166
+ }
167
+ }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * brew(Homebrew)包管理器实现(macOS)
3
+ *
4
+ * 通过执行 brew CLI 命令实现统一接口(系统级,无用户级/local 概念)。
5
+ * 源查询使用 brew info --json=v2,本地查询使用 brew list,均只读系统状态。
6
+ * 镜像(非 official)通过 HOMEBREW_API_DOMAIN / HOMEBREW_BOTTLE_DOMAIN 环境变量传递,
7
+ * 仅作用于本工具发起的命令,不修改系统 brew 配置。
8
+ */
9
+
10
+ import { execSync, spawnSync } from 'node:child_process'
11
+ import fs from 'node:fs'
12
+ import { PackageManager } from './base.js'
13
+ import { getActiveRegistry, getPresets, setActiveRegistry } from '../config.js'
14
+
15
+ const BREW_INSTALL_URL = 'https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh'
16
+ const BREW_UNINSTALL_URL = 'https://raw.githubusercontent.com/Homebrew/uninstall/HEAD/uninstall.sh'
17
+
18
+ /**
19
+ * 定位 brew 可执行文件(兼容 Intel / Apple Silicon 路径),找不到时退回 PATH
20
+ * @returns {string}
21
+ */
22
+ export function getBrewBin() {
23
+ for (const p of ['/opt/homebrew/bin/brew', '/usr/local/bin/brew']) {
24
+ if (fs.existsSync(p)) return p
25
+ }
26
+ return 'brew'
27
+ }
28
+
29
+ /**
30
+ * 检查 Homebrew 是否已安装
31
+ * @returns {boolean}
32
+ */
33
+ export function isHomebrewInstalled() {
34
+ try {
35
+ execSync(`${getBrewBin()} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 安装 Homebrew(curl 官方安装脚本)
44
+ * @returns {{ action: string, hint: string }}
45
+ */
46
+ export function installHomebrew() {
47
+ if (process.platform === 'win32') {
48
+ throw new Error('Homebrew is not supported on Windows (use WSL)')
49
+ }
50
+ execSync(`curl -fsSL "${BREW_INSTALL_URL}" | bash`, { stdio: 'inherit', shell: true })
51
+ return { action: 'installed', hint: 'Restart shell or ensure /opt/homebrew/bin (or /usr/local/bin) is in PATH' }
52
+ }
53
+
54
+ export class BrewManager extends PackageManager {
55
+ constructor() {
56
+ super()
57
+ this.name = 'brew'
58
+ }
59
+
60
+ /**
61
+ * 定位 brew 可执行文件(兼容 Intel / Apple Silicon 路径),找不到时退回 PATH
62
+ * @returns {string}
63
+ * @private
64
+ */
65
+ _brewBin() {
66
+ return getBrewBin()
67
+ }
68
+
69
+ /**
70
+ * 对包含特殊字符的参数进行 shell 转义(单引号包裹)
71
+ * @param {string} arg
72
+ * @returns {string}
73
+ * @private
74
+ */
75
+ _quoteArg(arg) {
76
+ if (arg && /[\s'"$`\\]/.test(arg)) {
77
+ return `'${arg.replace(/'/g, `'\\''`)}'`
78
+ }
79
+ return arg
80
+ }
81
+
82
+ /**
83
+ * 构造 brew 命令环境:镜像域名 + 代理 + 关闭自动更新/安装清理
84
+ * @param {string} [proxy] - 代理地址
85
+ * @returns {Record<string,string>}
86
+ * @private
87
+ */
88
+ _buildEnv(proxy) {
89
+ const env = { ...process.env }
90
+ const mirror = this._mirrorEnv()
91
+ Object.assign(env, mirror)
92
+ Object.assign(env, this._proxyEnv(proxy))
93
+ env.HOMEBREW_NO_AUTO_UPDATE = '1'
94
+ env.HOMEBREW_NO_INSTALL_CLEANUP = '1'
95
+ return env
96
+ }
97
+
98
+ /**
99
+ * 构造镜像环境变量(仅当 active 非 official 时设置)
100
+ * @returns {Record<string,string>}
101
+ * @private
102
+ */
103
+ _mirrorEnv() {
104
+ const active = getActiveRegistry('brew')
105
+ if (!active || active === getPresets('brew').official) return {}
106
+ const base = active.replace(/\/+$/, '')
107
+ if (!/^https?:\/\//.test(base)) return {}
108
+ return {
109
+ HOMEBREW_API_DOMAIN: `${base}/api`,
110
+ HOMEBREW_BOTTLE_DOMAIN: base,
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 构造 brew 代理环境变量(brew 遵循标准 HTTP_PROXY/ALL_PROXY)
116
+ * @param {string} [proxy] - 代理地址
117
+ * @returns {Record<string,string>}
118
+ * @private
119
+ */
120
+ _proxyEnv(proxy) {
121
+ if (!proxy) return {}
122
+ return {
123
+ HTTP_PROXY: proxy,
124
+ HTTPS_PROXY: proxy,
125
+ ALL_PROXY: proxy,
126
+ http_proxy: proxy,
127
+ https_proxy: proxy,
128
+ all_proxy: proxy,
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 执行 brew 命令,返回 stdout
134
+ * @param {string[]} args
135
+ * @param {{ allowNonZero?: boolean, proxy?: string }} [opts]
136
+ * @returns {string}
137
+ * @private
138
+ */
139
+ _exec(args, opts = {}) {
140
+ const cmd = [this._brewBin(), ...args].map((a) => this._quoteArg(a)).join(' ')
141
+ try {
142
+ return execSync(cmd, {
143
+ encoding: 'utf8',
144
+ stdio: ['pipe', 'pipe', 'pipe'],
145
+ env: this._buildEnv(opts.proxy),
146
+ ...opts,
147
+ })
148
+ } catch (err) {
149
+ if (opts.allowNonZero) {
150
+ return err.stdout ? err.stdout.toString() : ''
151
+ }
152
+ const stderr = err.stderr ? err.stderr.toString().trim() : ''
153
+ if (stderr && !stderr.startsWith('Warning:')) {
154
+ throw new Error(stderr)
155
+ }
156
+ return err.stdout ? err.stdout.toString() : ''
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 执行 brew 命令,输出直接继承到终端(用于 install/uninstall/upgrade)
162
+ * @param {string[]} args
163
+ * @param {{ proxy?: string }} [opts]
164
+ * @private
165
+ */
166
+ _execInherit(args, opts = {}) {
167
+ const result = spawnSync(this._brewBin(), args, {
168
+ stdio: 'inherit',
169
+ env: this._buildEnv(opts.proxy),
170
+ })
171
+ if (result.error) {
172
+ throw new Error(result.error.message)
173
+ }
174
+ if (result.status !== 0) {
175
+ throw new Error(`brew ${args.join(' ')} exited with code ${result.status}`)
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 解析 brew info --json=v2 输出为 { formula, cask }(取首个)
181
+ * @param {string} text
182
+ * @returns {{ formula?: object, cask?: object, type: string }|null}
183
+ * @private
184
+ */
185
+ _parseInfoJson(text) {
186
+ try {
187
+ const data = JSON.parse(text)
188
+ if (Array.isArray(data.formulae) && data.formulae.length > 0) {
189
+ return { formula: data.formulae[0], cask: null, type: 'formula' }
190
+ }
191
+ if (Array.isArray(data.casks) && data.casks.length > 0) {
192
+ return { formula: null, cask: data.casks[0], type: 'cask' }
193
+ }
194
+ } catch {}
195
+ return null
196
+ }
197
+
198
+ /**
199
+ * 解析 brew list --versions 输出为已安装项数组
200
+ * 每行格式:name version [version...]
201
+ * @param {string} out
202
+ * @returns {{ name: string, version: string }[]}
203
+ * @private
204
+ */
205
+ _parseListVersions(out) {
206
+ const items = []
207
+ for (const line of out.split('\n')) {
208
+ const trimmed = line.trim()
209
+ if (!trimmed) continue
210
+ const m = trimmed.match(/^(\S+)\s+(.+)$/)
211
+ if (m) items.push({ name: m[1], version: m[2].trim() })
212
+ }
213
+ return items
214
+ }
215
+
216
+ /**
217
+ * 解析 brew outdated --json=v2 输出为过期项数组(formulae + casks)
218
+ * @param {string} text
219
+ * @returns {{ name: string, current: string, latest: string }[]}
220
+ * @private
221
+ */
222
+ _parseOutdatedJson(text) {
223
+ let data
224
+ try {
225
+ data = JSON.parse(text)
226
+ } catch {
227
+ return []
228
+ }
229
+ const items = []
230
+ for (const group of ['formulae', 'casks']) {
231
+ const list = Array.isArray(data[group]) ? data[group] : []
232
+ for (const item of list) {
233
+ items.push({
234
+ name: item.name,
235
+ current: (Array.isArray(item.installed_versions) ? item.installed_versions : []).join(', '),
236
+ latest: item.current_version || '',
237
+ })
238
+ }
239
+ }
240
+ return items
241
+ }
242
+
243
+ async getVersion() {
244
+ try {
245
+ const out = this._exec(['--version'], { allowNonZero: true })
246
+ const firstLine = out.trim().split('\n')[0] || ''
247
+ const match = firstLine.match(/^Homebrew\s+(\S+)/)
248
+ if (match) {
249
+ return { name: 'brew', installed: true, version: match[1] }
250
+ }
251
+ if (firstLine) {
252
+ return { name: 'brew', installed: true, version: firstLine }
253
+ }
254
+ } catch {}
255
+ return { name: 'brew', version: '', installed: false, hint: 'Install Homebrew from https://brew.sh/' }
256
+ }
257
+
258
+ async getRegistry() {
259
+ return { registry: getActiveRegistry('brew') }
260
+ }
261
+
262
+ async setRegistry(url) {
263
+ const resolved = setActiveRegistry('brew', url)
264
+ return { registry: resolved }
265
+ }
266
+
267
+ /**
268
+ * 查询包的源内信息(brew info --json=v2)
269
+ * @param {string} name
270
+ * @param {{ proxy?: string }} [opts]
271
+ * @returns {{ formula?: object, cask?: object, type: string }|null}
272
+ * @private
273
+ */
274
+ _fetchInfo(name, opts = {}) {
275
+ const out = this._exec(['info', '--json=v2', name], { allowNonZero: true, proxy: opts.proxy })
276
+ return this._parseInfoJson(out)
277
+ }
278
+
279
+ async searchPackage(name, version, opts = {}) {
280
+ const { registry } = await this.getRegistry()
281
+ const info = this._fetchInfo(name, opts)
282
+ const item = info ? info.formula || info.cask : null
283
+ if (!item) {
284
+ if (version) return { name, version, exists: false, available: false, registry }
285
+ return { name, exists: false, registry }
286
+ }
287
+ const stable = item.versions?.stable || ''
288
+ if (version) {
289
+ return { name, version, exists: true, available: stable === version, registry }
290
+ }
291
+ const versions = [stable].filter(Boolean)
292
+ return {
293
+ name,
294
+ exists: true,
295
+ latest: stable,
296
+ versions,
297
+ registry,
298
+ }
299
+ }
300
+
301
+ async isInstalled(name, opts = {}) {
302
+ const out = this._exec(['list', '--versions', name], { allowNonZero: true })
303
+ const items = this._parseListVersions(out)
304
+ const hit = items.find((i) => i.name === name)
305
+ if (hit) {
306
+ return { name, installed: true, version: hit.version }
307
+ }
308
+ return { name, installed: false }
309
+ }
310
+
311
+ async packageInfo(name, opts = {}) {
312
+ const info = this._fetchInfo(name, opts)
313
+ const item = info ? info.formula || info.cask : null
314
+ if (!item) {
315
+ return { name, description: '(not found)' }
316
+ }
317
+ return {
318
+ name,
319
+ description: item.desc || '',
320
+ latest: item.versions?.stable || '',
321
+ homepage: item.homepage || '',
322
+ license: item.license || '',
323
+ author: '',
324
+ repository: item.tap || '',
325
+ }
326
+ }
327
+
328
+ async packageVersions(name, opts = {}) {
329
+ const { registry } = await this.getRegistry()
330
+ const info = this._fetchInfo(name, opts)
331
+ const item = info ? info.formula || info.cask : null
332
+ const versions = []
333
+ if (item) {
334
+ if (item.versions?.stable) versions.push(item.versions.stable)
335
+ if (item.versions?.head) versions.push(item.versions.head)
336
+ }
337
+ return { name, versions, registry }
338
+ }
339
+
340
+ async listInstalled(opts = {}) {
341
+ const out = this._exec(['list', '--versions'], { allowNonZero: true })
342
+ return this._parseListVersions(out)
343
+ }
344
+
345
+ async listOutdated(opts = {}) {
346
+ const out = this._exec(['outdated', '--json=v2'], { allowNonZero: true, proxy: opts.proxy })
347
+ return this._parseOutdatedJson(out)
348
+ }
349
+
350
+ async install(name, version, opts = {}) {
351
+ if (!isHomebrewInstalled()) {
352
+ console.log('[opm] Homebrew not installed, auto-installing...')
353
+ installHomebrew()
354
+ }
355
+ const spec = version && !name.includes('@') ? `${name}@${version}` : name
356
+ this._execInherit(['install', spec], { proxy: opts.proxy })
357
+ return { name, version: version || 'latest', action: 'installed' }
358
+ }
359
+
360
+ async uninstall(name, opts = {}) {
361
+ this._execInherit(['uninstall', name])
362
+ return { name, action: 'uninstalled' }
363
+ }
364
+
365
+ async upgrade(name, opts = {}) {
366
+ this._execInherit(['upgrade', name], { proxy: opts.proxy })
367
+ return { name, version: 'latest', action: 'upgraded' }
368
+ }
369
+
370
+ async cleanCache() {
371
+ this._execInherit(['cleanup', '-s', '--prune=all'])
372
+ return { action: 'cache cleaned' }
373
+ }
374
+ }