@wwkit/opm 1.0.9 → 1.0.10
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 +21 -4
- package/docs/managers/nvm.md +2 -0
- package/docs/managers/phpenv.md +2 -0
- package/docs/managers/uv.md +2 -0
- package/docs/opm.html +6 -3
- package/package.json +3 -3
- package/src/cli/groups/ensure.js +25 -4
- package/src/cli/groups/nvm.js +5 -1
- package/src/cli/groups/phpenv.js +5 -1
- package/src/cli/groups/uv.js +5 -1
- package/src/cli/groups/version-manager.js +27 -0
- package/src/cli/index.js +2 -0
- package/src/harnesses/base.js +88 -20
- package/src/harnesses/harness/index.js +132 -0
- package/src/index.js +1 -0
- package/src/managers/node.js +17 -8
- package/src/managers/php.js +17 -8
- package/src/managers/python.js +18 -8
- package/src/tools/webwork/index.js +132 -463
|
@@ -1,73 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* webwork 命令组 — WebWork 开发环境一键安装/管理
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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['
|
|
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['
|
|
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['
|
|
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
|
-
|
|
159
|
-
|
|
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/
|
|
204
|
-
await this.
|
|
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/
|
|
207
|
-
|
|
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/
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
202
|
+
async _upgrade(parsed) {
|
|
215
203
|
const proxy = this._resolveProxy(parsed)
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
509
|
+
opm webwork upgrade Upgrade blues-lib + ww init -u
|
|
841
510
|
`)
|
|
842
511
|
}
|
|
843
512
|
}
|