@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,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
|
+
}
|