@wwkit/opm 1.0.3 → 1.0.4

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.
@@ -0,0 +1,558 @@
1
+ /**
2
+ * webwork 命令组 — WebWork 开发环境一键安装/管理
3
+ *
4
+ * 编排安装 7 个组件,复用已有 manager / group 能力,不重复实现:
5
+ * 1. Python(确保系统 python3 可用,版本 >= config.python.defaultVersion)
6
+ * 2. blues-lib(pip 全局安装,已安装则跳过)
7
+ * 3. cft(Chrome for Testing 二进制安装,已安装则跳过)
8
+ * 4. Node.js(确保可用,版本 >= config.node.defaultVersion)
9
+ * 5. opencode(npm 全局安装 opencode-ai,已安装则跳过)
10
+ * 6. harness(npm 全局安装 @wwkit/harness,已安装则跳过)
11
+ * 7. sshproxy(npm 全局安装 @wwkit/sshproxy,已安装则跳过)
12
+ *
13
+ * 命令: version / versions / installed / install / uninstall / upgrade / help
14
+ */
15
+
16
+ import { execSync } from 'node:child_process'
17
+
18
+ import { parseFlags } from '../../cli/helpers/args.js'
19
+ import { output } from '../../formatter.js'
20
+ import { getActiveProxy, getConfig } from '../../config.js'
21
+ import { NpmManager } from '../../managers/npm.js'
22
+ import { PipManager } from '../../managers/pip.js'
23
+ import { PythonManager, isUvInstalled, installUv } from '../../managers/python.js'
24
+ import { NodeManager, isNvmInstalled, installNvm } from '../../managers/node.js'
25
+ import { CftGroup } from '../../binaries/cft/index.js'
26
+
27
+ const COMPONENTS = [
28
+ { key: 'python', label: 'Python' },
29
+ { key: 'blues-lib', label: 'blues-lib' },
30
+ { key: 'cft', label: 'cft' },
31
+ { key: 'node', label: 'Node.js' },
32
+ { key: 'opencode', label: 'opencode' },
33
+ { key: 'harness', label: 'harness' },
34
+ { key: 'sshproxy', label: 'sshproxy' },
35
+ ]
36
+
37
+ const NPM_PACKAGES = {
38
+ opencode: 'opencode-ai',
39
+ harness: '@wwkit/harness',
40
+ sshproxy: '@wwkit/sshproxy',
41
+ }
42
+
43
+ const ACTIONS = {
44
+ version: { desc: 'Show installed version of each component' },
45
+ versions: { desc: 'Show latest available version of each component (remote)' },
46
+ installed: { desc: 'Show install status (true/false) of each component' },
47
+ install: { desc: 'Install all components in order (python -> blues-lib -> cft -> node -> opencode -> harness -> sshproxy)' },
48
+ uninstall: { desc: 'Uninstall all components in reverse order (sshproxy -> harness -> opencode -> node -> cft -> blues-lib)' },
49
+ upgrade: { desc: 'Upgrade all components to latest' },
50
+ help: { desc: 'Show this help' },
51
+ }
52
+
53
+ export class WebworkGroup {
54
+ constructor() {
55
+ this.name = 'webwork'
56
+ this.desc = 'WebWork dev environment (python/blues-lib/cft/node/opencode/harness/sshproxy)'
57
+ }
58
+
59
+ async run(argv) {
60
+ const [action, ...rest] = argv
61
+
62
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
63
+ this.printHelp()
64
+ return
65
+ }
66
+
67
+ const parsed = parseFlags(rest)
68
+
69
+ switch (action) {
70
+ case 'version':
71
+ return this._version()
72
+ case 'versions':
73
+ return this._versions(parsed)
74
+ case 'installed':
75
+ return this._installed()
76
+ case 'install':
77
+ return this._install(parsed)
78
+ case 'uninstall':
79
+ return this._uninstall(parsed)
80
+ case 'upgrade':
81
+ return this._upgrade(parsed)
82
+ default:
83
+ console.error(`Unknown action: ${action}`)
84
+ this.printHelp()
85
+ process.exit(1)
86
+ }
87
+ }
88
+
89
+ _resolveProxy(parsed) {
90
+ let proxy = parsed.flags.p || parsed.flags.proxy || ''
91
+ if (proxy === 'true') proxy = ''
92
+ if (!proxy) proxy = getActiveProxy()
93
+ return proxy
94
+ }
95
+
96
+ async _version() {
97
+ const result = {}
98
+
99
+ result['python'] = await this._pythonVersion()
100
+ result['blues-lib'] = await this._pipVersion('blues-lib')
101
+ result['cft'] = this._cftVersion()
102
+ result['node'] = this._nodeVersion()
103
+ result['opencode'] = await this._npmVersion(NPM_PACKAGES.opencode)
104
+ result['harness'] = await this._npmVersion(NPM_PACKAGES.harness)
105
+ result['sshproxy'] = await this._npmVersion(NPM_PACKAGES.sshproxy)
106
+
107
+ output(result)
108
+ }
109
+
110
+ async _versions(parsed) {
111
+ const proxy = this._resolveProxy(parsed)
112
+ const result = {}
113
+
114
+ result['python'] = await this._pythonLatest(proxy)
115
+ result['blues-lib'] = await this._pipLatest('blues-lib', proxy)
116
+ result['cft'] = await this._cftLatest(proxy)
117
+ result['node'] = await this._nodeLatest(proxy)
118
+ result['opencode'] = await this._npmLatest(NPM_PACKAGES.opencode, proxy)
119
+ result['harness'] = await this._npmLatest(NPM_PACKAGES.harness, proxy)
120
+ result['sshproxy'] = await this._npmLatest(NPM_PACKAGES.sshproxy, proxy)
121
+
122
+ output(result)
123
+ }
124
+
125
+ async _installed() {
126
+ const result = {}
127
+
128
+ result['python'] = this._pythonInstalled()
129
+ result['blues-lib'] = await this._pipInstalled('blues-lib')
130
+ result['cft'] = this._cftInstalled()
131
+ result['node'] = this._nodeInstalled()
132
+ result['opencode'] = await this._npmInstalled(NPM_PACKAGES.opencode)
133
+ result['harness'] = await this._npmInstalled(NPM_PACKAGES.harness)
134
+ result['sshproxy'] = await this._npmInstalled(NPM_PACKAGES.sshproxy)
135
+
136
+ output(result)
137
+ }
138
+
139
+ /**
140
+ * 批量执行组件操作(install/uninstall/upgrade 共用)
141
+ * 每步打印 [n/total] 组件名 + 跳过/成功/失败日志,最后汇总
142
+ * @param {string} verb - install|uninstall|upgrade
143
+ * @param {Array} components - 组件列表
144
+ * @param {string} proxy - 代理地址
145
+ * @param {Function} fn - (key, proxy) => Promise<result>
146
+ * @returns {Promise<void>}
147
+ * @private
148
+ */
149
+ async _runBatch(verb, components, proxy, fn) {
150
+ const results = []
151
+ const total = components.length
152
+ const verbUpper = verb.charAt(0).toUpperCase() + verb.slice(1)
153
+
154
+ for (let i = 0; i < components.length; i++) {
155
+ const { key, label } = components[i]
156
+ console.log(`\n[${i + 1}/${total}] ${verbUpper} ${label}...`)
157
+ try {
158
+ const r = await fn(key, proxy)
159
+ const action = r.action || 'ok'
160
+ if (action === 'skipped') {
161
+ console.log(` → skipped: ${r.reason || 'already satisfied'}`)
162
+ } else {
163
+ console.log(` ✓ ${label} ${action}` + (r.version ? ` ${r.version}` : ''))
164
+ }
165
+ results.push({ component: key, status: 'ok', ...r })
166
+ } catch (err) {
167
+ results.push({ component: key, status: 'failed', error: err.message })
168
+ console.error(` ✗ ${label} failed: ${err.message}`)
169
+ }
170
+ }
171
+
172
+ const ok = results.filter((r) => r.status === 'ok').length
173
+ const failed = results.filter((r) => r.status === 'failed').length
174
+ const skipped = results.filter((r) => r.action === 'skipped').length
175
+ console.log(`\n${verbUpper} summary: ${ok} ok, ${skipped} skipped, ${failed} failed`)
176
+
177
+ output({ action: verb, results, summary: { ok, skipped, failed, total } })
178
+
179
+ if (failed > 0) {
180
+ process.exit(1)
181
+ }
182
+ }
183
+
184
+ async _install(parsed) {
185
+ const proxy = this._resolveProxy(parsed)
186
+ await this._runBatch('install', COMPONENTS, proxy, (key, p) => this._installComponent(key, p))
187
+ }
188
+
189
+ async _uninstall(parsed) {
190
+ const proxy = this._resolveProxy(parsed)
191
+ const reversed = [...COMPONENTS].reverse()
192
+ await this._runBatch('uninstall', reversed, proxy, (key, p) => this._uninstallComponent(key, p))
193
+ }
194
+
195
+ async _upgrade(parsed) {
196
+ const proxy = this._resolveProxy(parsed)
197
+ await this._runBatch('upgrade', COMPONENTS, proxy, (key, p) => this._upgradeComponent(key, p))
198
+ }
199
+
200
+ async _installComponent(key, proxy) {
201
+ switch (key) {
202
+ case 'python':
203
+ return this._installPython(proxy)
204
+ case 'blues-lib':
205
+ return this._installPipPackage('blues-lib', proxy)
206
+ case 'cft':
207
+ return this._installCft(proxy)
208
+ case 'node':
209
+ return this._installNode(proxy)
210
+ case 'opencode':
211
+ return this._installNpmPackage(NPM_PACKAGES.opencode, proxy)
212
+ case 'harness':
213
+ return this._installNpmPackage(NPM_PACKAGES.harness, proxy)
214
+ case 'sshproxy':
215
+ return this._installNpmPackage(NPM_PACKAGES.sshproxy, proxy)
216
+ default:
217
+ throw new Error(`Unknown component: ${key}`)
218
+ }
219
+ }
220
+
221
+ async _uninstallComponent(key, proxy) {
222
+ switch (key) {
223
+ case 'python':
224
+ return { action: 'skipped', reason: 'python is a system dependency, not uninstalled' }
225
+ case 'blues-lib':
226
+ return this._uninstallPipPackage('blues-lib')
227
+ case 'cft':
228
+ return this._uninstallCft()
229
+ case 'node':
230
+ return { action: 'skipped', reason: 'node is a system dependency, not uninstalled' }
231
+ case 'opencode':
232
+ return this._uninstallNpmPackage(NPM_PACKAGES.opencode)
233
+ case 'harness':
234
+ return this._uninstallNpmPackage(NPM_PACKAGES.harness)
235
+ case 'sshproxy':
236
+ return this._uninstallNpmPackage(NPM_PACKAGES.sshproxy)
237
+ default:
238
+ throw new Error(`Unknown component: ${key}`)
239
+ }
240
+ }
241
+
242
+ async _upgradeComponent(key, proxy) {
243
+ switch (key) {
244
+ case 'python':
245
+ return this._upgradePython(proxy)
246
+ case 'blues-lib':
247
+ return this._upgradePipPackage('blues-lib', proxy)
248
+ case 'cft':
249
+ return this._installCft(proxy)
250
+ case 'node':
251
+ return this._upgradeNode(proxy)
252
+ case 'opencode':
253
+ return this._upgradeNpmPackage(NPM_PACKAGES.opencode, proxy)
254
+ case 'harness':
255
+ return this._upgradeNpmPackage(NPM_PACKAGES.harness, proxy)
256
+ case 'sshproxy':
257
+ return this._upgradeNpmPackage(NPM_PACKAGES.sshproxy, proxy)
258
+ default:
259
+ throw new Error(`Unknown component: ${key}`)
260
+ }
261
+ }
262
+
263
+ _pythonInstalled() {
264
+ return this._pythonVersion() !== '-'
265
+ }
266
+
267
+ /**
268
+ * 比较两个语义化版本号
269
+ * @param {string} a - 版本号 a(如 "3.12.1")
270
+ * @param {string} b - 版本号 b(如 "3.12")
271
+ * @returns {number} -1 (a<b), 0 (a==b), 1 (a>b)
272
+ * @private
273
+ */
274
+ _compareVersion(a, b) {
275
+ const pa = a.split('.').map((n) => parseInt(n, 10) || 0)
276
+ const pb = b.split('.').map((n) => parseInt(n, 10) || 0)
277
+ const len = Math.max(pa.length, pb.length)
278
+ for (let i = 0; i < len; i++) {
279
+ const va = pa[i] ?? 0
280
+ const vb = pb[i] ?? 0
281
+ if (va < vb) return -1
282
+ if (va > vb) return 1
283
+ }
284
+ return 0
285
+ }
286
+
287
+ async _pythonVersion() {
288
+ for (const cmd of ['python3', 'python']) {
289
+ try {
290
+ const out = execSync(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
291
+ const m = out.trim().match(/Python\s+(\d+\.\d+\.\d+)/)
292
+ if (m) return m[1]
293
+ } catch {}
294
+ }
295
+ return '-'
296
+ }
297
+
298
+ async _pythonLatest(proxy) {
299
+ const mgr = new PythonManager()
300
+ try {
301
+ const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
302
+ const versions = mgr._parseRemote(out)
303
+ return versions[versions.length - 1] || '-'
304
+ } catch {
305
+ return '-'
306
+ }
307
+ }
308
+
309
+ async _installPython(proxy) {
310
+ const defaultVersion = getConfig().python?.defaultVersion || '3.12'
311
+ const installed = await this._pythonVersion()
312
+ if (installed !== '-' && this._compareVersion(installed, defaultVersion) >= 0) {
313
+ return { action: 'skipped', reason: `python ${installed} >= ${defaultVersion}`, version: installed }
314
+ }
315
+ if (installed !== '-') {
316
+ console.log(` current: ${installed}, required: >= ${defaultVersion}`)
317
+ } else {
318
+ console.log(` python not found, installing ${defaultVersion}`)
319
+ }
320
+ if (!isUvInstalled()) {
321
+ console.log(' uv not found, installing uv...')
322
+ installUv(proxy)
323
+ }
324
+ const mgr = new PythonManager()
325
+ mgr._execInherit(mgr.cfg.installCmd(defaultVersion), { proxy })
326
+ return { action: 'installed', version: defaultVersion }
327
+ }
328
+
329
+ async _upgradePython(proxy) {
330
+ if (!isUvInstalled()) {
331
+ console.log(' uv not found, installing uv...')
332
+ installUv(proxy)
333
+ }
334
+ const mgr = new PythonManager()
335
+ const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
336
+ const versions = mgr._parseRemote(out)
337
+ const latest = versions[versions.length - 1]
338
+ if (!latest) {
339
+ return { action: 'skipped', reason: 'no remote version found' }
340
+ }
341
+ mgr._execInherit(mgr.cfg.installCmd(latest), { proxy })
342
+ return { action: 'upgraded', version: latest }
343
+ }
344
+
345
+ _nodeInstalled() {
346
+ return this._nodeVersion() !== '-'
347
+ }
348
+
349
+ _nodeVersion() {
350
+ try {
351
+ const out = execSync('node --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
352
+ const m = out.trim().match(/v(\d+\.\d+\.\d+)/)
353
+ if (m) return m[1]
354
+ } catch {}
355
+ return '-'
356
+ }
357
+
358
+ async _nodeLatest(proxy) {
359
+ const mgr = new NodeManager()
360
+ if (!isNvmInstalled()) return '-'
361
+ try {
362
+ const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
363
+ const versions = mgr._parseRemote(out)
364
+ return versions[versions.length - 1]?.replace(/^v/, '') || '-'
365
+ } catch {
366
+ return '-'
367
+ }
368
+ }
369
+
370
+ async _installNode(proxy) {
371
+ const defaultVersion = getConfig().node?.defaultVersion || '24'
372
+ const installed = this._nodeVersion()
373
+ if (installed !== '-' && this._compareVersion(installed, defaultVersion) >= 0) {
374
+ return { action: 'skipped', reason: `node ${installed} >= ${defaultVersion}`, version: installed }
375
+ }
376
+ if (installed !== '-') {
377
+ console.log(` current: ${installed}, required: >= ${defaultVersion}`)
378
+ } else {
379
+ console.log(` node not found, installing ${defaultVersion}`)
380
+ }
381
+ if (!isNvmInstalled()) {
382
+ console.log(' nvm not found, installing nvm...')
383
+ installNvm(proxy)
384
+ }
385
+ const mgr = new NodeManager()
386
+ mgr._execInherit(mgr.cfg.installCmd(defaultVersion), { proxy })
387
+ mgr._execInherit(mgr.cfg.useCmd(defaultVersion), { proxy })
388
+ mgr._execInherit(mgr.cfg.defaultCmd(defaultVersion), { proxy })
389
+ return { action: 'installed', version: defaultVersion }
390
+ }
391
+
392
+ async _upgradeNode(proxy) {
393
+ if (!isNvmInstalled()) {
394
+ console.log(' nvm not found, installing nvm...')
395
+ installNvm(proxy)
396
+ }
397
+ const mgr = new NodeManager()
398
+ const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
399
+ const versions = mgr._parseRemote(out)
400
+ const latest = versions[versions.length - 1]
401
+ if (!latest) {
402
+ return { action: 'skipped', reason: 'no remote version found' }
403
+ }
404
+ const ver = latest.replace(/^v/, '')
405
+ mgr._execInherit(mgr.cfg.installCmd(ver), { proxy })
406
+ mgr._execInherit(mgr.cfg.useCmd(ver), { proxy })
407
+ mgr._execInherit(mgr.cfg.defaultCmd(ver), { proxy })
408
+ return { action: 'upgraded', version: ver }
409
+ }
410
+
411
+ async _pipInstalled(name) {
412
+ const pip = new PipManager()
413
+ const result = await pip.isInstalled(name, { global: true })
414
+ return result.installed
415
+ }
416
+
417
+ async _pipVersion(name) {
418
+ const pip = new PipManager()
419
+ const result = await pip.isInstalled(name, { global: true })
420
+ return result.version || '-'
421
+ }
422
+
423
+ async _pipLatest(name, proxy) {
424
+ const pip = new PipManager()
425
+ const result = await pip.searchPackage(name, null, { proxy })
426
+ return result.latest || '-'
427
+ }
428
+
429
+ async _installPipPackage(name, proxy) {
430
+ const pip = new PipManager()
431
+ const existing = await pip.isInstalled(name, { global: true })
432
+ if (existing.installed) {
433
+ return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
434
+ }
435
+ return pip.install(name, null, { global: true, proxy })
436
+ }
437
+
438
+ async _upgradePipPackage(name, proxy) {
439
+ const pip = new PipManager()
440
+ return pip.upgrade(name, { global: true, proxy })
441
+ }
442
+
443
+ async _uninstallPipPackage(name) {
444
+ const pip = new PipManager()
445
+ return pip.uninstall(name, { global: true })
446
+ }
447
+
448
+ _cftInstalled() {
449
+ const cft = new CftGroup()
450
+ const list = cft._listInstalled()
451
+ return list.length > 0
452
+ }
453
+
454
+ _cftVersion() {
455
+ const cft = new CftGroup()
456
+ const active = cft._readActive()
457
+ return active || '-'
458
+ }
459
+
460
+ async _cftLatest(proxy) {
461
+ const cft = new CftGroup()
462
+ try {
463
+ const data = await cft._fetchVersions(proxy)
464
+ const versions = data.versions || []
465
+ return versions[versions.length - 1] || '-'
466
+ } catch {
467
+ return '-'
468
+ }
469
+ }
470
+
471
+ async _installCft(proxy) {
472
+ const cft = new CftGroup()
473
+ const version = cft.cfg.getDefaultVersion()
474
+ if (!version) {
475
+ throw new Error('No cft default version configured')
476
+ }
477
+ const existing = cft._readInstalledInfo(version)
478
+ if (existing) {
479
+ return { action: 'skipped', reason: `cft ${version} already installed`, version }
480
+ }
481
+ await cft._install([version])
482
+ return { action: 'installed', version }
483
+ }
484
+
485
+ async _uninstallCft() {
486
+ const cft = new CftGroup()
487
+ await cft._clear([])
488
+ return { action: 'uninstalled' }
489
+ }
490
+
491
+ async _npmInstalled(name) {
492
+ const npm = new NpmManager()
493
+ const result = await npm.isInstalled(name, { global: true })
494
+ return result.installed
495
+ }
496
+
497
+ async _npmVersion(name) {
498
+ const npm = new NpmManager()
499
+ const result = await npm.isInstalled(name, { global: true })
500
+ return result.version || '-'
501
+ }
502
+
503
+ async _npmLatest(name, proxy) {
504
+ const npm = new NpmManager()
505
+ try {
506
+ const data = await npm.searchPackage(name, null, { proxy })
507
+ return data.latest || '-'
508
+ } catch {
509
+ return '-'
510
+ }
511
+ }
512
+
513
+ async _installNpmPackage(name, proxy) {
514
+ const npm = new NpmManager()
515
+ const existing = await npm.isInstalled(name, { global: true })
516
+ if (existing.installed) {
517
+ return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
518
+ }
519
+ return npm.install(name, null, { global: true, proxy })
520
+ }
521
+
522
+ async _upgradeNpmPackage(name, proxy) {
523
+ const npm = new NpmManager()
524
+ return npm.upgrade(name, { global: true, proxy })
525
+ }
526
+
527
+ async _uninstallNpmPackage(name) {
528
+ const npm = new NpmManager()
529
+ return npm.uninstall(name, { global: true })
530
+ }
531
+
532
+ printHelp() {
533
+ const actionLines = Object.entries(ACTIONS)
534
+ .map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
535
+ .join('\n')
536
+ console.log(`
537
+ Usage: opm webwork <action> [options]
538
+
539
+ WebWork development environment orchestration.
540
+ Components (in install order): python, blues-lib, cft, node, opencode, harness, sshproxy
541
+
542
+ Actions:
543
+ ${actionLines}
544
+
545
+ Options:
546
+ -p, --proxy <url> Proxy for install/upgrade/versions (overrides config proxy.active)
547
+ -h, --help Show this help
548
+
549
+ Examples:
550
+ opm webwork version Show installed versions of all components
551
+ opm webwork versions Show latest available versions (remote)
552
+ opm webwork installed Show install status of all components
553
+ opm webwork install Install all components in order
554
+ opm webwork uninstall Uninstall all components in reverse order
555
+ opm webwork upgrade Upgrade all components to latest
556
+ `)
557
+ }
558
+ }