@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 +2 -2
- package/scripts/postinstall.js +4 -4
- package/src/binaries/base.js +7 -1
- package/src/cli/groups/package.js +8 -0
- package/src/cli/groups/ping.js +2 -0
- package/src/config.js +8 -0
- package/src/harnesses/base.js +28 -10
- package/src/harnesses/opencode/index.js +8 -4
- package/src/managers/apt.js +20 -0
- package/src/managers/base.js +8 -0
- package/src/managers/brew.js +20 -0
- package/src/managers/bun.js +22 -0
- package/src/managers/composer.js +21 -0
- package/src/managers/dnf.js +19 -0
- package/src/managers/npm.js +62 -0
- package/src/managers/php.js +8 -2
- package/src/managers/pip.js +24 -0
- package/src/managers/proc.js +14 -1
- package/src/managers/python.js +12 -2
- package/src/managers/winget.js +13 -0
- package/src/tools/ftp/client.js +9 -2
- package/src/tools/share/index.js +35 -35
- package/src/tools/share/server.js +3 -3
- package/src/tools/share/server.php +1 -1
- package/src/tools/webwork/index.js +165 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwkit/opm",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
38
|
+
"@wwkit/shared": "1.0.9"
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"registry": "https://registry.npmjs.org/",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { mergeBuiltinConfig, getUserConfigFile } from '../src/config.js'
|
|
4
4
|
|
|
5
|
-
if (
|
|
6
|
-
console.log(`opm:
|
|
7
|
-
}
|
|
5
|
+
if (mergeBuiltinConfig()) {
|
|
6
|
+
console.log(`opm: config updated at ${getUserConfigFile()}`)
|
|
7
|
+
}
|
package/src/binaries/base.js
CHANGED
|
@@ -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/cli/groups/ping.js
CHANGED
|
@@ -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 的返回结果
|
package/src/harnesses/base.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
194
|
-
const
|
|
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
|
|
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}
|
|
236
|
-
opm ${this.cfg.name}
|
|
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 =
|
|
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.
|
|
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 = {
|
package/src/managers/apt.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/base.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/brew.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/bun.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/composer.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/dnf.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/npm.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/php.js
CHANGED
|
@@ -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 (
|
|
119
|
-
|
|
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
|
}
|
package/src/managers/pip.js
CHANGED
|
@@ -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
|
}
|
package/src/managers/proc.js
CHANGED
|
@@ -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(),
|
package/src/managers/python.js
CHANGED
|
@@ -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 (
|
|
109
|
-
|
|
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
|
}
|
package/src/managers/winget.js
CHANGED
|
@@ -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
|
}
|
package/src/tools/ftp/client.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/tools/share/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* opm share 命令组 — 文本分享服务
|
|
3
3
|
*
|
|
4
|
-
* 命令:upload / zip / get / view / open / list / delete / clear / config /
|
|
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
|
-
*
|
|
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
|
-
* 获取
|
|
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
|
-
* 读取
|
|
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
|
-
|
|
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/
|
|
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 '
|
|
154
|
-
return this.
|
|
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
|
|
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
|
|
501
|
+
async _server(parsed) {
|
|
502
502
|
const [sub] = parsed.positional
|
|
503
503
|
|
|
504
504
|
if (sub === 'start') {
|
|
505
|
-
return this.
|
|
505
|
+
return this._serverrStart()
|
|
506
506
|
}
|
|
507
507
|
if (sub === 'stop') {
|
|
508
|
-
return this.
|
|
508
|
+
return this._serverrStop()
|
|
509
509
|
}
|
|
510
510
|
if (sub === 'status' || !sub) {
|
|
511
|
-
return this.
|
|
511
|
+
return this._serverrStatus()
|
|
512
512
|
}
|
|
513
513
|
if (sub === 'help' || sub === '-h' || sub === '--help') {
|
|
514
|
-
this.
|
|
514
|
+
this._serverrHelp()
|
|
515
515
|
return
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
console.error(`Unknown
|
|
519
|
-
console.error('Usage: opm share
|
|
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
|
-
|
|
523
|
+
_serverrHelp() {
|
|
524
524
|
console.log(`
|
|
525
|
-
Usage: opm share
|
|
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
|
|
535
|
-
opm share
|
|
536
|
-
opm share
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
631
|
+
_serverrStop() {
|
|
632
632
|
const pidInfo = readServePid()
|
|
633
633
|
if (!pidInfo) {
|
|
634
|
-
output({ stopped: true, message: 'No
|
|
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
|
-
|
|
653
|
+
_serverrStatus() {
|
|
654
654
|
const pidInfo = readServePid()
|
|
655
655
|
if (!pidInfo) {
|
|
656
|
-
output({ running: false, message: 'No
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
708
|
-
opm share
|
|
709
|
-
opm share
|
|
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
|
|
19
|
-
* opm share
|
|
20
|
-
* opm share
|
|
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
|
|
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
|
-
*
|
|
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
|
|
48
|
-
|
|
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
|
|
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
|
`)
|