@wwkit/opm 1.0.1 → 1.0.3
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 +4 -2
- package/src/cli/groups/docker.js +121 -0
- 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/groups/wsl.js +220 -0
- package/src/cli/index.js +12 -0
- package/src/config.json5 +80 -6
- package/src/harnesses/base.js +34 -0
- package/src/harnesses/opencode/index.js +56 -0
- package/src/index.js +19 -0
- package/src/managers/brew.js +6 -6
- package/src/managers/docker.js +298 -0
- package/src/managers/node.js +8 -2
- package/src/managers/proc.js +424 -0
- package/src/managers/python.js +7 -4
- package/src/managers/wsl.js +258 -0
- package/src/presets.js +3 -0
- package/src/tools/ftp/client.js +214 -0
- package/src/tools/ftp/index.js +406 -0
- package/src/tools/ftp/util.js +174 -0
- package/src/tools/git/config.js +374 -0
- package/src/tools/git/index.js +378 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WSL2 管理器(Windows Subsystem for Linux)
|
|
3
|
+
*
|
|
4
|
+
* 通过 wsl.exe CLI 管理自身与发行版。仅 Windows 可用。
|
|
5
|
+
* wsl.exe 输出为 UTF-16LE 编码,需用对应 encoding 读取。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
|
|
12
|
+
const WSL_EXE = 'wsl.exe'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 要求 Windows 平台,否则抛出
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
function requireWindows() {
|
|
19
|
+
if (process.platform !== 'win32') {
|
|
20
|
+
throw new Error('WSL is only available on Windows')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 检查 WSL 是否已安装(wsl.exe 是否可用)
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function isWslInstalled() {
|
|
29
|
+
if (process.platform !== 'win32') return false
|
|
30
|
+
try {
|
|
31
|
+
execSync(`${WSL_EXE} --version`, { encoding: 'utf16le', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
32
|
+
return true
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析 wsl --version 输出,提取 WSL 版本号
|
|
40
|
+
* @param {string} out
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function parseWslVersionOutput(out) {
|
|
44
|
+
const m = out.match(/WSL\s+(?:版本|version)[:\s]\s*([\d.]+)/i)
|
|
45
|
+
return m ? m[1] : out.trim().split('\n')[0].trim()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取 WSL 版本号
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function getWslVersion() {
|
|
53
|
+
requireWindows()
|
|
54
|
+
const out = execSync(`${WSL_EXE} --version`, { encoding: 'utf16le', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
55
|
+
return parseWslVersionOutput(out)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 安装 WSL2(含默认发行版)
|
|
60
|
+
* @returns {{ action: string, hint: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function installWsl() {
|
|
63
|
+
requireWindows()
|
|
64
|
+
execSync(`${WSL_EXE} --install`, { stdio: 'inherit', shell: true })
|
|
65
|
+
return { action: 'installed', hint: 'Restart may be required' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 更新 WSL 内核
|
|
70
|
+
* @returns {{ action: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function updateWsl() {
|
|
73
|
+
requireWindows()
|
|
74
|
+
execSync(`${WSL_EXE} --update`, { stdio: 'inherit', shell: true })
|
|
75
|
+
return { action: 'updated' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 卸载 WSL 提示(需通过 Windows 设置操作)
|
|
80
|
+
* @returns {{ action: string, hint: string }}
|
|
81
|
+
*/
|
|
82
|
+
export function uninstallWsl() {
|
|
83
|
+
return {
|
|
84
|
+
action: 'uninstall',
|
|
85
|
+
hint: 'Go to Settings > Apps > Optional Features > "Windows Subsystem for Linux" to uninstall',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取 WSL 数据目录
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function getWslDir() {
|
|
94
|
+
if (process.platform !== 'win32') return ''
|
|
95
|
+
const localAppData = process.env.LOCALAPPDATA || ''
|
|
96
|
+
if (!localAppData) return ''
|
|
97
|
+
const packagesDir = path.join(localAppData, 'Packages')
|
|
98
|
+
try {
|
|
99
|
+
const entries = fs.readdirSync(packagesDir)
|
|
100
|
+
const wslEntry = entries.find((e) => /CanonicalGroupLimited|Microsoft\.WSL/i.test(e))
|
|
101
|
+
if (wslEntry) return path.join(packagesDir, wslEntry)
|
|
102
|
+
} catch {}
|
|
103
|
+
return packagesDir
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 解析 wsl -l -v 输出为发行版列表
|
|
108
|
+
* @param {string} out
|
|
109
|
+
* @returns {{ name: string, state: string, version: string, default: boolean }[]}
|
|
110
|
+
*/
|
|
111
|
+
export function parseDistroList(out) {
|
|
112
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
113
|
+
const distros = []
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (/NAME/i.test(line) && /STATE/i.test(line)) continue
|
|
116
|
+
const parts = line.split(/\s+/)
|
|
117
|
+
if (parts.length >= 3) {
|
|
118
|
+
const isDefault = parts[0] === '*'
|
|
119
|
+
const name = isDefault ? parts[1] : parts[0]
|
|
120
|
+
const state = isDefault ? parts[2] : parts[1]
|
|
121
|
+
const version = isDefault ? parts[3] : parts[2]
|
|
122
|
+
distros.push({ name, state, version, default: isDefault })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return distros
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 列出已安装的发行版
|
|
130
|
+
* @returns {{ name: string, state: string, version: string, default: boolean }[]}
|
|
131
|
+
*/
|
|
132
|
+
export function listDistros() {
|
|
133
|
+
requireWindows()
|
|
134
|
+
const out = execSync(`${WSL_EXE} -l -v`, { encoding: 'utf16le', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
135
|
+
return parseDistroList(out)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 解析 wsl -l -o 输出为可用发行版名称列表
|
|
140
|
+
* @param {string} out
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
export function parseAvailableList(out) {
|
|
144
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
145
|
+
return lines.slice(1).map((l) => l.split(/\s+/)[0]).filter(Boolean)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 列出可安装的发行版
|
|
150
|
+
* @returns {string[]}
|
|
151
|
+
*/
|
|
152
|
+
export function listAvailable() {
|
|
153
|
+
requireWindows()
|
|
154
|
+
const out = execSync(`${WSL_EXE} -l -o`, { encoding: 'utf16le', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
155
|
+
return parseAvailableList(out)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 安装指定发行版
|
|
160
|
+
* @param {string} name
|
|
161
|
+
* @returns {{ name: string, action: string }}
|
|
162
|
+
*/
|
|
163
|
+
export function installDistro(name) {
|
|
164
|
+
requireWindows()
|
|
165
|
+
execSync(`${WSL_EXE} --install -d ${name}`, { stdio: 'inherit', shell: true })
|
|
166
|
+
return { name, action: 'installed' }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 移除指定发行版
|
|
171
|
+
* @param {string} name
|
|
172
|
+
* @returns {{ name: string, action: string }}
|
|
173
|
+
*/
|
|
174
|
+
export function removeDistro(name) {
|
|
175
|
+
requireWindows()
|
|
176
|
+
execSync(`${WSL_EXE} --unregister ${name}`, { stdio: 'inherit', shell: true })
|
|
177
|
+
return { name, action: 'removed' }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 在当前终端启动发行版
|
|
182
|
+
* @param {string} name
|
|
183
|
+
* @returns {{ name: string, action: string }}
|
|
184
|
+
*/
|
|
185
|
+
export function startDistro(name) {
|
|
186
|
+
requireWindows()
|
|
187
|
+
execSync(`${WSL_EXE} -d ${name}`, { stdio: 'inherit', shell: true })
|
|
188
|
+
return { name, action: 'started' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 停止指定发行版
|
|
193
|
+
* @param {string} name
|
|
194
|
+
* @returns {{ name: string, action: string }}
|
|
195
|
+
*/
|
|
196
|
+
export function stopDistro(name) {
|
|
197
|
+
requireWindows()
|
|
198
|
+
execSync(`${WSL_EXE} --terminate ${name}`, { stdio: 'inherit', shell: true })
|
|
199
|
+
return { name, action: 'stopped' }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 停止所有发行版
|
|
204
|
+
* @returns {{ action: string }}
|
|
205
|
+
*/
|
|
206
|
+
export function stopAll() {
|
|
207
|
+
requireWindows()
|
|
208
|
+
execSync(`${WSL_EXE} --shutdown`, { stdio: 'inherit', shell: true })
|
|
209
|
+
return { action: 'all stopped' }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 设置默认发行版
|
|
214
|
+
* @param {string} name
|
|
215
|
+
* @returns {{ name: string, action: string }}
|
|
216
|
+
*/
|
|
217
|
+
export function setDefaultDistro(name) {
|
|
218
|
+
requireWindows()
|
|
219
|
+
execSync(`${WSL_EXE} --set-default ${name}`, { stdio: 'inherit', shell: true })
|
|
220
|
+
return { name, action: 'default set' }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 获取当前默认发行版名称
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
export function getDefaultDistro() {
|
|
228
|
+
if (!isWslInstalled()) return ''
|
|
229
|
+
const distros = listDistros()
|
|
230
|
+
const def = distros.find((d) => d.default)
|
|
231
|
+
return def ? def.name : ''
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 获取 WSL 状态
|
|
236
|
+
* @returns {{ installed: boolean, status?: string }}
|
|
237
|
+
*/
|
|
238
|
+
export function getWslStatus() {
|
|
239
|
+
if (!isWslInstalled()) return { installed: false }
|
|
240
|
+
try {
|
|
241
|
+
const out = execSync(`${WSL_EXE} --status`, { encoding: 'utf16le', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
242
|
+
return { installed: true, status: out.trim() }
|
|
243
|
+
} catch {
|
|
244
|
+
return { installed: true, status: '' }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 打开 PowerShell 窗口并进入 WSL
|
|
250
|
+
* @param {string} [distro] - 发行版名称(空则用默认)
|
|
251
|
+
* @returns {{ distro: string, action: string }}
|
|
252
|
+
*/
|
|
253
|
+
export function openWsl(distro) {
|
|
254
|
+
requireWindows()
|
|
255
|
+
const wslCmd = distro ? `wsl -d ${distro}` : 'wsl'
|
|
256
|
+
spawnSync('cmd.exe', ['/c', 'start', '"WSL"', 'powershell', '-NoExit', '-Command', wslCmd], { stdio: 'inherit' })
|
|
257
|
+
return { distro: distro || 'default', action: 'opened' }
|
|
258
|
+
}
|