@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,41 @@
|
|
|
1
|
+
# opencode 命令对照
|
|
2
|
+
|
|
3
|
+
`@wwkit/opm` 统一命令管理 opencode 运维操作。
|
|
4
|
+
|
|
5
|
+
## 命令对照
|
|
6
|
+
|
|
7
|
+
| 统一命令 | 原始命令 | 说明 |
|
|
8
|
+
|---------|---------|------|
|
|
9
|
+
| `opm opencode clear` | — | 清理 opencode 会话并 VACUUM 压缩数据库 |
|
|
10
|
+
| `opm opencode clear -d, --days <n>` | — | 保留最近 n 天(对齐本地零点,含今天;默认 `-d 1`) |
|
|
11
|
+
| `opm opencode clear -h, --hours <n>` | — | 保留最近 n 小时(滚动窗口) |
|
|
12
|
+
| `opm opencode clear -n, --num <n>` | — | 保留最新 n 个会话 |
|
|
13
|
+
| `opm opencode clear --dry-run` | — | 仅预览,不修改任何数据 |
|
|
14
|
+
| `opm opencode clear -y` | — | 跳过确认提示 |
|
|
15
|
+
| `opm opencode config` | — | 显示 opencode 配置文件内容(优先 `opencode.jsonc`,其次 `opencode.json`) |
|
|
16
|
+
| `opm opencode version` | `npm list -g opencode-ai` | 已安装版本 |
|
|
17
|
+
| `opm opencode install` | `npm install -g opencode-ai` | 安装(使用 opm npm 镜像 + proxy) |
|
|
18
|
+
| `opm opencode uninstall` | `npm uninstall -g opencode-ai` | 卸载 |
|
|
19
|
+
| `opm opencode upgrade` | `npm install -g opencode-ai@latest` | 升级(使用 opm npm 镜像 + proxy) |
|
|
20
|
+
| `opm opencode dir` | — | 显示 data/db/log/config 路径 |
|
|
21
|
+
|
|
22
|
+
> `--days` / `--hours` / `--num` 三选一,互斥。
|
|
23
|
+
|
|
24
|
+
## 实现说明
|
|
25
|
+
|
|
26
|
+
- `src/harnesses/opencode/db.js` — 数据目录 / DB / 日志路径
|
|
27
|
+
- `src/harnesses/opencode/sqlite.js` — 跨平台 SQLite 驱动(Node >= 22.5 内置 `node:sqlite`,否则回退 sqlite3 CLI)
|
|
28
|
+
- `src/harnesses/opencode/clear.js` — 清理流程(保留策略计算 + 删除 + VACUUM)
|
|
29
|
+
- `src/harnesses/base.js` — `HarnessGroup` 统一命令组基类(version/install/uninstall/upgrade/config/clear/dir)
|
|
30
|
+
|
|
31
|
+
## 示例
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
opm opencode clear # 只保留今天创建的会话
|
|
35
|
+
opm opencode clear -d 7 # 保留最近 7 天会话
|
|
36
|
+
opm opencode clear -h 24 # 保留最近 24 小时会话
|
|
37
|
+
opm opencode clear -n 10 # 保留最新 10 个会话
|
|
38
|
+
opm opencode clear --dry-run # 仅预览
|
|
39
|
+
opm opencode clear -y # 跳过确认直接执行
|
|
40
|
+
opm opencode config # 显示 opencode 配置文件内容
|
|
41
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wwkit/opm",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"author": "bluesliu <langcai163@163.com>",
|
|
5
|
+
"description": "Unified CLI — package management (npm/pip/dnf/apt) + config view + opencode maintenance",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"opm": "./bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"docs",
|
|
15
|
+
"scripts",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.0.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"opm",
|
|
23
|
+
"pkg",
|
|
24
|
+
"npm",
|
|
25
|
+
"pip",
|
|
26
|
+
"apt",
|
|
27
|
+
"dnf",
|
|
28
|
+
"registry",
|
|
29
|
+
"mirror",
|
|
30
|
+
"opencode",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"extract-zip": "^2.0.1",
|
|
36
|
+
"@wwkit/shared": "1.0.5"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/",
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"postinstall": "node scripts/postinstall.js",
|
|
44
|
+
"check": "node --check bin/index.js",
|
|
45
|
+
"start": "node bin/index.js",
|
|
46
|
+
"test": "node --test \"tests/**/*.test.js\"",
|
|
47
|
+
"test:all": "node --test \"tests/**/*.test.js\"",
|
|
48
|
+
"doc": "node -e \"import('./src/cli/doc.js').then(m => m.generateDocHtml({ open: false }))\"",
|
|
49
|
+
"release": "node ../../packages/shared/bin/release-pkg.js"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 二进制工具命令组基类
|
|
3
|
+
*
|
|
4
|
+
* 处理「下载 zip 二进制工具 → 解压 → chmod → Linux 依赖检查 → 多版本目录管理」。
|
|
5
|
+
* 与 AI harness(opm opencode,走 npm 全局包)性质不同,故独立成组。
|
|
6
|
+
*
|
|
7
|
+
* 通用动作(数据驱动,子类/配置描述工具差异):
|
|
8
|
+
* version / installed / home / versions / list / install / status / clear / registry
|
|
9
|
+
*
|
|
10
|
+
* 新增二进制工具:在 src/binaries/<name>/ 下提供 config 后 new BinaryGroup(cfg) 注册即可。
|
|
11
|
+
* cfg 给出 artifacts(下载地址/目录/二进制名)、默认版本来源、registry 段名等。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import extractZip from 'extract-zip'
|
|
17
|
+
|
|
18
|
+
import { parseFlags } from '../cli/helpers/args.js'
|
|
19
|
+
import { output } from '../formatter.js'
|
|
20
|
+
import { getActiveRegistry, setActiveRegistry, getPresets, getActiveProxy, getUserConfigDir } from '../config.js'
|
|
21
|
+
import { fetchText, downloadToFile } from '../managers/http.js'
|
|
22
|
+
import { getOs, OS_TYPES } from './platform.js'
|
|
23
|
+
import { find as findExe } from './exefinder.js'
|
|
24
|
+
import { verify as verifyDeps } from './depchecker.js'
|
|
25
|
+
|
|
26
|
+
const ACTIONS = {
|
|
27
|
+
version: { desc: 'Show current (active) version' },
|
|
28
|
+
installed: { desc: 'Check if a version is installed' },
|
|
29
|
+
dir: { desc: 'Show install dir + binary paths of the active version' },
|
|
30
|
+
versions: { desc: 'List all available versions (remote, known-good JSON)' },
|
|
31
|
+
list: { desc: 'List installed versions' },
|
|
32
|
+
install: { desc: 'Install a version (default: config default version)' },
|
|
33
|
+
status: { desc: 'Show install status of a version (default: all)' },
|
|
34
|
+
clear: { desc: 'Remove a version (default: all versions)' },
|
|
35
|
+
registry: { desc: 'Query or set registry/mirror address' },
|
|
36
|
+
help: { desc: 'Show this help' },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} BinaryToolConfig
|
|
41
|
+
* @property {string} name - 命令名/配置段名(如 "cft")
|
|
42
|
+
* @property {string} desc - 顶层帮助描述
|
|
43
|
+
* @property {() => string} getDefaultVersion - 返回默认安装版本
|
|
44
|
+
* @property {Object[]} artifacts - 下载产物描述
|
|
45
|
+
* @property {string} artifacts[].id - 产物 id(chrome/chromedriver/...)
|
|
46
|
+
* @property {string} artifacts[].binName - exefinder 查找名
|
|
47
|
+
* @property {(registry: string, version: string, os: string) => string} artifacts[].buildUrl
|
|
48
|
+
* @property {(os: string) => string} artifacts[].dirName - 解压目标目录名
|
|
49
|
+
* @property {string} [versionsFile] - 远程版本 JSON 相对路径(默认 known-good-versions-with-downloads.json)
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
export class BinaryGroup {
|
|
53
|
+
/**
|
|
54
|
+
* @param {BinaryToolConfig} cfg
|
|
55
|
+
*/
|
|
56
|
+
constructor(cfg) {
|
|
57
|
+
this.name = cfg.name
|
|
58
|
+
this.desc = cfg.desc
|
|
59
|
+
this.cfg = cfg
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 安装根目录:~/.config/opm/<name>/
|
|
64
|
+
* @returns {string}
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
_installRoot() {
|
|
68
|
+
return path.join(getUserConfigDir(), this.name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 某版本的安装目录
|
|
73
|
+
* @param {string} version
|
|
74
|
+
* @returns {string}
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_versionRoot(version) {
|
|
78
|
+
return path.join(this._installRoot(), version)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 激活版本 manifest 路径
|
|
83
|
+
* @returns {string}
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
_manifestPath() {
|
|
87
|
+
return path.join(this._installRoot(), 'manifest.json')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 读取激活版本
|
|
92
|
+
* @returns {string}
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_readActive() {
|
|
96
|
+
try {
|
|
97
|
+
const raw = JSON.parse(fs.readFileSync(this._manifestPath(), 'utf8'))
|
|
98
|
+
return raw.active || ''
|
|
99
|
+
} catch {
|
|
100
|
+
return ''
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 写激活版本
|
|
106
|
+
* @param {string} version
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_writeActive(version) {
|
|
110
|
+
const root = this._installRoot()
|
|
111
|
+
fs.mkdirSync(root, { recursive: true })
|
|
112
|
+
fs.writeFileSync(this._manifestPath(), `${JSON.stringify({ active: version }, null, 2)}\n`, 'utf8')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 列出已安装版本目录(含 installed.json 的 <version>/ 目录)
|
|
117
|
+
* @returns {{ version: string, root: string }[]}
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_listInstalled() {
|
|
121
|
+
const root = this._installRoot()
|
|
122
|
+
if (!fs.existsSync(root)) return []
|
|
123
|
+
const items = []
|
|
124
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
125
|
+
if (!entry.isDirectory()) continue
|
|
126
|
+
if (!fs.existsSync(path.join(root, entry.name, 'installed.json'))) continue
|
|
127
|
+
items.push({ version: entry.name, root: path.join(root, entry.name) })
|
|
128
|
+
}
|
|
129
|
+
items.sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))
|
|
130
|
+
return items
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 读取某版本 installed.json
|
|
135
|
+
* @param {string} version
|
|
136
|
+
* @returns {Object|null}
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_readInstalledInfo(version) {
|
|
140
|
+
const file = path.join(this._versionRoot(version), 'installed.json')
|
|
141
|
+
if (!fs.existsSync(file)) return null
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
144
|
+
} catch {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 解析代理(-p/--proxy 优先,否则回退配置 active proxy)
|
|
151
|
+
* @param {string[]} argv
|
|
152
|
+
* @returns {string}
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
_resolveProxy(argv) {
|
|
156
|
+
const parsed = parseFlags(argv)
|
|
157
|
+
let proxy = parsed.flags.proxy || parsed.flags.p || ''
|
|
158
|
+
if (proxy === 'true') proxy = ''
|
|
159
|
+
if (!proxy) proxy = getActiveProxy()
|
|
160
|
+
return proxy
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async run(argv) {
|
|
164
|
+
const [action, ...rest] = argv
|
|
165
|
+
|
|
166
|
+
if (!action || action === '-h' || action === '--help' || action === 'help') {
|
|
167
|
+
this.printHelp()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch (action) {
|
|
172
|
+
case 'version':
|
|
173
|
+
return this._version()
|
|
174
|
+
case 'installed':
|
|
175
|
+
return this._installed()
|
|
176
|
+
case 'dir':
|
|
177
|
+
return this._dir()
|
|
178
|
+
case 'versions':
|
|
179
|
+
return this._versions(rest)
|
|
180
|
+
case 'list':
|
|
181
|
+
return this._list()
|
|
182
|
+
case 'install':
|
|
183
|
+
return this._install(rest)
|
|
184
|
+
case 'status':
|
|
185
|
+
return this._status(rest)
|
|
186
|
+
case 'clear':
|
|
187
|
+
return this._clear(rest)
|
|
188
|
+
case 'registry':
|
|
189
|
+
return this._registry(rest)
|
|
190
|
+
default:
|
|
191
|
+
console.error(`Unknown action: ${action}`)
|
|
192
|
+
this.printHelp()
|
|
193
|
+
process.exit(1)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 拉取远程可用版本(known-good JSON)
|
|
199
|
+
* @param {string} proxy
|
|
200
|
+
* @returns {Promise<{ name: string, versions: string[], registry: string, os: string }>}
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
async _fetchVersions(proxy) {
|
|
204
|
+
const registry = getActiveRegistry(this.name)
|
|
205
|
+
const os = getOs()
|
|
206
|
+
const file = this.cfg.versionsFile || 'known-good-versions-with-downloads.json'
|
|
207
|
+
const url = `${registry.replace(/\/$/, '')}/${file}`
|
|
208
|
+
const text = await fetchText(url, { proxy })
|
|
209
|
+
if (!text) {
|
|
210
|
+
throw new Error(`无法获取可用版本列表: ${url}(请检查 registry / 网络 / 代理)`)
|
|
211
|
+
}
|
|
212
|
+
let data
|
|
213
|
+
try {
|
|
214
|
+
data = JSON.parse(text)
|
|
215
|
+
} catch {
|
|
216
|
+
throw new Error(`known-good JSON 解析失败: ${url}`)
|
|
217
|
+
}
|
|
218
|
+
const versions = []
|
|
219
|
+
for (const item of data.versions || []) {
|
|
220
|
+
if (!item.version) continue
|
|
221
|
+
const dl = item.downloads || {}
|
|
222
|
+
if (dl.chrome?.some((d) => d.platform === os)) {
|
|
223
|
+
versions.push(item.version)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { name: this.name, versions, registry, os }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async _version() {
|
|
230
|
+
const active = this._readActive()
|
|
231
|
+
const info = active ? this._readInstalledInfo(active) : null
|
|
232
|
+
output(info ? active.replace(/^v/, '') : '')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _installed() {
|
|
236
|
+
const list = this._listInstalled()
|
|
237
|
+
const active = this._readActive()
|
|
238
|
+
output({
|
|
239
|
+
name: this.name,
|
|
240
|
+
installed: list.length > 0,
|
|
241
|
+
active: active || undefined,
|
|
242
|
+
versions: list.map(({ version }) => version),
|
|
243
|
+
count: list.length,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async _dir() {
|
|
248
|
+
const active = this._readActive()
|
|
249
|
+
const info = active ? this._readInstalledInfo(active) : null
|
|
250
|
+
output({
|
|
251
|
+
name: this.name,
|
|
252
|
+
install: active ? this._versionRoot(active) : this._installRoot(),
|
|
253
|
+
...(info?.chrome_path ? { chrome: info.chrome_path } : {}),
|
|
254
|
+
...(info?.chromedriver_path ? { chromedriver: info.chromedriver_path } : {}),
|
|
255
|
+
...(active ? {} : { hint: `No version installed, run \`opm ${this.name} install\`` }),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async _versions(rest) {
|
|
260
|
+
const parsed = parseFlags(rest)
|
|
261
|
+
let proxy = parsed.flags.proxy || parsed.flags.p || ''
|
|
262
|
+
if (proxy === 'true') proxy = ''
|
|
263
|
+
if (!proxy) proxy = getActiveProxy()
|
|
264
|
+
try {
|
|
265
|
+
output(await this._fetchVersions(proxy))
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(err.message)
|
|
268
|
+
process.exit(1)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async _list() {
|
|
273
|
+
const list = this._listInstalled()
|
|
274
|
+
const active = this._readActive()
|
|
275
|
+
output({
|
|
276
|
+
name: this.name,
|
|
277
|
+
count: list.length,
|
|
278
|
+
active: active || undefined,
|
|
279
|
+
versions: list.map(({ version }) => ({
|
|
280
|
+
version,
|
|
281
|
+
current: version === active,
|
|
282
|
+
install: this._versionRoot(version),
|
|
283
|
+
})),
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async _install(rest) {
|
|
288
|
+
const parsed = parseFlags(rest)
|
|
289
|
+
const version = parsed.positional[0] || this.cfg.getDefaultVersion()
|
|
290
|
+
if (!version) {
|
|
291
|
+
console.error(`Usage: opm ${this.name} install [version]`)
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
const proxy = this._resolveProxy(rest)
|
|
295
|
+
const existing = this._readInstalledInfo(version)
|
|
296
|
+
if (existing) {
|
|
297
|
+
output({ name: this.name, version, installed: true, action: 'skipped (already installed)', install: this._versionRoot(version) })
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const os = getOs()
|
|
302
|
+
const registry = getActiveRegistry(this.name)
|
|
303
|
+
const targetRoot = this._versionRoot(version)
|
|
304
|
+
fs.mkdirSync(targetRoot, { recursive: true })
|
|
305
|
+
|
|
306
|
+
const result = { version, os }
|
|
307
|
+
for (const artifact of this.cfg.artifacts) {
|
|
308
|
+
const url = artifact.buildUrl(registry, version, os)
|
|
309
|
+
const dirName = artifact.dirName(os)
|
|
310
|
+
const extractDir = path.join(targetRoot, dirName)
|
|
311
|
+
const zipPath = path.join(targetRoot, `${artifact.id}.zip`)
|
|
312
|
+
|
|
313
|
+
console.log(` 下载 ${artifact.id} ${version} (${os})`)
|
|
314
|
+
await this._downloadArtifact(artifact, url, zipPath, targetRoot, dirName, proxy)
|
|
315
|
+
console.log(` ${artifact.id} 就绪: ${extractDir}`)
|
|
316
|
+
|
|
317
|
+
const binPath = findExe(extractDir, artifact.binName)
|
|
318
|
+
if (binPath) {
|
|
319
|
+
result[`${artifact.id}_path`] = binPath
|
|
320
|
+
console.log(` ${artifact.id} 路径: ${binPath}`)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (result.chrome_path && verifyDeps) {
|
|
325
|
+
const depResult = verifyDeps(result.chrome_path)
|
|
326
|
+
if (!depResult.ok) {
|
|
327
|
+
console.log(` 系统依赖: ${depResult.missing.join(', ')}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fs.writeFileSync(path.join(targetRoot, 'installed.json'), `${JSON.stringify({ ...result, location: targetRoot }, null, 2)}\n`, 'utf8')
|
|
332
|
+
this._writeActive(version)
|
|
333
|
+
output({ name: this.name, version, installed: true, action: 'installed', install: targetRoot, ...result })
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 下载单个 artifact 并解压(zip 内自带 <dirName>/ 目录,解压到目标根即可)
|
|
338
|
+
* @param {Object} artifact
|
|
339
|
+
* @param {string} url
|
|
340
|
+
* @param {string} zipPath - 下载的临时 zip 路径(解压后删除)
|
|
341
|
+
* @param {string} extractRoot - 解压目标根目录(版本目录)
|
|
342
|
+
* @param {string} dirName - zip 内目录名(解压目标)
|
|
343
|
+
* @param {string} proxy
|
|
344
|
+
* @returns {Promise<void>}
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
async _downloadArtifact(artifact, url, zipPath, extractRoot, dirName, proxy) {
|
|
348
|
+
const extractDir = path.join(extractRoot, dirName)
|
|
349
|
+
if (fs.existsSync(extractDir)) {
|
|
350
|
+
fs.rmSync(extractDir, { recursive: true, force: true })
|
|
351
|
+
}
|
|
352
|
+
if (fs.existsSync(zipPath)) {
|
|
353
|
+
fs.rmSync(zipPath, { force: true })
|
|
354
|
+
}
|
|
355
|
+
await downloadToFile(url, zipPath, { proxy })
|
|
356
|
+
try {
|
|
357
|
+
await extractZip(zipPath, { dir: extractRoot })
|
|
358
|
+
} finally {
|
|
359
|
+
if (fs.existsSync(zipPath)) {
|
|
360
|
+
fs.rmSync(zipPath, { force: true })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
this._chmodExecDir(extractDir)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 递归赋可执行权限
|
|
368
|
+
* @param {string} directory
|
|
369
|
+
* @private
|
|
370
|
+
*/
|
|
371
|
+
_chmodExecDir(directory) {
|
|
372
|
+
if (!directory || !fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) return
|
|
373
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
374
|
+
const fullPath = path.join(directory, entry.name)
|
|
375
|
+
if (entry.isDirectory()) {
|
|
376
|
+
this._chmodExecDir(fullPath)
|
|
377
|
+
} else {
|
|
378
|
+
try { fs.chmodSync(fullPath, 0o755) } catch {}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async _status(rest) {
|
|
384
|
+
const parsed = parseFlags(rest)
|
|
385
|
+
const version = parsed.positional[0]
|
|
386
|
+
const active = this._readActive()
|
|
387
|
+
const list = this._listInstalled()
|
|
388
|
+
|
|
389
|
+
if (version) {
|
|
390
|
+
const info = this._readInstalledInfo(version)
|
|
391
|
+
if (!info) {
|
|
392
|
+
output({ name: this.name, version, installed: false, hint: `Run \`opm ${this.name} install ${version}\`` })
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
const chromeOk = info.chrome_path ? fs.existsSync(info.chrome_path) : false
|
|
396
|
+
const driverOk = info.chromedriver_path ? fs.existsSync(info.chromedriver_path) : false
|
|
397
|
+
output({
|
|
398
|
+
name: this.name,
|
|
399
|
+
version,
|
|
400
|
+
installed: true,
|
|
401
|
+
current: version === active,
|
|
402
|
+
install: this._versionRoot(version),
|
|
403
|
+
...(info.chrome_path ? { chrome: info.chrome_path, chrome_ok: chromeOk } : {}),
|
|
404
|
+
...(info.chromedriver_path ? { chromedriver: info.chromedriver_path, chromedriver_ok: driverOk } : {}),
|
|
405
|
+
})
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
output({
|
|
410
|
+
name: this.name,
|
|
411
|
+
count: list.length,
|
|
412
|
+
active: active || undefined,
|
|
413
|
+
versions: list.map(({ version }) => {
|
|
414
|
+
const info = this._readInstalledInfo(version)
|
|
415
|
+
return {
|
|
416
|
+
version,
|
|
417
|
+
current: version === active,
|
|
418
|
+
install: this._versionRoot(version),
|
|
419
|
+
...(info?.chrome_path ? { chrome: info.chrome_path, chrome_ok: fs.existsSync(info.chrome_path) } : {}),
|
|
420
|
+
...(info?.chromedriver_path ? { chromedriver: info.chromedriver_path, chromedriver_ok: fs.existsSync(info.chromedriver_path) } : {}),
|
|
421
|
+
}
|
|
422
|
+
}),
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async _clear(rest) {
|
|
427
|
+
const parsed = parseFlags(rest)
|
|
428
|
+
const version = parsed.positional[0]
|
|
429
|
+
const list = this._listInstalled()
|
|
430
|
+
|
|
431
|
+
const targets = version
|
|
432
|
+
? [{ version }].filter((t) => list.some((i) => i.version === t.version))
|
|
433
|
+
: list
|
|
434
|
+
|
|
435
|
+
if (targets.length === 0) {
|
|
436
|
+
output({ name: this.name, action: 'nothing to clear' })
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const { version: v } of targets) {
|
|
441
|
+
const dir = this._versionRoot(v)
|
|
442
|
+
if (fs.existsSync(dir)) {
|
|
443
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
444
|
+
console.log(` ✓ 已删除 ${v}`)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const remaining = this._listInstalled()
|
|
449
|
+
const active = this._readActive()
|
|
450
|
+
if (active && !remaining.some((i) => i.version === active)) {
|
|
451
|
+
try {
|
|
452
|
+
fs.rmSync(this._manifestPath(), { force: true })
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
output({ name: this.name, action: 'cleared', removed: targets.map((t) => t.version), remaining: remaining.map((i) => i.version) })
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async _registry(rest) {
|
|
460
|
+
const parsed = parseFlags(rest)
|
|
461
|
+
const [sub, ...args] = parsed.positional
|
|
462
|
+
if (sub === 'set') {
|
|
463
|
+
const url = args[0]
|
|
464
|
+
if (!url) {
|
|
465
|
+
console.error(`Usage: opm ${this.name} registry set <url|preset>`)
|
|
466
|
+
process.exit(1)
|
|
467
|
+
}
|
|
468
|
+
output({ registry: setActiveRegistry(this.name, url) })
|
|
469
|
+
} else if (sub === 'presets') {
|
|
470
|
+
const items = Object.entries(getPresets(this.name)).map(([n, u]) => ({ name: n, url: u }))
|
|
471
|
+
output(items)
|
|
472
|
+
} else {
|
|
473
|
+
output({ registry: getActiveRegistry(this.name) })
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
printHelp() {
|
|
478
|
+
const actionLines = Object.entries(ACTIONS)
|
|
479
|
+
.map(([sig, { desc }]) => ` ${sig.padEnd(25)} ${desc}`)
|
|
480
|
+
.join('\n')
|
|
481
|
+
console.log(`
|
|
482
|
+
Usage: opm ${this.name} <action> [args] [options]
|
|
483
|
+
|
|
484
|
+
Actions:
|
|
485
|
+
${actionLines}
|
|
486
|
+
|
|
487
|
+
Options:
|
|
488
|
+
-p, --proxy <url> Proxy for versions/install (overrides config proxy.active)
|
|
489
|
+
-h, --help Show this help
|
|
490
|
+
|
|
491
|
+
Examples:
|
|
492
|
+
opm ${this.name} version
|
|
493
|
+
opm ${this.name} dir
|
|
494
|
+
opm ${this.name} list
|
|
495
|
+
opm ${this.name} versions
|
|
496
|
+
opm ${this.name} install
|
|
497
|
+
opm ${this.name} install <version>
|
|
498
|
+
opm ${this.name} status
|
|
499
|
+
opm ${this.name} status <version>
|
|
500
|
+
opm ${this.name} clear
|
|
501
|
+
opm ${this.name} clear <version>
|
|
502
|
+
opm ${this.name} registry
|
|
503
|
+
opm ${this.name} registry set <preset|url>
|
|
504
|
+
`)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export { OS_TYPES }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cft(Chrome for Testing)二进制工具配置
|
|
3
|
+
*
|
|
4
|
+
* 安装当前系统适配版本的 chrome + chromedriver 为独立可执行二进制,
|
|
5
|
+
* 与 opm 的 npm 全局包 harness 不同,直接下载 zip → 解压 → chmod。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BinaryGroup } from '../base.js'
|
|
9
|
+
import { getConfig } from '../../config.js'
|
|
10
|
+
|
|
11
|
+
const cft = {
|
|
12
|
+
name: 'cft',
|
|
13
|
+
desc: 'Chrome for Testing binary installer (chrome + chromedriver)',
|
|
14
|
+
getDefaultVersion: () => getConfig().cft?.defaultVersion || '',
|
|
15
|
+
artifacts: [
|
|
16
|
+
{
|
|
17
|
+
id: 'chrome',
|
|
18
|
+
binName: 'chrome',
|
|
19
|
+
dirName: (os) => `chrome-${os}`,
|
|
20
|
+
buildUrl: (registry, version, os) => `${registry}/${version}/${os}/chrome-${os}.zip`,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'chromedriver',
|
|
24
|
+
binName: 'chromedriver',
|
|
25
|
+
dirName: (os) => `chromedriver-${os}`,
|
|
26
|
+
buildUrl: (registry, version, os) => `${registry}/${version}/${os}/chromedriver-${os}.zip`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class CftGroup extends BinaryGroup {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(cft)
|
|
34
|
+
}
|
|
35
|
+
}
|