@wwkit/opm 1.0.9 → 1.0.11

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.
@@ -1,73 +1,50 @@
1
1
  /**
2
2
  * webwork 命令组 — WebWork 开发环境一键安装/管理
3
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,已安装则跳过)
4
+ * 只负责运行环境本身(node/python/blues-lib),插件(cft/opencode/harness/sshproxy)
5
+ * 由 blues-lib 提供的 ww init / ww init -u 全权处理,不再单独安装/检查。
12
6
  *
13
- * npm 包安装使用 --dangerously-allow-all-scripts(npm >= 11)放行 postinstall 脚本。
7
+ * install 流程(5 步):
8
+ * 镜像检查(必查 npm/pip/cft + 按需 nvm/node/uv) → ensure node/python → 写 NPM/PIP/CFT_REGISTRY → 装 blues-lib → ww init
9
+ * upgrade 流程(4 步):
10
+ * 镜像检查(必查 npm/pip/cft) → 写 NPM/PIP/CFT_REGISTRY → pip 升级 blues-lib → ww init -u
14
11
  *
15
- * install 流程:镜像可达检查 → 安装组件 → 初始化 → 验证
16
- * init 流程:ww config init → 写运行时路径(cft/harness) → ww sqlite init → ww web init → summary
17
- * verify 流程:ww doctor → ww sqlite debug → ww run debug → ww web status → ww webdriver debug
18
- *
19
- * 命令: version / versions / installed / install / init / verify / uninstall / upgrade / help
12
+ * 命令: version / versions / installed / install / uninstall / upgrade / help
20
13
  */
21
14
 
22
- import fs from 'node:fs'
23
- import path from 'node:path'
24
15
  import { execSync } from 'node:child_process'
25
16
 
26
- import { Shell, getXdgConfigDir } from '@wwkit/shared'
17
+ import { Shell } from '@wwkit/shared'
27
18
  import { parseFlags } from '../../cli/helpers/args.js'
28
19
  import { output } from '../../formatter.js'
29
20
  import { getActiveProxy, getConfig, getActiveRegistry } from '../../config.js'
30
- import { NpmManager } from '../../managers/npm.js'
31
21
  import { PipManager } from '../../managers/pip.js'
32
22
  import { PythonManager, isUvInstalled, installUv } from '../../managers/python.js'
33
23
  import { NodeManager, isNvmInstalled, installNvm } from '../../managers/node.js'
34
- import { CftGroup } from '../../binaries/cft/index.js'
35
24
  import { checkReachability } from '../../cli/groups/ping.js'
36
25
 
37
26
  const shell = new Shell()
38
27
 
39
28
  const COMPONENTS = [
40
29
  { key: 'python', label: 'Python' },
41
- { key: 'blues-lib', label: 'blues-lib' },
42
- { key: 'cft', label: 'cft' },
43
30
  { key: 'node', label: 'Node.js' },
44
- { key: 'opencode', label: 'opencode' },
45
- { key: 'harness', label: 'harness' },
46
- { key: 'sshproxy', label: 'sshproxy' },
31
+ { key: 'blues-lib', label: 'blues-lib' },
47
32
  ]
48
33
 
49
- const NPM_PACKAGES = {
50
- opencode: 'opencode-ai',
51
- harness: '@wwkit/harness',
52
- sshproxy: '@wwkit/sshproxy',
53
- }
54
-
55
34
  const ACTIONS = {
56
35
  version: { desc: 'Show installed version of each component' },
57
36
  versions: { desc: 'Show latest available version of each component (remote)' },
58
37
  installed: { desc: 'Show install status (true/false) of each component' },
59
- install: { desc: 'Install all components + init + verify' },
60
- init: { desc: 'Initialize runtime (config/webdriver/opencode/sqlite)' },
61
- verify: { desc: 'Verify installation (sqlite/run/web/webdriver)' },
38
+ install: { desc: 'Mirror check + ensure runtime + write registry env + blues-lib + ww init' },
62
39
  uninstall: { desc: 'Uninstall all components in reverse order' },
63
- upgrade: { desc: 'Upgrade all components to latest' },
40
+ upgrade: { desc: 'Upgrade blues-lib + ww init -u' },
64
41
  help: { desc: 'Show this help' },
65
42
  }
66
43
 
67
44
  export class WebworkGroup {
68
45
  constructor() {
69
46
  this.name = 'webwork'
70
- this.desc = 'WebWork dev environment (python/blues-lib/cft/node/opencode/harness/sshproxy)'
47
+ this.desc = 'WebWork dev environment (python/node/blues-lib + ww init)'
71
48
  }
72
49
 
73
50
  async run(argv) {
@@ -89,10 +66,6 @@ export class WebworkGroup {
89
66
  return this._installed()
90
67
  case 'install':
91
68
  return this._install(parsed)
92
- case 'init':
93
- return this._init(parsed)
94
- case 'verify':
95
- return this._verify(parsed)
96
69
  case 'uninstall':
97
70
  return this._uninstall(parsed)
98
71
  case 'upgrade':
@@ -115,12 +88,8 @@ export class WebworkGroup {
115
88
  const result = {}
116
89
 
117
90
  result['python'] = await this._pythonVersion()
118
- result['blues-lib'] = await this._pipVersion('blues-lib')
119
- result['cft'] = this._cftVersion()
120
91
  result['node'] = this._nodeVersion()
121
- result['opencode'] = await this._npmVersion(NPM_PACKAGES.opencode)
122
- result['harness'] = await this._npmVersion(NPM_PACKAGES.harness)
123
- result['sshproxy'] = await this._npmVersion(NPM_PACKAGES.sshproxy)
92
+ result['blues-lib'] = await this._pipVersion('blues-lib')
124
93
 
125
94
  output(result)
126
95
  }
@@ -130,12 +99,8 @@ export class WebworkGroup {
130
99
  const result = {}
131
100
 
132
101
  result['python'] = await this._pythonLatest(proxy)
133
- result['blues-lib'] = await this._pipLatest('blues-lib', proxy)
134
- result['cft'] = await this._cftLatest(proxy)
135
102
  result['node'] = await this._nodeLatest(proxy)
136
- result['opencode'] = await this._npmLatest(NPM_PACKAGES.opencode, proxy)
137
- result['harness'] = await this._npmLatest(NPM_PACKAGES.harness, proxy)
138
- result['sshproxy'] = await this._npmLatest(NPM_PACKAGES.sshproxy, proxy)
103
+ result['blues-lib'] = await this._pipLatest('blues-lib', proxy)
139
104
 
140
105
  output(result)
141
106
  }
@@ -144,19 +109,15 @@ export class WebworkGroup {
144
109
  const result = {}
145
110
 
146
111
  result['python'] = this._pythonInstalled()
147
- result['blues-lib'] = await this._pipInstalled('blues-lib')
148
- result['cft'] = this._cftInstalled()
149
112
  result['node'] = this._nodeInstalled()
150
- result['opencode'] = await this._npmInstalled(NPM_PACKAGES.opencode)
151
- result['harness'] = await this._npmInstalled(NPM_PACKAGES.harness)
152
- result['sshproxy'] = await this._npmInstalled(NPM_PACKAGES.sshproxy)
113
+ result['blues-lib'] = await this._pipInstalled('blues-lib')
153
114
 
154
115
  output(result)
155
116
  }
156
117
 
157
- /**
158
- * 批量执行组件操作(install/uninstall/upgrade 共用)
159
- * 每步打印 [n/total] 组件名 + 跳过/成功/失败日志,最后汇总
118
+ /**
119
+ * 批量执行组件操作(uninstall 用)
120
+ * 每步打印 [n/total] 组件名 + 跳过/成功/失败日志,最后汇总
160
121
  * @param {string} verb - install|uninstall|upgrade
161
122
  * @param {Array} components - 组件列表
162
123
  * @param {string} proxy - 代理地址
@@ -200,56 +161,140 @@ export class WebworkGroup {
200
161
  async _install(parsed) {
201
162
  const proxy = this._resolveProxy(parsed)
202
163
 
203
- console.log('\n========== 步骤 1/3: 检查镜像可达性 ==========')
204
- await this._checkMirrors(proxy)
164
+ console.log('\n========== 步骤 1/5: 镜像可达检查 ==========')
165
+ const env = await this._checkEnv()
166
+ await this._checkMirrors(proxy, env)
205
167
 
206
- console.log('\n========== 步骤 2/3: 安装组件 ==========')
207
- await this._runBatch('install', COMPONENTS, proxy, (key, p) => this._installComponent(key, p))
168
+ console.log('\n========== 步骤 2/5: Ensure node / python ==========')
169
+ const ensured = []
170
+ if (!env.node.satisfied) {
171
+ console.log(` [ENSURE] node >= ${env.node.required}...`)
172
+ ensured.push(await this._installNode(proxy))
173
+ } else {
174
+ ensured.push({ action: 'skipped', reason: `node ${env.node.provided} already satisfied` })
175
+ console.log(` → node ${env.node.provided} 已满足`)
176
+ }
177
+ if (!env.python.satisfied) {
178
+ console.log(` [ENSURE] python >= ${env.python.required}...`)
179
+ ensured.push(await this._installPython(proxy))
180
+ } else {
181
+ ensured.push({ action: 'skipped', reason: `python ${env.python.provided} already satisfied` })
182
+ console.log(` → python ${env.python.provided} 已满足`)
183
+ }
208
184
 
209
- console.log('\n========== 步骤 3/3: 初始化 + 验证 ==========')
210
- await this._init(parsed)
211
- await this._verify(parsed)
185
+ console.log('\n========== 步骤 3/5: 写入 NPM_REGISTRY / PIP_REGISTRY / CFT_REGISTRY ==========')
186
+ this._writeRegistryEnv()
187
+
188
+ console.log('\n========== 步骤 4/5: 安装 blues-lib ==========')
189
+ const blue = await this._installPipPackage('blues-lib', proxy)
190
+ if (blue.action === 'skipped') {
191
+ console.log(` → blues-lib ${blue.version} 已安装,跳过`)
192
+ } else {
193
+ console.log(` ✓ blues-lib 安装完成(提供 ww 命令)`)
194
+ }
195
+
196
+ console.log('\n========== 步骤 5/5: ww init ==========')
197
+ execSync('ww init', { encoding: 'utf8', stdio: 'inherit' })
198
+
199
+ output({ action: 'install', env, ensured, bluesLib: blue, init: 'ww init' })
212
200
  }
213
201
 
214
- async _uninstall(parsed) {
202
+ async _upgrade(parsed) {
215
203
  const proxy = this._resolveProxy(parsed)
216
- const reversed = [...COMPONENTS].reverse()
217
- await this._runBatch('uninstall', reversed, proxy, (key, p) => this._uninstallComponent(key, p))
204
+
205
+ console.log('\n========== 步骤 1/4: 镜像可达检查 ==========')
206
+ await this._checkMirrors(proxy, null)
207
+
208
+ console.log('\n========== 步骤 2/4: 写入 NPM_REGISTRY / PIP_REGISTRY / CFT_REGISTRY ==========')
209
+ this._writeRegistryEnv()
210
+
211
+ console.log('\n========== 步骤 3/4: 升级 blues-lib ==========')
212
+ const blue = await this._upgradePipPackage('blues-lib', proxy)
213
+ console.log(` ✓ blues-lib 升级完成`)
214
+
215
+ console.log('\n========== 步骤 4/4: ww init -u ==========')
216
+ execSync('ww init -u', { encoding: 'utf8', stdio: 'inherit' })
217
+
218
+ output({ action: 'upgrade', bluesLib: blue, init: 'ww init -u' })
218
219
  }
219
220
 
220
- async _upgrade(parsed) {
221
+ /**
222
+ * 将 NPM_REGISTRY / PIP_REGISTRY / CFT_REGISTRY 写入 shell profile(与 SELENIUM_CFT_DIR 同机制)
223
+ * blues-lib 的相关命令依赖镜像,写入后保持与 opm 配置一致
224
+ */
225
+ _writeRegistryEnv() {
226
+ const entries = [
227
+ ['NPM_REGISTRY', getActiveRegistry('npm')],
228
+ ['PIP_REGISTRY', getActiveRegistry('pip')],
229
+ ['CFT_REGISTRY', getActiveRegistry('cft')],
230
+ ]
231
+ for (const [name, url] of entries) {
232
+ if (!url) continue
233
+ shell.setEnvVar(name, url)
234
+ console.log(` [OK] ${name}=${url}`)
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 环境检查:node >= defaultVersion、python >= defaultVersion
240
+ * @returns {Promise<{ node: Object, python: Object }>}
241
+ */
242
+ async _checkEnv() {
243
+ const nodeVersion = this._nodeVersion()
244
+ const nodeRequired = getConfig().node?.defaultVersion || '24'
245
+ const nodeSatisfied = nodeVersion !== '-' && this._compareVersion(nodeVersion, nodeRequired) >= 0
246
+
247
+ const pythonVersion = await this._pythonVersion()
248
+ const pythonRequired = getConfig().python?.defaultVersion || '3.12'
249
+ const pythonSatisfied = pythonVersion !== '-' && this._compareVersion(pythonVersion, pythonRequired) >= 0
250
+
251
+ const env = {
252
+ node: { provided: nodeVersion, required: nodeRequired, satisfied: nodeSatisfied },
253
+ python: { provided: pythonVersion, required: pythonRequired, satisfied: pythonSatisfied },
254
+ }
255
+ console.log(` node: ${nodeVersion} (required >= ${nodeRequired}) ${nodeSatisfied ? '✓' : '✗'}`)
256
+ console.log(` python: ${pythonVersion} (required >= ${pythonRequired}) ${pythonSatisfied ? '✓' : '✗'}`)
257
+
258
+ return env
259
+ }
260
+
261
+ async _uninstall(parsed) {
221
262
  const proxy = this._resolveProxy(parsed)
222
- await this._runBatch('upgrade', COMPONENTS, proxy, (key, p) => this._upgradeComponent(key, p))
263
+ const reversed = [...COMPONENTS].reverse()
264
+ await this._runBatch('uninstall', reversed, proxy, (key, p) => this._uninstallComponent(key, p))
223
265
  }
224
266
 
225
267
  /**
226
- * 检查所有镜像可达性(HEAD 请求,不可达则抛错)
268
+ * 检查镜像可达性(HEAD 请求,不可达则抛错)
269
+ * 必查:npm / pip / cft(blues-lib 依赖,环境必然可用)
270
+ * 条件查:nvm + node(仅 env.node 不满足时)、uv(仅 env.python 不满足时)
227
271
  * @param {string} proxy
272
+ * @param {{ node: { satisfied: boolean }, python: { satisfied: boolean } } | null} env
273
+ * null 表示不做条件检查(upgrade 场景,不装 runtime)
228
274
  */
229
- async _checkMirrors(proxy) {
275
+ async _checkMirrors(proxy, env) {
230
276
  const tool = shell.getNetProbeTool()
231
277
  if (!tool) {
232
278
  console.log(' [WARN] No network probe tool (ping/curl/wget), skipping mirror check')
233
279
  return
234
280
  }
235
281
 
282
+ const needNode = env ? !env.node.satisfied : false
283
+ const needPython = env ? !env.python.satisfied : false
284
+
236
285
  const checks = [
237
286
  { name: 'npm', url: getActiveRegistry('npm') },
238
287
  { name: 'pip', url: getActiveRegistry('pip') },
288
+ { name: 'cft', url: getActiveRegistry('cft') },
289
+ ...(needNode ? [
290
+ { name: 'nvm', url: getActiveRegistry('nvm') },
291
+ { name: 'node', url: getActiveRegistry('node') },
292
+ ] : []),
293
+ ...(needPython ? [
294
+ { name: 'uv', url: getActiveRegistry('uv') },
295
+ ] : []),
239
296
  ]
240
297
 
241
- if (!isNvmInstalled()) {
242
- checks.push({ name: 'nvm', url: getActiveRegistry('nvm') })
243
- checks.push({ name: 'node', url: getActiveRegistry('node') })
244
- }
245
-
246
- const tool2 = new CftGroup()
247
- if (tool2._listInstalled().length === 0) {
248
- const config = getConfig()
249
- const cftUrl = config.cft?.registry?.active || config.cft?.registry?.npmmirror || ''
250
- if (cftUrl) checks.push({ name: 'cft', url: cftUrl })
251
- }
252
-
253
298
  let allOk = true
254
299
  for (const { name, url } of checks) {
255
300
  if (!url) {
@@ -275,269 +320,14 @@ export class WebworkGroup {
275
320
  }
276
321
  }
277
322
 
278
- /**
279
- * 运行时初始化(依赖 blues-lib 已安装)
280
- * @param {object} parsed
281
- */
282
- async _init(parsed) {
283
- const steps = [
284
- { name: 'config', cmd: 'ww config init', required: true },
285
- { name: 'sqlite', cmd: 'ww sqlite init', required: true },
286
- { name: 'web', cmd: 'ww web init', required: true, hint: 'Start web service :7901' },
287
- ]
288
-
289
- for (const step of steps) {
290
- console.log(` [INIT] ${step.name}...`)
291
- try {
292
- execSync(step.cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
293
- console.log(` [OK] ${step.name}`)
294
- if (step.name === 'config') {
295
- this._writeRuntimePaths()
296
- }
297
- } catch (err) {
298
- if (step.required) {
299
- throw new Error(`${step.name} init failed: ${err.message}`)
300
- }
301
- console.error(` [WARN] ${step.name}: ${err.message}`)
302
- }
303
- }
304
-
305
- await this._summary()
306
- }
307
-
308
- /**
309
- * 将 cft/harness 的实际安装路径写入 webwork.json5(替代 post.py 的配置集成)
310
- * 在 ww config init 之后执行,确保 webwork.json5 已存在
311
- */
312
- _writeRuntimePaths() {
313
- const configDir = getXdgConfigDir('webwork')
314
- let configPath = null
315
- for (const fname of ['webwork.json5', 'webwork.json']) {
316
- const p = path.join(configDir, fname)
317
- if (fs.existsSync(p)) { configPath = p; break }
318
- }
319
- if (!configPath) {
320
- console.log(' [SKIP] webwork.json5 not found, skip runtime paths')
321
- return
322
- }
323
-
324
- let cfg
325
- try {
326
- cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'))
327
- } catch {
328
- console.log(' [SKIP] webwork.json5 parse failed, skip runtime paths')
329
- return
330
- }
331
-
332
- let changed = false
333
-
334
- const cft = new CftGroup()
335
- const cftActive = cft._readActive()
336
- if (cftActive) {
337
- const cftPath = cft._versionRoot(cftActive)
338
- if (fs.existsSync(path.join(cftPath, 'installed.json'))) {
339
- cfg.webdriver = cfg.webdriver || {}
340
- cfg.webdriver.chromium = cfg.webdriver.chromium || {}
341
- cfg.webdriver.chromium.cft = cfg.webdriver.chromium.cft || {}
342
- cfg.webdriver.chromium.cft.location = cftPath
343
- console.log(` [OK] webdriver.chromium.cft.location = ${cftPath}`)
344
- changed = true
345
- }
346
- }
347
-
348
- try {
349
- const npmRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
350
- const harnessDir = path.join(npmRoot, '@wwkit', 'harness')
351
- if (fs.existsSync(harnessDir)) {
352
- cfg.plugin = cfg.plugin || {}
353
- cfg.plugin.opencode = cfg.plugin.opencode || {}
354
- cfg.plugin.opencode.config_dir = harnessDir
355
- console.log(` [OK] plugin.opencode.config_dir = ${harnessDir}`)
356
- changed = true
357
- }
358
- } catch {}
359
-
360
- if (changed) {
361
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8')
362
- console.log(' [OK] webwork.json5 updated with runtime paths')
363
- }
364
- }
365
-
366
- /**
367
- * 打印安装状态汇总(对齐 ww init 的 _step_summary)
368
- */
369
- async _summary() {
370
- console.log(' [SUMMARY]')
371
- console.log(` OS: ${process.platform} ${process.arch}`)
372
- console.log(` Node: ${process.version}`)
373
- try {
374
- const npmVer = execSync('npm --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
375
- console.log(` npm: ${npmVer}`)
376
- } catch {}
377
- console.log(' [Components]')
378
- for (const { key, label } of COMPONENTS) {
379
- try {
380
- const installed = await this._isComponentInstalled(key)
381
- const version = await this._getComponentVersion(key)
382
- console.log(` ${label.padEnd(14)} ${version || '-'} ${installed ? '✓' : '✗'}`)
383
- } catch {
384
- console.log(` ${label.padEnd(14)} - ?`)
385
- }
386
- }
387
- console.log(' [Config]')
388
- try {
389
- const npmRegistry = getActiveRegistry('npm')
390
- console.log(` npm mirror: ${npmRegistry || '(未配置)'}`)
391
- } catch {}
392
- }
393
-
394
- async _isComponentInstalled(key) {
395
- switch (key) {
396
- case 'python': return this._pythonInstalled()
397
- case 'blues-lib': return await this._pipInstalled('blues-lib')
398
- case 'cft': return this._cftInstalled()
399
- case 'node': return this._nodeInstalled()
400
- case 'opencode': return await this._npmInstalled(NPM_PACKAGES.opencode)
401
- case 'harness': return await this._npmInstalled(NPM_PACKAGES.harness)
402
- case 'sshproxy': return await this._npmInstalled(NPM_PACKAGES.sshproxy)
403
- default: return false
404
- }
405
- }
406
-
407
- async _getComponentVersion(key) {
408
- switch (key) {
409
- case 'python': return await this._pythonVersion()
410
- case 'blues-lib': return await this._pipVersion('blues-lib')
411
- case 'cft': return this._cftVersion()
412
- case 'node': return this._nodeVersion()
413
- case 'opencode': return await this._npmVersion(NPM_PACKAGES.opencode)
414
- case 'harness': return await this._npmVersion(NPM_PACKAGES.harness)
415
- case 'sshproxy': return await this._npmVersion(NPM_PACKAGES.sshproxy)
416
- default: return '-'
417
- }
418
- }
419
-
420
- /**
421
- * 验证安装结果
422
- * @param {object} parsed
423
- */
424
- async _verify(parsed) {
425
- console.log(' [VERIFY] doctor (ww doctor)...')
426
- try {
427
- execSync('ww doctor', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
428
- console.log(' [OK] doctor')
429
- } catch (err) {
430
- const out = err.stdout ? err.stdout.toString().trim() : ''
431
- console.error(` [WARN] doctor:\n${out || err.message}`)
432
- }
433
-
434
- const checks = [
435
- { name: 'sqlite', cmd: 'ww sqlite debug', pattern: /访问成功|ok|success/i },
436
- { name: 'run', cmd: 'ww run debug', pattern: /run debug/i },
437
- { name: 'web', cmd: 'ww web status', pattern: /running/i, optional: true },
438
- ]
439
-
440
- for (const check of checks) {
441
- console.log(` [VERIFY] ${check.name}...`)
442
- try {
443
- const out = execSync(check.cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
444
- if (check.pattern.test(out)) {
445
- console.log(` [OK] ${check.name}`)
446
- } else {
447
- if (check.optional) {
448
- console.log(` [WARN] ${check.name}: not running (optional)`)
449
- } else {
450
- console.error(` [FAIL] ${check.name}: output doesn't match expected pattern`)
451
- }
452
- }
453
- } catch (err) {
454
- if (check.optional) {
455
- console.log(` [WARN] ${check.name}: ${err.message}`)
456
- } else {
457
- console.error(` [FAIL] ${check.name}: ${err.message}`)
458
- }
459
- }
460
- }
461
-
462
- try {
463
- const staticIndex = execSync(
464
- `python3 -c "import os, blues_lib.web; print(os.path.join(os.path.dirname(blues_lib.web.__file__), 'static', 'index.html'))"`,
465
- { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
466
- ).trim()
467
- if (staticIndex) {
468
- console.log(` [VERIFY] webdriver...`)
469
- const out = execSync(`ww webdriver debug -u file://${staticIndex} --headless`, {
470
- encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000
471
- })
472
- if (/webwork/i.test(out)) {
473
- console.log(` [OK] webdriver`)
474
- } else {
475
- console.error(` [FAIL] webdriver: output doesn't contain 'webwork'`)
476
- }
477
- }
478
- } catch (err) {
479
- console.error(` [WARN] webdriver: ${err.message}`)
480
- }
481
- }
482
-
483
- async _installComponent(key, proxy) {
484
- switch (key) {
485
- case 'python':
486
- return this._installPython(proxy)
487
- case 'blues-lib':
488
- return this._installPipPackage('blues-lib', proxy)
489
- case 'cft':
490
- return this._installCft(proxy)
491
- case 'node':
492
- return this._installNode(proxy)
493
- case 'opencode':
494
- return this._installNpmPackage(NPM_PACKAGES.opencode, proxy)
495
- case 'harness':
496
- return this._installNpmPackage(NPM_PACKAGES.harness, proxy)
497
- case 'sshproxy':
498
- return this._installNpmPackage(NPM_PACKAGES.sshproxy, proxy)
499
- default:
500
- throw new Error(`Unknown component: ${key}`)
501
- }
502
- }
503
-
504
323
  async _uninstallComponent(key, proxy) {
505
324
  switch (key) {
506
325
  case 'python':
507
326
  return { action: 'skipped', reason: 'python is a system dependency, not uninstalled' }
508
- case 'blues-lib':
509
- return this._uninstallPipPackage('blues-lib')
510
- case 'cft':
511
- return this._uninstallCft()
512
327
  case 'node':
513
328
  return { action: 'skipped', reason: 'node is a system dependency, not uninstalled' }
514
- case 'opencode':
515
- return this._uninstallNpmPackage(NPM_PACKAGES.opencode)
516
- case 'harness':
517
- return this._uninstallNpmPackage(NPM_PACKAGES.harness)
518
- case 'sshproxy':
519
- return this._uninstallNpmPackage(NPM_PACKAGES.sshproxy)
520
- default:
521
- throw new Error(`Unknown component: ${key}`)
522
- }
523
- }
524
-
525
- async _upgradeComponent(key, proxy) {
526
- switch (key) {
527
- case 'python':
528
- return this._upgradePython(proxy)
529
329
  case 'blues-lib':
530
- return this._upgradePipPackage('blues-lib', proxy)
531
- case 'cft':
532
- return this._installCft(proxy)
533
- case 'node':
534
- return this._upgradeNode(proxy)
535
- case 'opencode':
536
- return this._upgradeNpmPackage(NPM_PACKAGES.opencode, proxy)
537
- case 'harness':
538
- return this._upgradeNpmPackage(NPM_PACKAGES.harness, proxy)
539
- case 'sshproxy':
540
- return this._upgradeNpmPackage(NPM_PACKAGES.sshproxy, proxy)
330
+ return this._uninstallPipPackage('blues-lib')
541
331
  default:
542
332
  throw new Error(`Unknown component: ${key}`)
543
333
  }
@@ -609,22 +399,6 @@ export class WebworkGroup {
609
399
  return { action: 'installed', version: defaultVersion }
610
400
  }
611
401
 
612
- async _upgradePython(proxy) {
613
- if (!isUvInstalled()) {
614
- console.log(' uv not found, installing uv...')
615
- installUv(proxy)
616
- }
617
- const mgr = new PythonManager()
618
- const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
619
- const versions = mgr._parseRemote(out)
620
- const latest = versions[versions.length - 1]
621
- if (!latest) {
622
- return { action: 'skipped', reason: 'no remote version found' }
623
- }
624
- mgr._execInherit(mgr.cfg.installCmd(latest), { proxy })
625
- return { action: 'upgraded', version: latest }
626
- }
627
-
628
402
  _nodeInstalled() {
629
403
  return this._nodeVersion() !== '-'
630
404
  }
@@ -672,25 +446,6 @@ export class WebworkGroup {
672
446
  return { action: 'installed', version: defaultVersion }
673
447
  }
674
448
 
675
- async _upgradeNode(proxy) {
676
- if (!isNvmInstalled()) {
677
- console.log(' nvm not found, installing nvm...')
678
- installNvm(proxy)
679
- }
680
- const mgr = new NodeManager()
681
- const out = mgr._exec(mgr.cfg.listRemoteCmd, { allowNonZero: true, proxy })
682
- const versions = mgr._parseRemote(out)
683
- const latest = versions[versions.length - 1]
684
- if (!latest) {
685
- return { action: 'skipped', reason: 'no remote version found' }
686
- }
687
- const ver = latest.replace(/^v/, '')
688
- mgr._execInherit(mgr.cfg.installCmd(ver), { proxy })
689
- mgr._execInherit(mgr.cfg.useCmd(ver), { proxy })
690
- mgr._execInherit(mgr.cfg.defaultCmd(ver), { proxy })
691
- return { action: 'upgraded', version: ver }
692
- }
693
-
694
449
  async _pipInstalled(name) {
695
450
  const pip = new PipManager()
696
451
  const result = await pip.isInstalled(name, { global: true })
@@ -728,90 +483,6 @@ export class WebworkGroup {
728
483
  return pip.uninstall(name, { global: true })
729
484
  }
730
485
 
731
- _cftInstalled() {
732
- const cft = new CftGroup()
733
- const list = cft._listInstalled()
734
- return list.length > 0
735
- }
736
-
737
- _cftVersion() {
738
- const cft = new CftGroup()
739
- const active = cft._readActive()
740
- return active || '-'
741
- }
742
-
743
- async _cftLatest(proxy) {
744
- const cft = new CftGroup()
745
- try {
746
- const data = await cft._fetchVersions(proxy)
747
- const versions = data.versions || []
748
- return versions[versions.length - 1] || '-'
749
- } catch {
750
- return '-'
751
- }
752
- }
753
-
754
- async _installCft(proxy) {
755
- const cft = new CftGroup()
756
- const version = cft.cfg.getDefaultVersion()
757
- if (!version) {
758
- throw new Error('No cft default version configured')
759
- }
760
- const existing = cft._readInstalledInfo(version)
761
- if (existing) {
762
- return { action: 'skipped', reason: `cft ${version} already installed`, version }
763
- }
764
- await cft._install([version])
765
- return { action: 'installed', version }
766
- }
767
-
768
- async _uninstallCft() {
769
- const cft = new CftGroup()
770
- await cft._clear([])
771
- return { action: 'uninstalled' }
772
- }
773
-
774
- async _npmInstalled(name) {
775
- const npm = new NpmManager()
776
- const result = await npm.isInstalled(name, { global: true })
777
- return result.installed
778
- }
779
-
780
- async _npmVersion(name) {
781
- const npm = new NpmManager()
782
- const result = await npm.isInstalled(name, { global: true })
783
- return result.version || '-'
784
- }
785
-
786
- async _npmLatest(name, proxy) {
787
- const npm = new NpmManager()
788
- try {
789
- const data = await npm.searchPackage(name, null, { proxy })
790
- return data.latest || '-'
791
- } catch {
792
- return '-'
793
- }
794
- }
795
-
796
- async _installNpmPackage(name, proxy) {
797
- const npm = new NpmManager()
798
- const existing = await npm.isInstalled(name, { global: true })
799
- if (existing.installed) {
800
- return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
801
- }
802
- return npm.install(name, null, { global: true, proxy, allowScripts: true })
803
- }
804
-
805
- async _upgradeNpmPackage(name, proxy) {
806
- const npm = new NpmManager()
807
- return npm.upgrade(name, { global: true, proxy, allowScripts: true })
808
- }
809
-
810
- async _uninstallNpmPackage(name) {
811
- const npm = new NpmManager()
812
- return npm.uninstall(name, { global: true })
813
- }
814
-
815
486
  printHelp() {
816
487
  const actionLines = Object.entries(ACTIONS)
817
488
  .map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
@@ -820,7 +491,7 @@ export class WebworkGroup {
820
491
  Usage: opm webwork <action> [options]
821
492
 
822
493
  WebWork development environment orchestration.
823
- Components (in install order): python, blues-lib, cft, node, opencode, harness, sshproxy
494
+ Components: python, node, blues-lib (plugins cft/opencode/harness/sshproxy handled by ww init)
824
495
 
825
496
  Actions:
826
497
  ${actionLines}
@@ -833,11 +504,9 @@ Examples:
833
504
  opm webwork version Show installed versions of all components
834
505
  opm webwork versions Show latest available versions (remote)
835
506
  opm webwork installed Show install status of all components
836
- opm webwork install Full install (mirror check + components + init + verify)
837
- opm webwork init Initialize runtime only (config/webdriver/opencode/sqlite)
838
- opm webwork verify Verify installation only
507
+ opm webwork install Mirror check + ensure runtime + registry env + blues-lib + ww init
839
508
  opm webwork uninstall Uninstall all components in reverse order
840
- opm webwork upgrade Upgrade all components to latest
509
+ opm webwork upgrade Upgrade blues-lib + ww init -u
841
510
  `)
842
511
  }
843
512
  }