@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,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,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { copyBuiltinConfig, getUserConfigFile } from '../src/config.js'
4
+
5
+ if (copyBuiltinConfig()) {
6
+ console.log(`opm: wrote default config to ${getUserConfigFile()}`)
7
+ }
@@ -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
+ }