@wwkit/opm 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -11
- package/bin/index.js +23 -0
- package/docs/managers/git.md +103 -0
- package/docs/managers/proc.md +77 -0
- package/docs/managers/uv.md +9 -3
- package/docs/opm.html +154 -11
- package/docs/packages/opencode.md +46 -15
- package/docs/test/linux-environment-test-report.md +147 -0
- package/package.json +3 -2
- package/src/cli/groups/docker.js +10 -5
- package/src/cli/groups/homebrew.js +9 -5
- package/src/cli/groups/nvm.js +6 -2
- package/src/cli/groups/ping.js +461 -0
- package/src/cli/groups/proc.js +232 -0
- package/src/cli/groups/uv.js +9 -8
- package/src/cli/groups/version-manager.js +34 -8
- package/src/cli/index.js +10 -0
- package/src/config.json5 +84 -7
- package/src/harnesses/base.js +34 -0
- package/src/harnesses/opencode/index.js +56 -0
- package/src/index.js +6 -0
- package/src/managers/brew.js +6 -6
- package/src/managers/docker.js +10 -7
- package/src/managers/node.js +8 -2
- package/src/managers/proc.js +424 -0
- package/src/managers/python.js +7 -4
- package/src/presets.js +3 -0
- package/src/tools/ftp/index.js +151 -16
- package/src/tools/git/config.js +374 -0
- package/src/tools/git/index.js +378 -0
- package/src/tools/share/.htaccess +20 -0
- package/src/tools/share/client.js +172 -0
- package/src/tools/share/index.js +735 -0
- package/src/tools/share/server.js +430 -0
- package/src/tools/share/server.php +452 -0
- package/src/tools/webwork/index.js +558 -0
|
@@ -3,14 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* opencode 是 AI coding agent,通过 npm 全局包 opencode-ai 安装(binary: opencode)。
|
|
5
5
|
* 数据目录跨平台(db.js),会话清理复用 clear.js。
|
|
6
|
+
* env 命令管理 OPENCODE_CONFIG_DIR / OPENCODE_CONFIG_CONTENT 环境变量。
|
|
7
|
+
* open 命令在指定工作目录启动 opencode TUI(自动注入 env 变量)。
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import fs from 'node:fs'
|
|
9
11
|
import path from 'node:path'
|
|
10
12
|
import os from 'node:os'
|
|
13
|
+
import { spawn } from 'node:child_process'
|
|
14
|
+
import { Shell } from '@wwkit/shared'
|
|
15
|
+
import { getConfig } from '../../config.js'
|
|
11
16
|
import { getDataDir, getDbPath, getLogPath } from './db.js'
|
|
12
17
|
import { runClear } from './clear.js'
|
|
13
18
|
|
|
19
|
+
const shell = new Shell()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* opencode 专属环境变量名
|
|
23
|
+
*/
|
|
24
|
+
const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG_CONTENT']
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* 查找 opencode 配置文件路径(优先 .jsonc,其次 .json)
|
|
16
28
|
* @returns {string|null}
|
|
@@ -25,6 +37,47 @@ function findConfigFile() {
|
|
|
25
37
|
return null
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 获取 env 变量当前值
|
|
42
|
+
* 优先级:系统环境变量 > opm config opencode.env
|
|
43
|
+
* @returns {Record<string, string>}
|
|
44
|
+
*/
|
|
45
|
+
function getEnvVars() {
|
|
46
|
+
const cfgEnv = (getConfig().opencode?.env) || {}
|
|
47
|
+
const result = {}
|
|
48
|
+
for (const key of ENV_VARS) {
|
|
49
|
+
result[key] = process.env[key] || cfgEnv[key] || ''
|
|
50
|
+
}
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 在指定工作目录启动 opencode TUI
|
|
56
|
+
* 自动注入有值的环境变量(系统 env > opm config)
|
|
57
|
+
* @param {string} workDir - 工作目录
|
|
58
|
+
*/
|
|
59
|
+
function openTui(workDir) {
|
|
60
|
+
const env = { ...process.env }
|
|
61
|
+
const cfgEnv = getEnvVars()
|
|
62
|
+
for (const key of ENV_VARS) {
|
|
63
|
+
if (cfgEnv[key]) {
|
|
64
|
+
env[key] = cfgEnv[key]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cmd = shell.isWindows ? 'opencode.exe' : 'opencode'
|
|
69
|
+
const child = spawn(cmd, [], {
|
|
70
|
+
cwd: workDir,
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
env,
|
|
73
|
+
shell: shell.shellForExec(false),
|
|
74
|
+
})
|
|
75
|
+
child.on('error', (err) => {
|
|
76
|
+
console.error(`Failed to start opencode: ${err.message}`)
|
|
77
|
+
process.exit(1)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
28
81
|
export const opencodeHarness = {
|
|
29
82
|
name: 'opencode',
|
|
30
83
|
desc: 'opencode AI coding agent',
|
|
@@ -43,4 +96,7 @@ export const opencodeHarness = {
|
|
|
43
96
|
['--backup', 'Backup the DB before deleting (default: off)'],
|
|
44
97
|
['--db <path>', 'Override the opencode database path'],
|
|
45
98
|
],
|
|
99
|
+
envVars: ENV_VARS,
|
|
100
|
+
getEnvVars,
|
|
101
|
+
open: openTui,
|
|
46
102
|
}
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,12 @@ export { UvGroup } from './cli/groups/uv.js'
|
|
|
19
19
|
export { HomebrewGroup } from './cli/groups/homebrew.js'
|
|
20
20
|
export { WslGroup } from './cli/groups/wsl.js'
|
|
21
21
|
export { DockerGroup } from './cli/groups/docker.js'
|
|
22
|
+
export { PingGroup } from './cli/groups/ping.js'
|
|
23
|
+
export { ProcGroup } from './cli/groups/proc.js'
|
|
22
24
|
export { FtpGroup } from './tools/ftp/index.js'
|
|
25
|
+
export { GitGroup } from './tools/git/index.js'
|
|
26
|
+
export { ShareGroup } from './tools/share/index.js'
|
|
27
|
+
export { WebworkGroup } from './tools/webwork/index.js'
|
|
23
28
|
export { ToolManagerGroup } from './cli/groups/version-manager.js'
|
|
24
29
|
export { parseFlags } from './cli/helpers/args.js'
|
|
25
30
|
|
|
@@ -50,6 +55,7 @@ export {
|
|
|
50
55
|
export {
|
|
51
56
|
NPM_PRESETS, PIP_PRESETS, DNF_PRESETS, APT_PRESETS, BREW_PRESETS, COMPOSER_PRESETS,
|
|
52
57
|
BUN_PRESETS, NVM_PRESETS, NODE_PRESETS, PHPENV_PRESETS, PHP_PRESETS, PYTHON_PRESETS,
|
|
58
|
+
UV_PRESETS, DOCKER_PRESETS, HOMEBREW_PRESETS,
|
|
53
59
|
getPresets, resolveRegistryInput,
|
|
54
60
|
} from './presets.js'
|
|
55
61
|
export { PROXY_PRESETS } from './proxy-presets.js'
|
package/src/managers/brew.js
CHANGED
|
@@ -12,9 +12,6 @@ import fs from 'node:fs'
|
|
|
12
12
|
import { PackageManager } from './base.js'
|
|
13
13
|
import { getActiveRegistry, getPresets, setActiveRegistry } from '../config.js'
|
|
14
14
|
|
|
15
|
-
const BREW_INSTALL_URL = 'https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh'
|
|
16
|
-
const BREW_UNINSTALL_URL = 'https://raw.githubusercontent.com/Homebrew/uninstall/HEAD/uninstall.sh'
|
|
17
|
-
|
|
18
15
|
/**
|
|
19
16
|
* 定位 brew 可执行文件(兼容 Intel / Apple Silicon 路径),找不到时退回 PATH
|
|
20
17
|
* @returns {string}
|
|
@@ -40,14 +37,17 @@ export function isHomebrewInstalled() {
|
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
|
-
* 安装 Homebrew(curl
|
|
40
|
+
* 安装 Homebrew(curl 官方安装脚本;脚本 URL 来自 homebrew.registry.active)
|
|
41
|
+
* @param {string} [proxy] - 代理地址
|
|
44
42
|
* @returns {{ action: string, hint: string }}
|
|
45
43
|
*/
|
|
46
|
-
export function installHomebrew() {
|
|
44
|
+
export function installHomebrew(proxy) {
|
|
47
45
|
if (process.platform === 'win32') {
|
|
48
46
|
throw new Error('Homebrew is not supported on Windows (use WSL)')
|
|
49
47
|
}
|
|
50
|
-
|
|
48
|
+
const installUrl = getActiveRegistry('homebrew') || 'https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh'
|
|
49
|
+
const proxyArg = proxy ? ` -x "${proxy}"` : ''
|
|
50
|
+
execSync(`curl -fsSL${proxyArg} "${installUrl}" | bash`, { stdio: 'inherit', shell: true })
|
|
51
51
|
return { action: 'installed', hint: 'Restart shell or ensure /opt/homebrew/bin (or /usr/local/bin) is in PATH' }
|
|
52
52
|
}
|
|
53
53
|
|
package/src/managers/docker.js
CHANGED
|
@@ -16,8 +16,7 @@ import { execSync, spawnSync } from 'node:child_process'
|
|
|
16
16
|
import fs from 'node:fs'
|
|
17
17
|
import { getBrewBin } from './brew.js'
|
|
18
18
|
import { openWsl } from './wsl.js'
|
|
19
|
-
|
|
20
|
-
const DOCKER_INSTALL_URL = 'https://get.docker.com'
|
|
19
|
+
import { getActiveRegistry } from '../config.js'
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* 检查 docker 命令是否可用
|
|
@@ -43,18 +42,22 @@ export function getDockerVersion() {
|
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
|
-
* 在 Linux 环境中执行 Docker
|
|
45
|
+
* 在 Linux 环境中执行 Docker 便捷脚本(脚本 URL 来自 docker.registry.active)
|
|
46
|
+
* @param {string} [proxy] - 代理地址
|
|
47
47
|
* @private
|
|
48
48
|
*/
|
|
49
|
-
function runConvenienceScript() {
|
|
50
|
-
|
|
49
|
+
function runConvenienceScript(proxy) {
|
|
50
|
+
const installUrl = getActiveRegistry('docker') || 'https://get.docker.com'
|
|
51
|
+
const proxyArg = proxy ? ` -x "${proxy}"` : ''
|
|
52
|
+
execSync(`curl -fsSL${proxyArg} ${installUrl} | sh`, { stdio: 'inherit', shell: true })
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* 安装 Docker
|
|
57
|
+
* @param {string} [proxy] - 代理地址
|
|
55
58
|
* @returns {{ action: string, method: string, hint?: string }}
|
|
56
59
|
*/
|
|
57
|
-
export function installDocker() {
|
|
60
|
+
export function installDocker(proxy) {
|
|
58
61
|
const platform = process.platform
|
|
59
62
|
|
|
60
63
|
if (platform === 'linux') {
|
|
@@ -91,7 +94,7 @@ export function upgradeDocker() {
|
|
|
91
94
|
execSync('dnf upgrade -y docker-ce', { stdio: 'inherit', shell: true })
|
|
92
95
|
return { action: 'upgraded', method: 'dnf' }
|
|
93
96
|
} catch {
|
|
94
|
-
|
|
97
|
+
runConvenienceScript(proxy)
|
|
95
98
|
return { action: 'upgraded', method: 'convenience-script' }
|
|
96
99
|
}
|
|
97
100
|
}
|
package/src/managers/node.js
CHANGED
|
@@ -44,12 +44,18 @@ export function isNvmInstalled() {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* 安装 nvm(git clone + 写 shell profile)
|
|
47
|
+
* @param {string} [proxy] - 代理地址
|
|
47
48
|
* @returns {{ action: string, profile: string, hint: string }}
|
|
48
49
|
*/
|
|
49
|
-
export function installNvm() {
|
|
50
|
+
export function installNvm(proxy) {
|
|
50
51
|
const mirror = getActiveRegistry('nvm')
|
|
51
52
|
const nvmDir = getNvmDir()
|
|
52
|
-
|
|
53
|
+
const env = { ...process.env }
|
|
54
|
+
if (proxy) {
|
|
55
|
+
env.http_proxy = proxy
|
|
56
|
+
env.https_proxy = proxy
|
|
57
|
+
}
|
|
58
|
+
execSync(`git clone "${mirror}" "${nvmDir}"`, { stdio: 'inherit', shell: shell.resolveBash(), env })
|
|
53
59
|
const lines = [
|
|
54
60
|
`export NVM_DIR="${nvmDir}"`,
|
|
55
61
|
`[ -s "\\$NVM_DIR/nvm.sh" ] && \\. "\\$NVM_DIR/nvm.sh"`,
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proc 管理器 — 进程与系统信息管理
|
|
3
|
+
*
|
|
4
|
+
* 基于 systeminformation 库 + netstat 回退,提供:
|
|
5
|
+
* 系统信息、进程列表、进程搜索、端口列表、进程终止、进程树、进程详情
|
|
6
|
+
*
|
|
7
|
+
* 端口映射:优先 systeminformation.networkConnections(),
|
|
8
|
+
* 回退 netstat -tlnp(Linux)/ netstat -ano(Windows)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import si from 'systeminformation'
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
import { Shell } from '@wwkit/shared'
|
|
14
|
+
|
|
15
|
+
const shell = new Shell()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MB 字节数转换
|
|
19
|
+
* @param {number} bytes
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function toMB(bytes) {
|
|
23
|
+
return Math.round(bytes / 1024 / 1024)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 获取端口→进程映射
|
|
28
|
+
* @returns {Promise<Map<number, {pid: number, name: string}>>}
|
|
29
|
+
*/
|
|
30
|
+
export async function getPortMap() {
|
|
31
|
+
const map = new Map()
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const conns = await si.networkConnections()
|
|
35
|
+
for (const c of conns) {
|
|
36
|
+
if (c.state === 'listen' && c.localPort && c.pid) {
|
|
37
|
+
if (!map.has(c.localPort)) {
|
|
38
|
+
map.set(c.localPort, { pid: c.pid, name: c.process || '' })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (map.size > 0) return map
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
const fallback = shell.isWindows ? 'netstat' : 'netstat'
|
|
46
|
+
const args = shell.isWindows ? ['-ano'] : ['-tlnp']
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const child = spawn(fallback, args, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
50
|
+
let stdout = ''
|
|
51
|
+
child.stdout.on('data', (d) => { stdout += d })
|
|
52
|
+
child.on('close', () => {
|
|
53
|
+
const lines = stdout.split('\n')
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.includes('LISTEN')) continue
|
|
56
|
+
let port = null
|
|
57
|
+
let pid = null
|
|
58
|
+
let name = ''
|
|
59
|
+
|
|
60
|
+
if (shell.isWindows) {
|
|
61
|
+
const m = line.match(/(\d+)\s*$/)
|
|
62
|
+
if (m) pid = parseInt(m[1], 10)
|
|
63
|
+
const addrMatch = line.match(/[:\.](\d+)\s/)
|
|
64
|
+
if (addrMatch) port = parseInt(addrMatch[1], 10)
|
|
65
|
+
} else {
|
|
66
|
+
const parts = line.trim().split(/\s+/)
|
|
67
|
+
if (parts.length >= 6) {
|
|
68
|
+
const localAddr = parts[3] || parts[1] || ''
|
|
69
|
+
const portMatch = localAddr.match(/:(\d+)$/)
|
|
70
|
+
if (portMatch) port = parseInt(portMatch[1], 10)
|
|
71
|
+
const pidPart = parts[parts.length - 1] || ''
|
|
72
|
+
const pidMatch = pidPart.match(/^(\d+)\/(.+)/)
|
|
73
|
+
if (pidMatch) {
|
|
74
|
+
pid = parseInt(pidMatch[1], 10)
|
|
75
|
+
name = pidMatch[2]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (port && pid && !map.has(port)) {
|
|
81
|
+
map.set(port, { pid, name })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
resolve(map)
|
|
85
|
+
})
|
|
86
|
+
child.on('error', () => resolve(map))
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 获取系统信息
|
|
92
|
+
* @returns {Promise<object>}
|
|
93
|
+
*/
|
|
94
|
+
export async function getSystemInfo() {
|
|
95
|
+
const [osInfo, cpuInfo, memInfo, fsInfo, timeInfo] = await Promise.all([
|
|
96
|
+
si.osInfo(),
|
|
97
|
+
si.cpu(),
|
|
98
|
+
si.mem(),
|
|
99
|
+
si.fsSize(),
|
|
100
|
+
si.time(),
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
os: {
|
|
105
|
+
platform: osInfo.platform,
|
|
106
|
+
distro: osInfo.distro,
|
|
107
|
+
release: osInfo.release,
|
|
108
|
+
arch: osInfo.arch,
|
|
109
|
+
hostname: osInfo.hostname,
|
|
110
|
+
kernel: osInfo.kernel,
|
|
111
|
+
},
|
|
112
|
+
cpu: {
|
|
113
|
+
manufacturer: cpuInfo.manufacturer,
|
|
114
|
+
brand: cpuInfo.brand,
|
|
115
|
+
cores: cpuInfo.cores,
|
|
116
|
+
speed: cpuInfo.speed ? `${cpuInfo.speed}GHz` : '',
|
|
117
|
+
},
|
|
118
|
+
memory: {
|
|
119
|
+
total: toMB(memInfo.total),
|
|
120
|
+
used: toMB(memInfo.active),
|
|
121
|
+
available: toMB(memInfo.available),
|
|
122
|
+
unit: 'MB',
|
|
123
|
+
},
|
|
124
|
+
disk: fsInfo.map((f) => ({
|
|
125
|
+
fs: f.fs,
|
|
126
|
+
mount: f.mount,
|
|
127
|
+
total: toMB(f.size),
|
|
128
|
+
used: toMB(f.used),
|
|
129
|
+
available: toMB(f.available),
|
|
130
|
+
use: f.use,
|
|
131
|
+
unit: 'MB',
|
|
132
|
+
})),
|
|
133
|
+
uptime: Math.round(timeInfo.uptime),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 获取进程列表(排序后取前 N 个)
|
|
139
|
+
* @param {number} count - 数量
|
|
140
|
+
* @param {'mem'|'cpu'} sortBy - 排序字段
|
|
141
|
+
* @returns {Promise<object>}
|
|
142
|
+
*/
|
|
143
|
+
export async function listProcesses(count, sortBy) {
|
|
144
|
+
const [procs, portMap] = await Promise.all([
|
|
145
|
+
si.processes(),
|
|
146
|
+
getPortMap(),
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
const pidToPorts = new Map()
|
|
150
|
+
for (const [port, info] of portMap) {
|
|
151
|
+
const arr = pidToPorts.get(info.pid) || []
|
|
152
|
+
arr.push(port)
|
|
153
|
+
pidToPorts.set(info.pid, arr)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const list = procs.list.map((p) => ({
|
|
157
|
+
pid: p.pid,
|
|
158
|
+
parentPid: p.parentPid,
|
|
159
|
+
name: p.name,
|
|
160
|
+
mem: Math.round(p.memRss / 1024),
|
|
161
|
+
memPercent: Math.round(p.mem * 100) / 100,
|
|
162
|
+
cpu: Math.round(p.cpu * 100) / 100,
|
|
163
|
+
ports: pidToPorts.get(p.pid) || [],
|
|
164
|
+
user: p.user || '',
|
|
165
|
+
state: p.state || '',
|
|
166
|
+
started: p.started || '',
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
list.sort((a, b) => {
|
|
170
|
+
if (sortBy === 'cpu') return b.cpu - a.cpu
|
|
171
|
+
return b.mem - a.mem
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
count,
|
|
176
|
+
sortBy,
|
|
177
|
+
processes: list.slice(0, count),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 搜索进程
|
|
183
|
+
* @param {{name?: string, port?: string, exact?: boolean}} opts
|
|
184
|
+
* @returns {Promise<object>}
|
|
185
|
+
*/
|
|
186
|
+
export async function searchProcesses({ name, port, exact }) {
|
|
187
|
+
const [procs, portMap] = await Promise.all([
|
|
188
|
+
si.processes(),
|
|
189
|
+
getPortMap(),
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
const pidToPorts = new Map()
|
|
193
|
+
for (const [p, info] of portMap) {
|
|
194
|
+
const arr = pidToPorts.get(info.pid) || []
|
|
195
|
+
arr.push(p)
|
|
196
|
+
pidToPorts.set(info.pid, arr)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let list = procs.list.map((p) => ({
|
|
200
|
+
pid: p.pid,
|
|
201
|
+
parentPid: p.parentPid,
|
|
202
|
+
name: p.name,
|
|
203
|
+
mem: Math.round(p.memRss / 1024),
|
|
204
|
+
memPercent: Math.round(p.mem * 100) / 100,
|
|
205
|
+
cpu: Math.round(p.cpu * 100) / 100,
|
|
206
|
+
ports: pidToPorts.get(p.pid) || [],
|
|
207
|
+
user: p.user || '',
|
|
208
|
+
state: p.state || '',
|
|
209
|
+
started: p.started || '',
|
|
210
|
+
command: p.command,
|
|
211
|
+
params: p.params,
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
if (name) {
|
|
215
|
+
if (exact) {
|
|
216
|
+
list = list.filter((p) => p.name === name || p.command === name)
|
|
217
|
+
} else {
|
|
218
|
+
const lower = name.toLowerCase()
|
|
219
|
+
list = list.filter((p) =>
|
|
220
|
+
p.name.toLowerCase().includes(lower) ||
|
|
221
|
+
(p.command && p.command.toLowerCase().includes(lower))
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (port) {
|
|
227
|
+
const portNum = parseInt(port, 10)
|
|
228
|
+
list = list.filter((p) => p.ports.includes(portNum))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: name || null,
|
|
233
|
+
port: port || null,
|
|
234
|
+
exact: !!exact,
|
|
235
|
+
count: list.length,
|
|
236
|
+
processes: list,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 列出监听端口
|
|
242
|
+
* @param {string} [portFilter] - 可选端口过滤
|
|
243
|
+
* @returns {Promise<object>}
|
|
244
|
+
*/
|
|
245
|
+
export async function listPorts(portFilter) {
|
|
246
|
+
const portMap = await getPortMap()
|
|
247
|
+
const procs = await si.processes()
|
|
248
|
+
const procMap = new Map(procs.list.map((p) => [p.pid, p]))
|
|
249
|
+
|
|
250
|
+
let entries = []
|
|
251
|
+
for (const [port, info] of portMap) {
|
|
252
|
+
const proc = procMap.get(info.pid)
|
|
253
|
+
entries.push({
|
|
254
|
+
port,
|
|
255
|
+
pid: info.pid,
|
|
256
|
+
name: info.name || (proc ? proc.name : ''),
|
|
257
|
+
user: proc ? proc.user : '',
|
|
258
|
+
state: proc ? proc.state : '',
|
|
259
|
+
started: proc ? proc.started : '',
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
entries.sort((a, b) => a.port - b.port)
|
|
264
|
+
|
|
265
|
+
if (portFilter) {
|
|
266
|
+
const portNum = parseInt(portFilter, 10)
|
|
267
|
+
entries = entries.filter((e) => e.port === portNum)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
port: portFilter || null,
|
|
272
|
+
count: entries.length,
|
|
273
|
+
ports: entries,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 终止进程
|
|
279
|
+
* @param {number} pid
|
|
280
|
+
* @param {'SIGTERM'|'SIGKILL'} signal
|
|
281
|
+
* @returns {Promise<object>}
|
|
282
|
+
*/
|
|
283
|
+
export async function killByPid(pid, signal) {
|
|
284
|
+
try {
|
|
285
|
+
process.kill(pid, signal)
|
|
286
|
+
return { pid, signal, killed: true }
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return { pid, signal, killed: false, error: err.message }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 按端口终止进程
|
|
294
|
+
* @param {number} port
|
|
295
|
+
* @param {'SIGTERM'|'SIGKILL'} signal
|
|
296
|
+
* @returns {Promise<object>}
|
|
297
|
+
*/
|
|
298
|
+
export async function killByPort(port, signal) {
|
|
299
|
+
const portMap = await getPortMap()
|
|
300
|
+
const info = portMap.get(port)
|
|
301
|
+
if (!info) {
|
|
302
|
+
return { port, signal, killed: [], error: `No process listening on port ${port}` }
|
|
303
|
+
}
|
|
304
|
+
const result = await killByPid(info.pid, signal)
|
|
305
|
+
return { port, signal, killed: [{ pid: info.pid, name: info.name, ...result }] }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 按名称终止进程
|
|
310
|
+
* @param {string} name - 进程名
|
|
311
|
+
* @param {'SIGTERM'|'SIGKILL'} signal
|
|
312
|
+
* @returns {Promise<object>}
|
|
313
|
+
*/
|
|
314
|
+
export async function killByName(name, signal) {
|
|
315
|
+
const procs = await si.processes()
|
|
316
|
+
const lower = name.toLowerCase()
|
|
317
|
+
const matched = procs.list.filter((p) =>
|
|
318
|
+
p.name.toLowerCase().includes(lower) ||
|
|
319
|
+
(p.command && p.command.toLowerCase().includes(lower))
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if (matched.length === 0) {
|
|
323
|
+
return { name, signal, killed: [], error: `No process found matching "${name}"` }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const results = []
|
|
327
|
+
for (const p of matched) {
|
|
328
|
+
const result = await killByPid(p.pid, signal)
|
|
329
|
+
results.push({ pid: p.pid, name: p.name, ...result })
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { name, signal, killed: results }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 获取进程树
|
|
337
|
+
* @param {number} [rootPid] - 根 PID(可选,不传则显示所有顶层进程)
|
|
338
|
+
* @returns {Promise<object>}
|
|
339
|
+
*/
|
|
340
|
+
export async function getProcessTree(rootPid) {
|
|
341
|
+
const procs = await si.processes()
|
|
342
|
+
const procMap = new Map(procs.list.map((p) => [p.pid, p]))
|
|
343
|
+
const childrenMap = new Map()
|
|
344
|
+
for (const p of procs.list) {
|
|
345
|
+
const arr = childrenMap.get(p.parentPid) || []
|
|
346
|
+
arr.push(p)
|
|
347
|
+
childrenMap.set(p.parentPid, arr)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildNode(pid) {
|
|
351
|
+
const proc = procMap.get(pid)
|
|
352
|
+
if (!proc) return null
|
|
353
|
+
const children = (childrenMap.get(pid) || []).map((c) => buildNode(c.pid)).filter(Boolean)
|
|
354
|
+
return {
|
|
355
|
+
pid: proc.pid,
|
|
356
|
+
parentPid: proc.parentPid,
|
|
357
|
+
name: proc.name,
|
|
358
|
+
mem: Math.round(proc.memRss / 1024),
|
|
359
|
+
cpu: Math.round(proc.cpu * 100) / 100,
|
|
360
|
+
state: proc.state || '',
|
|
361
|
+
children,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (rootPid) {
|
|
366
|
+
const tree = buildNode(rootPid)
|
|
367
|
+
return { rootPid, tree }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const topPids = procs.list
|
|
371
|
+
.filter((p) => !procMap.has(p.parentPid))
|
|
372
|
+
.map((p) => p.pid)
|
|
373
|
+
.sort((a, b) => a - b)
|
|
374
|
+
|
|
375
|
+
const trees = topPids.map((pid) => buildNode(pid)).filter(Boolean)
|
|
376
|
+
return { rootPid: null, trees }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 获取进程详情
|
|
381
|
+
* @param {number} pid
|
|
382
|
+
* @returns {Promise<object>}
|
|
383
|
+
*/
|
|
384
|
+
export async function getProcessDetail(pid) {
|
|
385
|
+
const [procs, portMap] = await Promise.all([
|
|
386
|
+
si.processes(),
|
|
387
|
+
getPortMap(),
|
|
388
|
+
])
|
|
389
|
+
|
|
390
|
+
const proc = procs.list.find((p) => p.pid === pid)
|
|
391
|
+
if (!proc) {
|
|
392
|
+
return { pid, error: `Process ${pid} not found` }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const pidToPorts = new Map()
|
|
396
|
+
for (const [port, info] of portMap) {
|
|
397
|
+
if (info.pid === pid) {
|
|
398
|
+
const arr = pidToPorts.get(pid) || []
|
|
399
|
+
arr.push(port)
|
|
400
|
+
pidToPorts.set(pid, arr)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const childCount = procs.list.filter((p) => p.parentPid === pid).length
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
pid: proc.pid,
|
|
408
|
+
parentPid: proc.parentPid,
|
|
409
|
+
name: proc.name,
|
|
410
|
+
command: proc.command,
|
|
411
|
+
params: proc.params,
|
|
412
|
+
path: proc.path,
|
|
413
|
+
user: proc.user,
|
|
414
|
+
state: proc.state,
|
|
415
|
+
priority: proc.priority,
|
|
416
|
+
mem: Math.round(proc.memRss / 1024),
|
|
417
|
+
memVsz: Math.round(proc.memVsz / 1024),
|
|
418
|
+
memPercent: Math.round(proc.mem * 100) / 100,
|
|
419
|
+
cpu: Math.round(proc.cpu * 100) / 100,
|
|
420
|
+
started: proc.started,
|
|
421
|
+
ports: pidToPorts.get(pid) || [],
|
|
422
|
+
childCount,
|
|
423
|
+
}
|
|
424
|
+
}
|
package/src/managers/python.js
CHANGED
|
@@ -17,8 +17,8 @@ import fs from 'node:fs'
|
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import os from 'node:os'
|
|
19
19
|
import { RuntimeManager } from './runtime.js'
|
|
20
|
+
import { getActiveRegistry } from '../config.js'
|
|
20
21
|
|
|
21
|
-
const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh'
|
|
22
22
|
const UV_INSTALL_PS_URL = 'https://astral.sh/uv/install.ps1'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -45,10 +45,13 @@ export function isUvInstalled() {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* 安装 uv(curl
|
|
48
|
+
* 安装 uv(curl 安装脚本;脚本 URL 来自 uv.registry.active)
|
|
49
|
+
* @param {string} [proxy] - 代理地址
|
|
49
50
|
* @returns {{ action: string, hint: string }}
|
|
50
51
|
*/
|
|
51
|
-
export function installUv() {
|
|
52
|
+
export function installUv(proxy) {
|
|
53
|
+
const installUrl = getActiveRegistry('uv') || 'https://astral.sh/uv/install.sh'
|
|
54
|
+
const proxyArg = proxy ? ` -x "${proxy}"` : ''
|
|
52
55
|
if (process.platform === 'win32') {
|
|
53
56
|
const r = spawnSync(
|
|
54
57
|
'powershell',
|
|
@@ -59,7 +62,7 @@ export function installUv() {
|
|
|
59
62
|
throw new Error('uv install failed (powershell exited non-zero)')
|
|
60
63
|
}
|
|
61
64
|
} else {
|
|
62
|
-
execSync(`curl -LsSf "${
|
|
65
|
+
execSync(`curl -LsSf${proxyArg} "${installUrl}" | sh`, { stdio: 'inherit', shell: true })
|
|
63
66
|
}
|
|
64
67
|
return { action: 'installed', hint: 'Restart shell or ensure ~/.local/bin is in PATH' }
|
|
65
68
|
}
|
package/src/presets.js
CHANGED
|
@@ -19,6 +19,9 @@ export const NODE_PRESETS = _getPresets('node')
|
|
|
19
19
|
export const PHPENV_PRESETS = _getPresets('phpenv')
|
|
20
20
|
export const PHP_PRESETS = _getPresets('php')
|
|
21
21
|
export const PYTHON_PRESETS = _getPresets('python')
|
|
22
|
+
export const UV_PRESETS = _getPresets('uv')
|
|
23
|
+
export const DOCKER_PRESETS = _getPresets('docker')
|
|
24
|
+
export const HOMEBREW_PRESETS = _getPresets('homebrew')
|
|
22
25
|
|
|
23
26
|
export const getPresets = _getPresets
|
|
24
27
|
export const resolveRegistryInput = _resolveRegistryInput
|