@wwkit/opm 1.0.5 → 1.0.7

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.5",
3
+ "version": "1.0.7",
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,8 @@
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.8"
38
+ "@wwkit/harness": "1.0.16",
39
+ "@wwkit/shared": "1.0.10"
39
40
  },
40
41
  "publishConfig": {
41
42
  "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:
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
  }
@@ -454,13 +454,14 @@ export class NpmManager extends PackageManager {
454
454
  }
455
455
 
456
456
  /**
457
- * 构造 --allow-scripts 参数(仅 npm >= 11 支持)
458
- * @param {string} name - 包名
457
+ * 构造 --dangerously-allow-all-scripts 参数(仅 npm >= 11 支持)
458
+ * npm < 11 默认放行所有脚本,无需此 flag;npm >= 11 默认阻止,需显式放行
459
+ * @param {string} name - 包名(保留签名兼容,未使用)
459
460
  * @returns {string[]}
460
461
  * @private
461
462
  */
462
463
  _allowScriptsArgs(name) {
463
- return this._npmMajor() >= 11 ? [`--allow-scripts=${name}`] : []
464
+ return this._npmMajor() >= 11 ? ['--dangerously-allow-all-scripts'] : []
464
465
  }
465
466
 
466
467
  async install(name, version, opts = {}) {
@@ -498,4 +499,38 @@ export class NpmManager extends PackageManager {
498
499
  this._execInherit(['cache', 'clean', '--force'])
499
500
  return { action: 'cache cleaned' }
500
501
  }
502
+
503
+ /**
504
+ * 查询 npm 安装路径和包目录
505
+ * @returns {{ name: string, install: string, globalDir: string, localDir: string, cacheDir: string, prefix: string }}
506
+ */
507
+ getDir() {
508
+ const which = process.platform === 'win32' ? 'where' : 'which'
509
+ let install = ''
510
+ try {
511
+ install = execSync(`${which} npm`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0]
512
+ } catch {}
513
+
514
+ let globalDir = ''
515
+ try {
516
+ globalDir = this._exec(['root', '-g'], { allowNonZero: true }).trim()
517
+ } catch {}
518
+
519
+ let localDir = ''
520
+ try {
521
+ localDir = this._exec(['root'], { allowNonZero: true }).trim()
522
+ } catch {}
523
+
524
+ let cacheDir = ''
525
+ try {
526
+ cacheDir = this._exec(['config', 'get', 'cache'], { allowNonZero: true }).trim()
527
+ } catch {}
528
+
529
+ let prefix = ''
530
+ try {
531
+ prefix = this._exec(['config', 'get', 'prefix'], { allowNonZero: true }).trim()
532
+ } catch {}
533
+
534
+ return { name: 'npm', install, globalDir, localDir, cacheDir, prefix }
535
+ }
501
536
  }
@@ -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,
@@ -10,16 +10,20 @@
10
10
  * 6. harness(npm 全局安装 @wwkit/harness,已安装则跳过)
11
11
  * 7. sshproxy(npm 全局安装 @wwkit/sshproxy,已安装则跳过)
12
12
  *
13
+ * npm 包安装使用 --dangerously-allow-all-scripts(npm >= 11)放行 postinstall 脚本。
14
+ *
13
15
  * 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
+ * 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
16
18
  *
17
19
  * 命令: version / versions / installed / install / init / verify / uninstall / upgrade / help
18
20
  */
19
21
 
22
+ import fs from 'node:fs'
23
+ import path from 'node:path'
20
24
  import { execSync } from 'node:child_process'
21
25
 
22
- import { Shell } from '@wwkit/shared'
26
+ import { Shell, getXdgConfigDir } from '@wwkit/shared'
23
27
  import { parseFlags } from '../../cli/helpers/args.js'
24
28
  import { output } from '../../formatter.js'
25
29
  import { getActiveProxy, getConfig, getActiveRegistry } from '../../config.js'
@@ -190,9 +194,7 @@ export class WebworkGroup {
190
194
 
191
195
  output({ action: verb, results, summary: { ok, skipped, failed, total } })
192
196
 
193
- if (failed > 0) {
194
- process.exit(1)
195
- }
197
+ return { results, ok, skipped, failed }
196
198
  }
197
199
 
198
200
  async _install(parsed) {
@@ -280,9 +282,8 @@ export class WebworkGroup {
280
282
  async _init(parsed) {
281
283
  const steps = [
282
284
  { 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
285
  { name: 'sqlite', cmd: 'ww sqlite init', required: true },
286
+ { name: 'web', cmd: 'ww web init', required: true, hint: 'Start web service :7901' },
286
287
  ]
287
288
 
288
289
  for (const step of steps) {
@@ -290,6 +291,9 @@ export class WebworkGroup {
290
291
  try {
291
292
  execSync(step.cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
292
293
  console.log(` [OK] ${step.name}`)
294
+ if (step.name === 'config') {
295
+ this._writeRuntimePaths()
296
+ }
293
297
  } catch (err) {
294
298
  if (step.required) {
295
299
  throw new Error(`${step.name} init failed: ${err.message}`)
@@ -297,6 +301,120 @@ export class WebworkGroup {
297
301
  console.error(` [WARN] ${step.name}: ${err.message}`)
298
302
  }
299
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
+ }
300
418
  }
301
419
 
302
420
  /**
@@ -304,6 +422,15 @@ export class WebworkGroup {
304
422
  * @param {object} parsed
305
423
  */
306
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
+
307
434
  const checks = [
308
435
  { name: 'sqlite', cmd: 'ww sqlite debug', pattern: /访问成功|ok|success/i },
309
436
  { name: 'run', cmd: 'ww run debug', pattern: /run debug/i },
@@ -672,12 +799,12 @@ export class WebworkGroup {
672
799
  if (existing.installed) {
673
800
  return { action: 'skipped', reason: `${name} ${existing.version} already installed`, version: existing.version }
674
801
  }
675
- return npm.install(name, null, { global: true, proxy, allowScripts: name.startsWith('@wwkit/') })
802
+ return npm.install(name, null, { global: true, proxy, allowScripts: true })
676
803
  }
677
804
 
678
805
  async _upgradeNpmPackage(name, proxy) {
679
806
  const npm = new NpmManager()
680
- return npm.upgrade(name, { global: true, proxy, allowScripts: name.startsWith('@wwkit/') })
807
+ return npm.upgrade(name, { global: true, proxy, allowScripts: true })
681
808
  }
682
809
 
683
810
  async _uninstallNpmPackage(name) {