@wwkit/opm 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwkit/opm",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "author": "bluesliu <langcai163@163.com>",
5
5
  "description": "Unified CLI — package management (npm/pip/dnf/apt) + config view + opencode maintenance",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "basic-ftp": "^6.2.1",
36
36
  "extract-zip": "^2.0.1",
37
37
  "systeminformation": "^5.23.0",
38
- "@wwkit/shared": "1.0.7"
38
+ "@wwkit/shared": "1.0.9"
39
39
  },
40
40
  "publishConfig": {
41
41
  "registry": "https://registry.npmjs.org/",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copyBuiltinConfig, getUserConfigFile } from '../src/config.js'
3
+ import { mergeBuiltinConfig, getUserConfigFile } from '../src/config.js'
4
4
 
5
- if (copyBuiltinConfig()) {
6
- console.log(`opm: wrote default config to ${getUserConfigFile()}`)
7
- }
5
+ if (mergeBuiltinConfig()) {
6
+ console.log(`opm: config updated at ${getUserConfigFile()}`)
7
+ }
@@ -13,7 +13,6 @@
13
13
 
14
14
  import fs from 'node:fs'
15
15
  import path from 'node:path'
16
- import extractZip from 'extract-zip'
17
16
 
18
17
  import { parseFlags } from '../cli/helpers/args.js'
19
18
  import { output } from '../formatter.js'
@@ -23,6 +22,12 @@ import { getOs, OS_TYPES } from './platform.js'
23
22
  import { find as findExe } from './exefinder.js'
24
23
  import { verify as verifyDeps } from './depchecker.js'
25
24
 
25
+ let _extractZip = null
26
+ async function loadExtractZip() {
27
+ if (!_extractZip) _extractZip = (await import('extract-zip')).default
28
+ return _extractZip
29
+ }
30
+
26
31
  const ACTIONS = {
27
32
  version: { desc: 'Show current (active) version' },
28
33
  installed: { desc: 'Check if a version is installed' },
@@ -354,6 +359,7 @@ async _dir() {
354
359
  }
355
360
  await downloadToFile(url, zipPath, { proxy })
356
361
  try {
362
+ const extractZip = await loadExtractZip()
357
363
  await extractZip(zipPath, { dir: extractRoot })
358
364
  } finally {
359
365
  if (fs.existsSync(zipPath)) {
@@ -63,6 +63,7 @@ const ACTIONS = {
63
63
  uninstall: { desc: 'Uninstall a package' },
64
64
  upgrade: { desc: 'Upgrade a package' },
65
65
  cache: { desc: 'Clean cache' },
66
+ dir: { desc: 'Show install and package directories' },
66
67
  help: { desc: 'Show help for this manager' },
67
68
  }
68
69
 
@@ -236,6 +237,12 @@ export class PackageGroup {
236
237
  break
237
238
  }
238
239
 
240
+ case 'dir': {
241
+ const result = manager.getDir()
242
+ output({ name: this.name, ...result })
243
+ break
244
+ }
245
+
239
246
  default:
240
247
  console.error(`Unknown action: ${action}`)
241
248
  process.exit(1)
@@ -262,6 +269,7 @@ Actions:
262
269
  uninstall <name> Uninstall a package
263
270
  upgrade <name> Upgrade a package to latest
264
271
  cache clean Clean cache
272
+ dir Show install and package directories
265
273
  help Show this help
266
274
 
267
275
  Options:
@@ -250,6 +250,8 @@ async function checkReachability(address, tool, count, proxy) {
250
250
  return { reachable: false, sent: 0, received: 0, loss: 0, lossPercent: 0, rttMin: '', rttAvg: '', rttMax: '', raw: 'No tool available' }
251
251
  }
252
252
 
253
+ export { checkReachability, extractHostPort }
254
+
253
255
  export class PingGroup {
254
256
  constructor() {
255
257
  this.name = 'ping'
package/src/config.js CHANGED
@@ -47,6 +47,14 @@ export function copyBuiltinConfig() {
47
47
  return _loader.copyBuiltinConfig()
48
48
  }
49
49
 
50
+ /**
51
+ * 合并内置配置到用户配置文件(追加缺失 key,不覆盖已有值)
52
+ * @returns {boolean} 是否写入了文件
53
+ */
54
+ export function mergeBuiltinConfig() {
55
+ return _loader.mergeBuiltinConfig()
56
+ }
57
+
50
58
  /**
51
59
  * 按点分路径取配置子节点
52
60
  * @param {object} config - getConfig 的返回结果
@@ -27,7 +27,6 @@ const ACTIONS = {
27
27
  clear: { desc: 'Clear sessions / data' },
28
28
  dir: { desc: 'Show data / db / log / config paths' },
29
29
  env: { desc: 'Show harness environment variables' },
30
- open: { desc: 'Open TUI in a work directory (with env vars injected)' },
31
30
  help: { desc: 'Show this help' },
32
31
  }
33
32
 
@@ -57,11 +56,15 @@ export class HarnessGroup {
57
56
  async run(argv) {
58
57
  const [action, ...rest] = argv
59
58
 
60
- if (!action || action === '-h' || action === '--help' || action === 'help') {
59
+ if (action === '-h' || action === '--help' || action === 'help') {
61
60
  this.printHelp()
62
61
  return
63
62
  }
64
63
 
64
+ if (!action || !ACTIONS[action]) {
65
+ return this._open(argv)
66
+ }
67
+
65
68
  switch (action) {
66
69
  case 'version':
67
70
  return this._version()
@@ -81,8 +84,6 @@ export class HarnessGroup {
81
84
  return this._dir()
82
85
  case 'env':
83
86
  return this._env()
84
- case 'open':
85
- return this._open(rest)
86
87
  default:
87
88
  console.error(`Unknown action: ${action}`)
88
89
  this.printHelp()
@@ -190,14 +191,30 @@ export class HarnessGroup {
190
191
  console.error(`open is not supported for ${this.cfg.name}`)
191
192
  process.exit(1)
192
193
  }
193
- const parsed = parseFlags(rest)
194
- const workDir = parsed.flags.d || parsed.flags.dir || process.cwd()
194
+ let workDir = process.cwd()
195
+ const passThrough = []
196
+ for (let i = 0; i < rest.length; i++) {
197
+ const arg = rest[i]
198
+ if (arg === '-d' || arg === '--dir') {
199
+ const next = rest[i + 1]
200
+ if (next !== undefined && !next.startsWith('-')) {
201
+ workDir = next
202
+ i++
203
+ } else {
204
+ passThrough.push(arg)
205
+ }
206
+ } else if (arg.startsWith('-d=') || arg.startsWith('--dir=')) {
207
+ workDir = arg.slice(arg.indexOf('=') + 1)
208
+ } else {
209
+ passThrough.push(arg)
210
+ }
211
+ }
195
212
  const resolved = path.resolve(workDir)
196
213
  if (!fs.existsSync(resolved)) {
197
214
  console.error(`Directory not found: ${resolved}`)
198
215
  process.exit(1)
199
216
  }
200
- this.cfg.open(resolved)
217
+ this.cfg.open(resolved, passThrough)
201
218
  }
202
219
 
203
220
  printHelp() {
@@ -221,7 +238,7 @@ ${actionLines}
221
238
  ${clearSection}
222
239
  Options:
223
240
  -p, --proxy <url> Proxy for install/upgrade (overrides config proxy.active)
224
- -d, --dir <dir> Working directory for open (default: cwd)
241
+ -d, --dir <dir> Working directory for direct launch (default: cwd)
225
242
  -h, --help Show this help
226
243
 
227
244
  Examples:
@@ -232,8 +249,9 @@ Examples:
232
249
  opm ${this.cfg.name} clear
233
250
  opm ${this.cfg.name} dir
234
251
  opm ${this.cfg.name} env
235
- opm ${this.cfg.name} open Open in current directory
236
- opm ${this.cfg.name} open -d /path/to/project
252
+ opm ${this.cfg.name} Launch TUI in current directory
253
+ opm ${this.cfg.name} -d /path/to/project
254
+ opm ${this.cfg.name} -c config --agent work Pass through args to opencode
237
255
  `)
238
256
  }
239
257
  }
@@ -55,8 +55,9 @@ function getEnvVars() {
55
55
  * 在指定工作目录启动 opencode TUI
56
56
  * 自动注入有值的环境变量(系统 env > opm config)
57
57
  * @param {string} workDir - 工作目录
58
+ * @param {string[]} [passThroughArgs=[]] - Extra args passed to opencode
58
59
  */
59
- function openTui(workDir) {
60
+ function openTui(workDir, passThroughArgs = []) {
60
61
  const env = { ...process.env }
61
62
  const cfgEnv = getEnvVars()
62
63
  for (const key of ENV_VARS) {
@@ -65,17 +66,20 @@ function openTui(workDir) {
65
66
  }
66
67
  }
67
68
 
68
- const cmd = shell.isWindows ? 'opencode.exe' : 'opencode'
69
- const child = spawn(cmd, [], {
69
+ const cmd = 'opencode'
70
+ const child = spawn(cmd, passThroughArgs, {
70
71
  cwd: workDir,
71
72
  stdio: 'inherit',
72
73
  env,
73
- shell: shell.shellForExec(false),
74
+ shell: shell.isWindows,
74
75
  })
75
76
  child.on('error', (err) => {
76
77
  console.error(`Failed to start opencode: ${err.message}`)
77
78
  process.exit(1)
78
79
  })
80
+ child.on('close', (code) => {
81
+ process.exit(code ?? 0)
82
+ })
79
83
  }
80
84
 
81
85
  export const opencodeHarness = {
@@ -374,4 +374,24 @@ export class AptManager extends PackageManager {
374
374
  this._execInherit(['get', 'clean'])
375
375
  return { action: 'cache cleaned' }
376
376
  }
377
+
378
+ /**
379
+ * 查询 apt 安装路径和包目录
380
+ * @returns {{ name: string, install: string, listsDir: string, sourcesList: string, sourcesDir: string }}
381
+ */
382
+ getDir() {
383
+ const which = process.platform === 'win32' ? 'where' : 'which'
384
+ let install = ''
385
+ try {
386
+ install = execSync(`${which} apt`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
387
+ } catch {}
388
+
389
+ return {
390
+ name: 'apt',
391
+ install,
392
+ listsDir: '/var/lib/apt/lists',
393
+ sourcesList: SOURCES_LIST,
394
+ sourcesDir: SOURCES_DIR,
395
+ }
396
+ }
377
397
  }
@@ -164,4 +164,12 @@ export class PackageManager {
164
164
  async cleanCache() {
165
165
  throw new Error(`${this.name}: cleanCache not implemented`)
166
166
  }
167
+
168
+ /**
169
+ * 查询管理器安装路径和包目录
170
+ * @returns {{ name: string, install: string, [key: string]: string }}
171
+ */
172
+ getDir() {
173
+ return { name: this.name, install: '' }
174
+ }
167
175
  }
@@ -371,4 +371,24 @@ export class BrewManager extends PackageManager {
371
371
  this._execInherit(['cleanup', '-s', '--prune=all'])
372
372
  return { action: 'cache cleaned' }
373
373
  }
374
+
375
+ /**
376
+ * 查询 brew 安装路径和包目录
377
+ * @returns {{ name: string, install: string, prefix: string, cellar: string }}
378
+ */
379
+ getDir() {
380
+ const install = getBrewBin()
381
+
382
+ let prefix = ''
383
+ try {
384
+ prefix = execSync(`${install} --prefix`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
385
+ } catch {}
386
+
387
+ let cellar = ''
388
+ try {
389
+ cellar = execSync(`${install} --cellar`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
390
+ } catch {}
391
+
392
+ return { name: 'brew', install, prefix, cellar }
393
+ }
374
394
  }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { execSync, spawnSync } from 'node:child_process'
10
10
  import fs from 'node:fs'
11
+ import os from 'node:os'
11
12
  import path from 'node:path'
12
13
  import { PackageManager } from './base.js'
13
14
  import { getActiveRegistry, setActiveRegistry } from '../config.js'
@@ -339,4 +340,25 @@ export class BunManager extends PackageManager {
339
340
  this._execInherit(['pm', 'cache', 'rm'])
340
341
  return { action: 'cache cleaned' }
341
342
  }
343
+
344
+ /**
345
+ * 查询 bun 安装路径和包目录
346
+ * @returns {{ name: string, install: string, globalDir: string, cacheDir: string }}
347
+ */
348
+ getDir() {
349
+ const which = process.platform === 'win32' ? 'where' : 'which'
350
+ let install = ''
351
+ try {
352
+ install = execSync(`${which} bun`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
353
+ } catch {}
354
+
355
+ const bunHome = path.join(os.homedir(), '.bun')
356
+
357
+ return {
358
+ name: 'bun',
359
+ install,
360
+ globalDir: path.join(bunHome, 'install', 'global'),
361
+ cacheDir: path.join(bunHome, 'install', 'cache'),
362
+ }
363
+ }
342
364
  }
@@ -277,4 +277,25 @@ export class ComposerManager extends PackageManager {
277
277
  this._execInherit(['clear-cache'])
278
278
  return { action: 'cache cleaned' }
279
279
  }
280
+
281
+ /**
282
+ * 查询 composer 安装路径和包目录
283
+ * @returns {{ name: string, install: string, globalHome: string, vendorDir: string }}
284
+ */
285
+ getDir() {
286
+ const which = process.platform === 'win32' ? 'where' : 'which'
287
+ let install = ''
288
+ try {
289
+ install = execSync(`${which} composer`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
290
+ } catch {}
291
+
292
+ let globalHome = ''
293
+ try {
294
+ globalHome = this._exec(['config', '--global', 'home'], { allowNonZero: true }).trim()
295
+ } catch {}
296
+
297
+ const vendorDir = fs.existsSync(path.join(process.cwd(), 'vendor')) ? path.join(process.cwd(), 'vendor') : ''
298
+
299
+ return { name: 'composer', install, globalHome, vendorDir }
300
+ }
280
301
  }
@@ -367,4 +367,23 @@ export class DnfManager extends PackageManager {
367
367
  this._execInherit(['clean', 'all'])
368
368
  return { action: 'cache cleaned' }
369
369
  }
370
+
371
+ /**
372
+ * 查询 dnf 安装路径和包目录
373
+ * @returns {{ name: string, install: string, cacheDir: string, repoDir: string }}
374
+ */
375
+ getDir() {
376
+ const which = process.platform === 'win32' ? 'where' : 'which'
377
+ let install = ''
378
+ try {
379
+ install = execSync(`${which} dnf`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
380
+ } catch {}
381
+
382
+ return {
383
+ name: 'dnf',
384
+ install,
385
+ cacheDir: '/var/cache/dnf',
386
+ repoDir: REPO_DIR,
387
+ }
388
+ }
370
389
  }
@@ -437,12 +437,39 @@ export class NpmManager extends PackageManager {
437
437
  }
438
438
  }
439
439
 
440
+ /**
441
+ * 查询 npm 主版本号(缓存),用于判断是否支持 --allow-scripts(npm >= 11)
442
+ * @returns {number}
443
+ * @private
444
+ */
445
+ _npmMajor() {
446
+ if (this._npmMajorCache !== undefined) return this._npmMajorCache
447
+ try {
448
+ const v = this._exec(['--version'], { allowNonZero: true }).trim()
449
+ this._npmMajorCache = parseInt(v.split('.')[0], 10) || 0
450
+ } catch {
451
+ this._npmMajorCache = 0
452
+ }
453
+ return this._npmMajorCache
454
+ }
455
+
456
+ /**
457
+ * 构造 --allow-scripts 参数(仅 npm >= 11 支持)
458
+ * @param {string} name - 包名
459
+ * @returns {string[]}
460
+ * @private
461
+ */
462
+ _allowScriptsArgs(name) {
463
+ return this._npmMajor() >= 11 ? [`--allow-scripts=${name}`] : []
464
+ }
465
+
440
466
  async install(name, version, opts = {}) {
441
467
  const spec = version ? `${name}@${version}` : name
442
468
  const args = ['install', spec]
443
469
  if (opts.global) args.push('-g')
444
470
  args.push(...this._proxyArgs(opts.proxy))
445
471
  args.push(...this._registryArgs())
472
+ if (opts.allowScripts) args.push(...this._allowScriptsArgs(name))
446
473
 
447
474
  this._execInherit(args)
448
475
  return { name, version: version || 'latest', action: 'installed' }
@@ -461,6 +488,7 @@ export class NpmManager extends PackageManager {
461
488
  if (opts.global) args.push('-g')
462
489
  args.push(...this._proxyArgs(opts.proxy))
463
490
  args.push(...this._registryArgs())
491
+ if (opts.allowScripts) args.push(...this._allowScriptsArgs(name))
464
492
 
465
493
  this._execInherit(args)
466
494
  return { name, version: 'latest', action: 'upgraded' }
@@ -470,4 +498,38 @@ export class NpmManager extends PackageManager {
470
498
  this._execInherit(['cache', 'clean', '--force'])
471
499
  return { action: 'cache cleaned' }
472
500
  }
501
+
502
+ /**
503
+ * 查询 npm 安装路径和包目录
504
+ * @returns {{ name: string, install: string, globalDir: string, localDir: string, cacheDir: string, prefix: string }}
505
+ */
506
+ getDir() {
507
+ const which = process.platform === 'win32' ? 'where' : 'which'
508
+ let install = ''
509
+ try {
510
+ install = execSync(`${which} npm`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
511
+ } catch {}
512
+
513
+ let globalDir = ''
514
+ try {
515
+ globalDir = this._exec(['root', '-g'], { allowNonZero: true }).trim()
516
+ } catch {}
517
+
518
+ let localDir = ''
519
+ try {
520
+ localDir = this._exec(['root'], { allowNonZero: true }).trim()
521
+ } catch {}
522
+
523
+ let cacheDir = ''
524
+ try {
525
+ cacheDir = this._exec(['config', 'get', 'cache'], { allowNonZero: true }).trim()
526
+ } catch {}
527
+
528
+ let prefix = ''
529
+ try {
530
+ prefix = this._exec(['config', 'get', 'prefix'], { allowNonZero: true }).trim()
531
+ } catch {}
532
+
533
+ return { name: 'npm', install, globalDir, localDir, cacheDir, prefix }
534
+ }
473
535
  }
@@ -113,10 +113,16 @@ export class PhpManager extends RuntimeManager {
113
113
  * @returns {string}
114
114
  */
115
115
  getInstallDir() {
116
+ // 优先 phpenv 管理的版本
116
117
  try {
117
118
  const out = this._exec(['which'], { allowNonZero: true }).trim()
118
- if (!out) return ''
119
- return path.dirname(path.dirname(out))
119
+ if (out) return path.dirname(path.dirname(out))
120
+ } catch {}
121
+ // 回退到系统级 php
122
+ try {
123
+ const which = process.platform === 'win32' ? 'where' : 'which'
124
+ const out = execSync(`${which} php`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
125
+ if (out) return path.dirname(path.dirname(out))
120
126
  } catch {}
121
127
  return ''
122
128
  }
@@ -552,4 +552,28 @@ export class PipManager extends PackageManager {
552
552
  this._execInherit(['cache', 'purge'])
553
553
  return { action: 'cache cleaned' }
554
554
  }
555
+
556
+ /**
557
+ * 查询 pip 安装路径和包目录
558
+ * @returns {{ name: string, install: string, sitePackages: string, userSite: string }}
559
+ */
560
+ getDir() {
561
+ const which = process.platform === 'win32' ? 'where' : 'which'
562
+ let install = ''
563
+ try {
564
+ install = execSync(`${which} pip`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
565
+ } catch {}
566
+
567
+ let sitePackages = ''
568
+ try {
569
+ sitePackages = execSync('python -c "import site; print(site.getsitepackages()[0])"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
570
+ } catch {}
571
+
572
+ let userSite = ''
573
+ try {
574
+ userSite = execSync('python -c "import site; print(site.getusersitepackages())"', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
575
+ } catch {}
576
+
577
+ return { name: 'pip', install, sitePackages, userSite }
578
+ }
555
579
  }
@@ -8,12 +8,17 @@
8
8
  * 回退 netstat -tlnp(Linux)/ netstat -ano(Windows)。
9
9
  */
10
10
 
11
- import si from 'systeminformation'
12
11
  import { spawn } from 'node:child_process'
13
12
  import { Shell } from '@wwkit/shared'
14
13
 
15
14
  const shell = new Shell()
16
15
 
16
+ let _si = null
17
+ async function loadSi() {
18
+ if (!_si) _si = (await import('systeminformation')).default
19
+ return _si
20
+ }
21
+
17
22
  /**
18
23
  * MB 字节数转换
19
24
  * @param {number} bytes
@@ -28,6 +33,7 @@ function toMB(bytes) {
28
33
  * @returns {Promise<Map<number, {pid: number, name: string}>>}
29
34
  */
30
35
  export async function getPortMap() {
36
+ const si = await loadSi()
31
37
  const map = new Map()
32
38
 
33
39
  try {
@@ -92,6 +98,7 @@ export async function getPortMap() {
92
98
  * @returns {Promise<object>}
93
99
  */
94
100
  export async function getSystemInfo() {
101
+ const si = await loadSi()
95
102
  const [osInfo, cpuInfo, memInfo, fsInfo, timeInfo] = await Promise.all([
96
103
  si.osInfo(),
97
104
  si.cpu(),
@@ -141,6 +148,7 @@ export async function getSystemInfo() {
141
148
  * @returns {Promise<object>}
142
149
  */
143
150
  export async function listProcesses(count, sortBy) {
151
+ const si = await loadSi()
144
152
  const [procs, portMap] = await Promise.all([
145
153
  si.processes(),
146
154
  getPortMap(),
@@ -184,6 +192,7 @@ export async function listProcesses(count, sortBy) {
184
192
  * @returns {Promise<object>}
185
193
  */
186
194
  export async function searchProcesses({ name, port, exact }) {
195
+ const si = await loadSi()
187
196
  const [procs, portMap] = await Promise.all([
188
197
  si.processes(),
189
198
  getPortMap(),
@@ -243,6 +252,7 @@ export async function searchProcesses({ name, port, exact }) {
243
252
  * @returns {Promise<object>}
244
253
  */
245
254
  export async function listPorts(portFilter) {
255
+ const si = await loadSi()
246
256
  const portMap = await getPortMap()
247
257
  const procs = await si.processes()
248
258
  const procMap = new Map(procs.list.map((p) => [p.pid, p]))
@@ -312,6 +322,7 @@ export async function killByPort(port, signal) {
312
322
  * @returns {Promise<object>}
313
323
  */
314
324
  export async function killByName(name, signal) {
325
+ const si = await loadSi()
315
326
  const procs = await si.processes()
316
327
  const lower = name.toLowerCase()
317
328
  const matched = procs.list.filter((p) =>
@@ -338,6 +349,7 @@ export async function killByName(name, signal) {
338
349
  * @returns {Promise<object>}
339
350
  */
340
351
  export async function getProcessTree(rootPid) {
352
+ const si = await loadSi()
341
353
  const procs = await si.processes()
342
354
  const procMap = new Map(procs.list.map((p) => [p.pid, p]))
343
355
  const childrenMap = new Map()
@@ -382,6 +394,7 @@ export async function getProcessTree(rootPid) {
382
394
  * @returns {Promise<object>}
383
395
  */
384
396
  export async function getProcessDetail(pid) {
397
+ const si = await loadSi()
385
398
  const [procs, portMap] = await Promise.all([
386
399
  si.processes(),
387
400
  getPortMap(),
@@ -103,10 +103,20 @@ export class PythonManager extends RuntimeManager {
103
103
  * @returns {string}
104
104
  */
105
105
  getInstallDir() {
106
+ // 优先 uv 管理的版本
106
107
  try {
107
108
  const out = this._exec(['python', 'find'], { allowNonZero: true }).trim()
108
- if (!out) return ''
109
- return path.dirname(path.dirname(out))
109
+ if (out) return path.dirname(path.dirname(out))
110
+ } catch {}
111
+ // 回退到系统级 python3 / python
112
+ try {
113
+ const which = process.platform === 'win32' ? 'where' : 'which'
114
+ for (const cmd of ['python3', 'python']) {
115
+ try {
116
+ const out = execSync(`${which} ${cmd}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
117
+ if (out) return path.dirname(path.dirname(out))
118
+ } catch {}
119
+ }
110
120
  } catch {}
111
121
  return ''
112
122
  }
@@ -265,4 +265,17 @@ export class WingetManager extends PackageManager {
265
265
  async cleanCache() {
266
266
  return { action: 'winget has no cache clean command' }
267
267
  }
268
+
269
+ /**
270
+ * 查询 winget 安装路径
271
+ * @returns {{ name: string, install: string }}
272
+ */
273
+ getDir() {
274
+ let install = ''
275
+ try {
276
+ install = execSync('where winget', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], shell: true }).trim().split('\r?\n')[0]
277
+ } catch {}
278
+
279
+ return { name: 'winget', install }
280
+ }
268
281
  }
@@ -5,11 +5,17 @@
5
5
  * 自动重试 + 超时 + 被动模式由配置驱动。
6
6
  */
7
7
 
8
- import ftp from 'basic-ftp'
9
8
  import { Readable } from 'node:stream'
10
9
  import { getConfig } from '../../config.js'
11
10
 
12
- const { Client } = ftp
11
+ let _Client = null
12
+ async function loadClient() {
13
+ if (!_Client) {
14
+ const ftp = (await import('basic-ftp')).default
15
+ _Client = ftp.Client
16
+ }
17
+ return _Client
18
+ }
13
19
 
14
20
  /**
15
21
  * 从 opm config 读取 FTP 配置
@@ -36,6 +42,7 @@ export function getFtpConfig() {
36
42
  */
37
43
  export async function connect() {
38
44
  const cfg = getFtpConfig()
45
+ const Client = await loadClient()
39
46
  const client = new Client(cfg.timeout * 1000)
40
47
  await client.access({
41
48
  host: cfg.host,
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * opm share 命令组 — 文本分享服务
3
3
  *
4
- * 命令:upload / zip / get / view / open / list / delete / clear / config / serve / help
4
+ * 命令:upload / zip / get / view / open / list / delete / clear / config / server / help
5
5
  * 服务地址由 config.share.server.active 解析(不支持命令行 -s 参数)。
6
6
  * 需要服务调用的命令执行前先 ping 验证可达性;
7
7
  * 若 active=local 且服务未启动,自动后台启动后再执行。
8
- * serve 子命令:start(后台启动)/ stop(停止)/ status(查询状态)。
8
+ * server 子命令:start(后台启动)/ stop(停止)/ status(查询状态)。
9
9
  * zip 上传文件/文件夹(压缩为 zip 后 base64 上传),get -d 下载并解压。
10
10
  */
11
11
 
@@ -65,7 +65,7 @@ function openBrowser(url) {
65
65
  }
66
66
 
67
67
  /**
68
- * 获取 serve 状态文件路径
68
+ * 获取 server 状态文件路径
69
69
  * @returns {string}
70
70
  */
71
71
  function getServePidFile() {
@@ -73,7 +73,7 @@ function getServePidFile() {
73
73
  }
74
74
 
75
75
  /**
76
- * 读取 serve PID 文件
76
+ * 读取 server PID 文件
77
77
  * @returns {{pid: number, url: string, host: string, port: number, dataDir: string, started: string}|null}
78
78
  */
79
79
  function readServePid() {
@@ -111,14 +111,14 @@ const ACTIONS = {
111
111
  delete: { desc: 'Delete a share by id' },
112
112
  clear: { desc: 'Clean up shares by days and/or keep count' },
113
113
  config: { desc: 'Show current share config section' },
114
- serve: { desc: 'Manage local server (start/stop/status)' },
114
+ server: { desc: 'Manage local server (start/stop/status)' },
115
115
  help: { desc: 'Show this help' },
116
116
  }
117
117
 
118
118
  export class ShareGroup {
119
119
  constructor() {
120
120
  this.name = 'share'
121
- this.desc = 'Text sharing service (upload/get/open/list/serve)'
121
+ this.desc = 'Text sharing service (upload/get/open/list/server)'
122
122
  }
123
123
 
124
124
  async run(argv) {
@@ -150,8 +150,8 @@ export class ShareGroup {
150
150
  return this._clear(parsed)
151
151
  case 'config':
152
152
  return this._config(parsed)
153
- case 'serve':
154
- return this._serve(parsed)
153
+ case 'server':
154
+ return this._server(parsed)
155
155
  default:
156
156
  return this._upload(parseFlags([action, ...rest]))
157
157
  }
@@ -228,7 +228,7 @@ export class ShareGroup {
228
228
  }
229
229
  }
230
230
 
231
- console.error(`[opm share] Failed to start local server. Run "opm share serve status" to check.`)
231
+ console.error(`[opm share] Failed to start local server. Run "opm share server status" to check.`)
232
232
  process.exit(1)
233
233
  }
234
234
 
@@ -498,31 +498,31 @@ export class ShareGroup {
498
498
  output(cfg)
499
499
  }
500
500
 
501
- async _serve(parsed) {
501
+ async _server(parsed) {
502
502
  const [sub] = parsed.positional
503
503
 
504
504
  if (sub === 'start') {
505
- return this._serveStart()
505
+ return this._serverrStart()
506
506
  }
507
507
  if (sub === 'stop') {
508
- return this._serveStop()
508
+ return this._serverrStop()
509
509
  }
510
510
  if (sub === 'status' || !sub) {
511
- return this._serveStatus()
511
+ return this._serverrStatus()
512
512
  }
513
513
  if (sub === 'help' || sub === '-h' || sub === '--help') {
514
- this._serveHelp()
514
+ this._serverrHelp()
515
515
  return
516
516
  }
517
517
 
518
- console.error(`Unknown serve subcommand: ${sub}`)
519
- console.error('Usage: opm share serve [start|stop|status|help]')
518
+ console.error(`Unknown server subcommand: ${sub}`)
519
+ console.error('Usage: opm share server [start|stop|status|help]')
520
520
  process.exit(1)
521
521
  }
522
522
 
523
- _serveHelp() {
523
+ _serverrHelp() {
524
524
  console.log(`
525
- Usage: opm share serve <subcommand>
525
+ Usage: opm share server <subcommand>
526
526
 
527
527
  Subcommands:
528
528
  start Start local server (background, reads host/port/dataDir from config)
@@ -531,9 +531,9 @@ Subcommands:
531
531
  help Show this help
532
532
 
533
533
  Local development:
534
- opm share serve start Start in background (reads config)
535
- opm share serve status Check status
536
- opm share serve stop Stop server
534
+ opm share server start Start in background (reads config)
535
+ opm share server status Check status
536
+ opm share server stop Stop server
537
537
  opm share config Show current config (host/port/dataDir)
538
538
 
539
539
  # Or run server.js directly (foreground):
@@ -549,7 +549,7 @@ Deploy to server:
549
549
  # share.server.active = "official" # or a custom URL
550
550
 
551
551
  # 3. Start the server (foreground or via process manager)
552
- opm share serve start
552
+ opm share server start
553
553
  # Or with custom env:
554
554
  OPM_SHARE_HOST=0.0.0.0 OPM_SHARE_PORT=80 OPM_SHARE_DATA_DIR=/var/lib/opm/share node src/tools/share/server.js
555
555
 
@@ -587,7 +587,7 @@ Environment variables (for server.js direct execution):
587
587
  `)
588
588
  }
589
589
 
590
- _serveStart() {
590
+ _serverrStart() {
591
591
  const existing = readServePid()
592
592
  if (existing && isProcessAlive(existing.pid)) {
593
593
  output({ running: true, pid: existing.pid, url: existing.url, host: existing.host, port: existing.port, dataDir: existing.dataDir, started: existing.started, message: 'Server already running' })
@@ -595,7 +595,7 @@ Environment variables (for server.js direct execution):
595
595
  }
596
596
 
597
597
  this._startBackground()
598
- output({ started: true, message: 'Server started in background. Run "opm share serve status" to check.' })
598
+ output({ started: true, message: 'Server started in background. Run "opm share server status" to check.' })
599
599
  }
600
600
 
601
601
  _startBackground() {
@@ -628,10 +628,10 @@ Environment variables (for server.js direct execution):
628
628
  fs.writeFileSync(pidFile, JSON.stringify(pidInfo, null, 2), 'utf8')
629
629
  }
630
630
 
631
- _serveStop() {
631
+ _serverrStop() {
632
632
  const pidInfo = readServePid()
633
633
  if (!pidInfo) {
634
- output({ stopped: true, message: 'No serve process found (pid file missing)' })
634
+ output({ stopped: true, message: 'No server process found (pid file missing)' })
635
635
  return
636
636
  }
637
637
 
@@ -650,10 +650,10 @@ Environment variables (for server.js direct execution):
650
650
  output({ stopped: true, pid: pidInfo.pid, url: pidInfo.url, message: 'Server stopped' })
651
651
  }
652
652
 
653
- _serveStatus() {
653
+ _serverrStatus() {
654
654
  const pidInfo = readServePid()
655
655
  if (!pidInfo) {
656
- output({ running: false, message: 'No serve process found' })
656
+ output({ running: false, message: 'No server process found' })
657
657
  return
658
658
  }
659
659
 
@@ -678,10 +678,10 @@ Usage: opm share <action> [args] [options]
678
678
  Actions:
679
679
  ${actionLines}
680
680
 
681
- Serve subcommands:
682
- serve start Start local server (background)
683
- serve stop Stop local server
684
- serve status Check local server status
681
+ Server subcommands:
682
+ server start Start local server (background)
683
+ server stop Stop local server
684
+ server status Check local server status
685
685
 
686
686
  Shorthand:
687
687
  opm share "<content>" [-t title] Upload (without "upload" keyword)
@@ -704,9 +704,9 @@ Server config (config.json5):
704
704
 
705
705
  Examples:
706
706
  opm share config Show current share config
707
- opm share serve start Start local server (background)
708
- opm share serve status Check if server is running
709
- opm share serve stop Stop local server
707
+ opm share server start Start local server (background)
708
+ opm share server status Check if server is running
709
+ opm share server stop Stop local server
710
710
  opm share "hello world" -t test Upload text
711
711
  opm share "hello world" -i myid Upload text with custom id "myid"
712
712
  opm share "hello world" -p 1234 Upload text with password override
@@ -15,9 +15,9 @@
15
15
  * pm2 save && pm2 startup
16
16
  *
17
17
  * 方式三:opm 内置管理
18
- * opm share serve start # 后台启动(读取 config.json5 中 share.serve 配置)
19
- * opm share serve stop
20
- * opm share serve status
18
+ * opm share server start # 后台启动(读取 config.json5 中 share.serve 配置)
19
+ * opm share server stop
20
+ * opm share server status
21
21
  *
22
22
  * ──────────────────────────────────────────────────────────────
23
23
  * 可配置常量(集中在此,按需修改)
@@ -44,7 +44,7 @@ $LISTEN_PORT = 8787;
44
44
  // 以下为服务实现,通常无需修改
45
45
  // ──────────────────────────────────────────────────────────────
46
46
 
47
- // 环境变量优先于常量(兼容 opm share serve 后台启动注入的场景)
47
+ // 环境变量优先于常量(兼容 opm share server 后台启动注入的场景)
48
48
 
49
49
  function resolveDataDir() {
50
50
  global $DATA_DIR;
@@ -10,19 +10,27 @@
10
10
  * 6. harness(npm 全局安装 @wwkit/harness,已安装则跳过)
11
11
  * 7. sshproxy(npm 全局安装 @wwkit/sshproxy,已安装则跳过)
12
12
  *
13
- * 命令: version / versions / installed / install / uninstall / upgrade / help
13
+ * install 流程:镜像可达检查 → 安装组件 → 初始化 → 验证
14
+ * init 流程:ww config init → ww webdriver init → ww opencode init → ww sqlite init
15
+ * verify 流程:ww sqlite debug → ww run debug → ww web status → ww webdriver debug
16
+ *
17
+ * 命令: version / versions / installed / install / init / verify / uninstall / upgrade / help
14
18
  */
15
19
 
16
20
  import { execSync } from 'node:child_process'
17
21
 
22
+ import { Shell } from '@wwkit/shared'
18
23
  import { parseFlags } from '../../cli/helpers/args.js'
19
24
  import { output } from '../../formatter.js'
20
- import { getActiveProxy, getConfig } from '../../config.js'
25
+ import { getActiveProxy, getConfig, getActiveRegistry } from '../../config.js'
21
26
  import { NpmManager } from '../../managers/npm.js'
22
27
  import { PipManager } from '../../managers/pip.js'
23
28
  import { PythonManager, isUvInstalled, installUv } from '../../managers/python.js'
24
29
  import { NodeManager, isNvmInstalled, installNvm } from '../../managers/node.js'
25
30
  import { CftGroup } from '../../binaries/cft/index.js'
31
+ import { checkReachability } from '../../cli/groups/ping.js'
32
+
33
+ const shell = new Shell()
26
34
 
27
35
  const COMPONENTS = [
28
36
  { key: 'python', label: 'Python' },
@@ -44,8 +52,10 @@ const ACTIONS = {
44
52
  version: { desc: 'Show installed version of each component' },
45
53
  versions: { desc: 'Show latest available version of each component (remote)' },
46
54
  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)' },
55
+ install: { desc: 'Install all components + init + verify' },
56
+ init: { desc: 'Initialize runtime (config/webdriver/opencode/sqlite)' },
57
+ verify: { desc: 'Verify installation (sqlite/run/web/webdriver)' },
58
+ uninstall: { desc: 'Uninstall all components in reverse order' },
49
59
  upgrade: { desc: 'Upgrade all components to latest' },
50
60
  help: { desc: 'Show this help' },
51
61
  }
@@ -75,6 +85,10 @@ export class WebworkGroup {
75
85
  return this._installed()
76
86
  case 'install':
77
87
  return this._install(parsed)
88
+ case 'init':
89
+ return this._init(parsed)
90
+ case 'verify':
91
+ return this._verify(parsed)
78
92
  case 'uninstall':
79
93
  return this._uninstall(parsed)
80
94
  case 'upgrade':
@@ -183,7 +197,16 @@ export class WebworkGroup {
183
197
 
184
198
  async _install(parsed) {
185
199
  const proxy = this._resolveProxy(parsed)
200
+
201
+ console.log('\n========== 步骤 1/3: 检查镜像可达性 ==========')
202
+ await this._checkMirrors(proxy)
203
+
204
+ console.log('\n========== 步骤 2/3: 安装组件 ==========')
186
205
  await this._runBatch('install', COMPONENTS, proxy, (key, p) => this._installComponent(key, p))
206
+
207
+ console.log('\n========== 步骤 3/3: 初始化 + 验证 ==========')
208
+ await this._init(parsed)
209
+ await this._verify(parsed)
187
210
  }
188
211
 
189
212
  async _uninstall(parsed) {
@@ -197,6 +220,139 @@ export class WebworkGroup {
197
220
  await this._runBatch('upgrade', COMPONENTS, proxy, (key, p) => this._upgradeComponent(key, p))
198
221
  }
199
222
 
223
+ /**
224
+ * 检查所有镜像可达性(HEAD 请求,不可达则抛错)
225
+ * @param {string} proxy
226
+ */
227
+ async _checkMirrors(proxy) {
228
+ const tool = shell.getNetProbeTool()
229
+ if (!tool) {
230
+ console.log(' [WARN] No network probe tool (ping/curl/wget), skipping mirror check')
231
+ return
232
+ }
233
+
234
+ const checks = [
235
+ { name: 'npm', url: getActiveRegistry('npm') },
236
+ { name: 'pip', url: getActiveRegistry('pip') },
237
+ ]
238
+
239
+ if (!isNvmInstalled()) {
240
+ checks.push({ name: 'nvm', url: getActiveRegistry('nvm') })
241
+ checks.push({ name: 'node', url: getActiveRegistry('node') })
242
+ }
243
+
244
+ const tool2 = new CftGroup()
245
+ if (tool2._listInstalled().length === 0) {
246
+ const config = getConfig()
247
+ const cftUrl = config.cft?.registry?.active || config.cft?.registry?.npmmirror || ''
248
+ if (cftUrl) checks.push({ name: 'cft', url: cftUrl })
249
+ }
250
+
251
+ let allOk = true
252
+ for (const { name, url } of checks) {
253
+ if (!url) {
254
+ console.log(` [SKIP] ${name}: no active registry`)
255
+ continue
256
+ }
257
+ try {
258
+ const result = await checkReachability(url, tool, 1, proxy)
259
+ if (result.reachable) {
260
+ console.log(` [OK] ${name}: ${url}`)
261
+ } else {
262
+ console.error(` [FAIL] ${name}: ${url} (${result.raw})`)
263
+ allOk = false
264
+ }
265
+ } catch (err) {
266
+ console.error(` [FAIL] ${name}: ${err.message}`)
267
+ allOk = false
268
+ }
269
+ }
270
+
271
+ if (!allOk) {
272
+ throw new Error('Some mirrors unreachable, aborting. Check network or set proxy with -p')
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 运行时初始化(依赖 blues-lib 已安装)
278
+ * @param {object} parsed
279
+ */
280
+ async _init(parsed) {
281
+ const steps = [
282
+ { name: 'config', cmd: 'ww config init', required: true },
283
+ { name: 'webdriver', cmd: 'ww webdriver init', required: true, hint: 'Downloads Chrome for Testing (~200MB)' },
284
+ { name: 'opencode', cmd: 'ww opencode init', required: true },
285
+ { name: 'sqlite', cmd: 'ww sqlite init', required: true },
286
+ ]
287
+
288
+ for (const step of steps) {
289
+ console.log(` [INIT] ${step.name}...`)
290
+ try {
291
+ execSync(step.cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
292
+ console.log(` [OK] ${step.name}`)
293
+ } catch (err) {
294
+ if (step.required) {
295
+ throw new Error(`${step.name} init failed: ${err.message}`)
296
+ }
297
+ console.error(` [WARN] ${step.name}: ${err.message}`)
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 验证安装结果
304
+ * @param {object} parsed
305
+ */
306
+ async _verify(parsed) {
307
+ const checks = [
308
+ { name: 'sqlite', cmd: 'ww sqlite debug', pattern: /访问成功|ok|success/i },
309
+ { name: 'run', cmd: 'ww run debug', pattern: /run debug/i },
310
+ { name: 'web', cmd: 'ww web status', pattern: /running/i, optional: true },
311
+ ]
312
+
313
+ for (const check of checks) {
314
+ console.log(` [VERIFY] ${check.name}...`)
315
+ try {
316
+ const out = execSync(check.cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
317
+ if (check.pattern.test(out)) {
318
+ console.log(` [OK] ${check.name}`)
319
+ } else {
320
+ if (check.optional) {
321
+ console.log(` [WARN] ${check.name}: not running (optional)`)
322
+ } else {
323
+ console.error(` [FAIL] ${check.name}: output doesn't match expected pattern`)
324
+ }
325
+ }
326
+ } catch (err) {
327
+ if (check.optional) {
328
+ console.log(` [WARN] ${check.name}: ${err.message}`)
329
+ } else {
330
+ console.error(` [FAIL] ${check.name}: ${err.message}`)
331
+ }
332
+ }
333
+ }
334
+
335
+ try {
336
+ const staticIndex = execSync(
337
+ `python3 -c "import os, blues_lib.web; print(os.path.join(os.path.dirname(blues_lib.web.__file__), 'static', 'index.html'))"`,
338
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
339
+ ).trim()
340
+ if (staticIndex) {
341
+ console.log(` [VERIFY] webdriver...`)
342
+ const out = execSync(`ww webdriver debug -u file://${staticIndex} --headless`, {
343
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000
344
+ })
345
+ if (/webwork/i.test(out)) {
346
+ console.log(` [OK] webdriver`)
347
+ } else {
348
+ console.error(` [FAIL] webdriver: output doesn't contain 'webwork'`)
349
+ }
350
+ }
351
+ } catch (err) {
352
+ console.error(` [WARN] webdriver: ${err.message}`)
353
+ }
354
+ }
355
+
200
356
  async _installComponent(key, proxy) {
201
357
  switch (key) {
202
358
  case 'python':
@@ -516,12 +672,12 @@ export class WebworkGroup {
516
672
  if (existing.installed) {
517
673
  return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
518
674
  }
519
- return npm.install(name, null, { global: true, proxy })
675
+ return npm.install(name, null, { global: true, proxy, allowScripts: name.startsWith('@wwkit/') })
520
676
  }
521
677
 
522
678
  async _upgradeNpmPackage(name, proxy) {
523
679
  const npm = new NpmManager()
524
- return npm.upgrade(name, { global: true, proxy })
680
+ return npm.upgrade(name, { global: true, proxy, allowScripts: name.startsWith('@wwkit/') })
525
681
  }
526
682
 
527
683
  async _uninstallNpmPackage(name) {
@@ -550,7 +706,9 @@ Examples:
550
706
  opm webwork version Show installed versions of all components
551
707
  opm webwork versions Show latest available versions (remote)
552
708
  opm webwork installed Show install status of all components
553
- opm webwork install Install all components in order
709
+ opm webwork install Full install (mirror check + components + init + verify)
710
+ opm webwork init Initialize runtime only (config/webdriver/opencode/sqlite)
711
+ opm webwork verify Verify installation only
554
712
  opm webwork uninstall Uninstall all components in reverse order
555
713
  opm webwork upgrade Upgrade all components to latest
556
714
  `)