@icyfenix-dmla/cli 2026.4.17-546
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/bin/dmla.js +5 -0
- package/icyfenix-dmla-cli-2026.4.17-546.tgz +0 -0
- package/package.json +52 -0
- package/src/commands/manage.js +334 -0
- package/src/commands/server.js +293 -0
- package/src/index.js +104 -0
- package/tests/cli.test.js +34 -0
package/bin/dmla.js
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icyfenix-dmla/cli",
|
|
3
|
+
"version": "2026.4.17-546",
|
|
4
|
+
"description": "DMLA 沙箱服务命令行工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dmla": "./bin/dmla.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "node --experimental-vm-modules $(npm root)/jest/bin/jest.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"commander": "^12.1.0",
|
|
16
|
+
"chalk": "^5.3.0",
|
|
17
|
+
"enquirer": "^2.4.1",
|
|
18
|
+
"dockerode": "^4.0.2",
|
|
19
|
+
"express": "^4.21.2",
|
|
20
|
+
"cors": "^2.8.5"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"jest": "^29.7.0"
|
|
24
|
+
},
|
|
25
|
+
"jest": {
|
|
26
|
+
"testEnvironment": "node",
|
|
27
|
+
"transform": {},
|
|
28
|
+
"moduleFileExtensions": [
|
|
29
|
+
"js",
|
|
30
|
+
"mjs"
|
|
31
|
+
],
|
|
32
|
+
"testMatch": [
|
|
33
|
+
"**/tests/**/*.test.js"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"dmla",
|
|
41
|
+
"sandbox",
|
|
42
|
+
"python",
|
|
43
|
+
"machine-learning",
|
|
44
|
+
"cli"
|
|
45
|
+
],
|
|
46
|
+
"author": "icyfenix",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/icyfenix/dmla"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 管理命令(安装、更新、诊断)
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import Docker from 'dockerode'
|
|
6
|
+
import { spawn, execSync } from 'child_process'
|
|
7
|
+
import http from 'http'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import { fileURLToPath } from 'url'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = path.dirname(__filename)
|
|
14
|
+
|
|
15
|
+
const docker = new Docker()
|
|
16
|
+
|
|
17
|
+
// 配置
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
imageCpu: 'dmla-sandbox:cpu',
|
|
20
|
+
imageGpu: 'dmla-sandbox:gpu',
|
|
21
|
+
dockerhubRegistry: 'icyfenix',
|
|
22
|
+
tcrRegistry: 'ccr.ccs.tencentyun.com/icyfenix',
|
|
23
|
+
imageName: 'dmla-sandbox',
|
|
24
|
+
defaultPort: 3001
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 获取镜像仓库地址
|
|
29
|
+
*/
|
|
30
|
+
function getRegistryUrl(registry) {
|
|
31
|
+
if (registry === 'tcr') {
|
|
32
|
+
return `${CONFIG.tcrRegistry}/${CONFIG.imageName}`
|
|
33
|
+
}
|
|
34
|
+
return `${CONFIG.dockerhubRegistry}/${CONFIG.imageName}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 安装镜像
|
|
39
|
+
*/
|
|
40
|
+
export async function installImages(types, registry = 'dockerhub') {
|
|
41
|
+
const registryUrl = getRegistryUrl(registry)
|
|
42
|
+
|
|
43
|
+
console.log(chalk.gray(` 从 ${registry === 'tcr' ? '腾讯云 TCR' : 'Docker Hub'} 拉取镜像`))
|
|
44
|
+
|
|
45
|
+
for (const type of types) {
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(chalk.bold(`📥 拉取 ${type.toUpperCase()} 版本镜像...`))
|
|
48
|
+
|
|
49
|
+
const remoteImage = `${registryUrl}:${type}`
|
|
50
|
+
const localImage = type === 'gpu' ? CONFIG.imageGpu : CONFIG.imageCpu
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 拉取镜像
|
|
54
|
+
await pullImageWithProgress(remoteImage)
|
|
55
|
+
|
|
56
|
+
// Tag 为本地名称
|
|
57
|
+
console.log(chalk.gray(` 重命名为 ${localImage}...`))
|
|
58
|
+
const image = docker.getImage(remoteImage)
|
|
59
|
+
await image.tag({ repo: CONFIG.imageName, tag: type })
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像安装完成`))
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.log(chalk.red(`❌ ${type.toUpperCase()} 镜像安装失败: ${error.message}`))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log()
|
|
68
|
+
console.log(chalk.green('🎉 镜像安装完成'))
|
|
69
|
+
console.log(chalk.yellow('💡 提示: 运行 dmla start 启动服务'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 带进度显示的镜像拉取
|
|
74
|
+
*/
|
|
75
|
+
async function pullImageWithProgress(imageName) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
docker.pull(imageName, (err, stream) => {
|
|
78
|
+
if (err) {
|
|
79
|
+
reject(err)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 解析进度
|
|
84
|
+
docker.modem.followProgress(stream, (err, output) => {
|
|
85
|
+
if (err) {
|
|
86
|
+
reject(err)
|
|
87
|
+
} else {
|
|
88
|
+
resolve(output)
|
|
89
|
+
}
|
|
90
|
+
}, (event) => {
|
|
91
|
+
// 显示进度
|
|
92
|
+
if (event.status) {
|
|
93
|
+
let progress = event.status
|
|
94
|
+
if (event.progress) {
|
|
95
|
+
progress += ` ${event.progress}`
|
|
96
|
+
}
|
|
97
|
+
if (event.id) {
|
|
98
|
+
console.log(chalk.gray(` [${event.id}] ${progress}`))
|
|
99
|
+
} else {
|
|
100
|
+
console.log(chalk.gray(` ${progress}`))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 更新所有组件
|
|
110
|
+
*/
|
|
111
|
+
export async function updateAll(registry = 'dockerhub') {
|
|
112
|
+
console.log()
|
|
113
|
+
|
|
114
|
+
// 更新 npm 包
|
|
115
|
+
console.log(chalk.bold('📦 更新 npm 包'))
|
|
116
|
+
try {
|
|
117
|
+
console.log(chalk.gray(' 执行 npm update -g @icyfenix-dmla/cli...'))
|
|
118
|
+
execSync('npm update -g @icyfenix-dmla/cli', { stdio: 'inherit' })
|
|
119
|
+
console.log(chalk.green('✅ npm 包已更新'))
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.log(chalk.yellow('⚠️ npm 包更新失败或已是最新版本'))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log()
|
|
125
|
+
|
|
126
|
+
// 检查并更新镜像
|
|
127
|
+
console.log(chalk.bold('🖼️ 检查 Docker 镜像更新'))
|
|
128
|
+
const registryUrl = getRegistryUrl(registry)
|
|
129
|
+
|
|
130
|
+
for (const type of ['cpu', 'gpu']) {
|
|
131
|
+
const remoteImage = `${registryUrl}:${type}`
|
|
132
|
+
const localImage = type === 'gpu' ? CONFIG.imageGpu : CONFIG.imageCpu
|
|
133
|
+
|
|
134
|
+
console.log(chalk.gray(` 检查 ${type.toUpperCase()} 版本...`))
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// 检查本地镜像是否存在
|
|
138
|
+
let localImageInfo = null
|
|
139
|
+
try {
|
|
140
|
+
localImageInfo = await docker.getImage(localImage).inspect()
|
|
141
|
+
} catch {
|
|
142
|
+
// 本地镜像不存在,需要拉取
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 拉取最新镜像
|
|
146
|
+
console.log(chalk.gray(` 拉取最新 ${type.toUpperCase()} 镜像...`))
|
|
147
|
+
await pullImageWithProgress(remoteImage)
|
|
148
|
+
|
|
149
|
+
// 获取拉取的镜像信息
|
|
150
|
+
const remoteImageInfo = await docker.getImage(remoteImage).inspect()
|
|
151
|
+
|
|
152
|
+
// 比较镜像 ID
|
|
153
|
+
if (localImageInfo && localImageInfo.Id === remoteImageInfo.Id) {
|
|
154
|
+
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已是最新版本`))
|
|
155
|
+
} else {
|
|
156
|
+
// Tag 为本地名称
|
|
157
|
+
console.log(chalk.gray(` 重命名为 ${localImage}...`))
|
|
158
|
+
const image = docker.getImage(remoteImage)
|
|
159
|
+
await image.tag({ repo: CONFIG.imageName, tag: type })
|
|
160
|
+
|
|
161
|
+
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已更新`))
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.log(chalk.yellow(`⚠️ ${type.toUpperCase()} 镜像更新失败: ${error.message}`))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log()
|
|
169
|
+
console.log(chalk.green('🎉 更新完成'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 环境诊断
|
|
174
|
+
*/
|
|
175
|
+
export async function runDoctor() {
|
|
176
|
+
console.log()
|
|
177
|
+
const issues = []
|
|
178
|
+
|
|
179
|
+
// ───────────────────────────────────────────────────────────
|
|
180
|
+
// Docker 检查
|
|
181
|
+
// ───────────────────────────────────────────────────────────
|
|
182
|
+
console.log(chalk.bold('🐳 Docker 环境'))
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const dockerInfo = await docker.info()
|
|
186
|
+
console.log(chalk.green(' ✅ Docker 已安装'))
|
|
187
|
+
console.log(chalk.gray(` 版本: ${dockerInfo.ServerVersion || '未知'}`))
|
|
188
|
+
|
|
189
|
+
// 检查版本是否满足要求
|
|
190
|
+
const minVersion = '20.10'
|
|
191
|
+
if (dockerInfo.ServerVersion && dockerInfo.ServerVersion < minVersion) {
|
|
192
|
+
issues.push(`Docker 版本过低,建议升级到 ${minVersion} 或更高`)
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.log(chalk.red(' ❌ Docker 未安装或未运行'))
|
|
196
|
+
issues.push('请安装 Docker 并确保服务正在运行')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log()
|
|
200
|
+
|
|
201
|
+
// ───────────────────────────────────────────────────────────
|
|
202
|
+
// 镜像检查
|
|
203
|
+
// ───────────────────────────────────────────────────────────
|
|
204
|
+
console.log(chalk.bold('🖼️ Docker 镜像'))
|
|
205
|
+
|
|
206
|
+
const cpuImage = CONFIG.imageCpu
|
|
207
|
+
const gpuImage = CONFIG.imageGpu
|
|
208
|
+
|
|
209
|
+
let cpuExists = false
|
|
210
|
+
let gpuExists = false
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const cpuInfo = await docker.getImage(cpuImage).inspect()
|
|
214
|
+
cpuExists = true
|
|
215
|
+
console.log(chalk.green(` ✅ CPU 镜像已安装`))
|
|
216
|
+
console.log(chalk.gray(` 大小: ${Math.round(cpuInfo.Size / 1024 / 1024)} MB`))
|
|
217
|
+
} catch {
|
|
218
|
+
console.log(chalk.red(` ❌ CPU 镜像未安装`))
|
|
219
|
+
issues.push('运行 dmla install --cpu 安装 CPU 镜像')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const gpuInfo = await docker.getImage(gpuImage).inspect()
|
|
224
|
+
gpuExists = true
|
|
225
|
+
console.log(chalk.green(` ✅ GPU 镜像已安装`))
|
|
226
|
+
console.log(chalk.gray(` 大小: ${Math.round(gpuInfo.Size / 1024 / 1024)} MB`))
|
|
227
|
+
} catch {
|
|
228
|
+
console.log(chalk.yellow(` ⚠️ GPU 镜像未安装`))
|
|
229
|
+
console.log(chalk.gray(' (可选,仅在需要 GPU 时安装)'))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log()
|
|
233
|
+
|
|
234
|
+
// ───────────────────────────────────────────────────────────
|
|
235
|
+
// GPU 检查
|
|
236
|
+
// ───────────────────────────────────────────────────────────
|
|
237
|
+
console.log(chalk.bold('🎮 GPU 驱动'))
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const output = execSync('nvidia-smi -L', { timeout: 5000, encoding: 'utf8' })
|
|
241
|
+
if (output.includes('GPU')) {
|
|
242
|
+
console.log(chalk.green(' ✅ NVIDIA GPU 可用'))
|
|
243
|
+
const lines = output.split('\n').filter(l => l.trim())
|
|
244
|
+
lines.forEach(line => console.log(chalk.gray(` ${line.trim()}`)))
|
|
245
|
+
|
|
246
|
+
// 检查 GPU 镜像
|
|
247
|
+
if (!gpuExists) {
|
|
248
|
+
console.log(chalk.yellow(' 💡 检测到 GPU,建议安装 GPU 镜像'))
|
|
249
|
+
issues.push('运行 dmla install --gpu 安装 GPU 镜像')
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
console.log(chalk.gray(' GPU 不可用'))
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
console.log(chalk.gray(' GPU 不可用'))
|
|
256
|
+
console.log(chalk.gray(' (如果需要 GPU,请安装 NVIDIA 驱动)'))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log()
|
|
260
|
+
|
|
261
|
+
// ───────────────────────────────────────────────────────────
|
|
262
|
+
// 端口检查
|
|
263
|
+
// ───────────────────────────────────────────────────────────
|
|
264
|
+
console.log(chalk.bold('🔌 端口可用性'))
|
|
265
|
+
|
|
266
|
+
const port = CONFIG.defaultPort
|
|
267
|
+
const portAvailable = await checkPortAvailable(port)
|
|
268
|
+
|
|
269
|
+
if (portAvailable) {
|
|
270
|
+
console.log(chalk.green(` ✅ 端口 ${port} 可用`))
|
|
271
|
+
} else {
|
|
272
|
+
console.log(chalk.red(` ❌ 端口 ${port} 已被占用`))
|
|
273
|
+
issues.push(`端口 ${port} 已被占用,使用 --port 指定其他端口`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log()
|
|
277
|
+
|
|
278
|
+
// ───────────────────────────────────────────────────────────
|
|
279
|
+
// 网络连通性
|
|
280
|
+
// ───────────────────────────────────────────────────────────
|
|
281
|
+
console.log(chalk.bold('🌐 网络连通性'))
|
|
282
|
+
|
|
283
|
+
// 测试 Docker Hub
|
|
284
|
+
console.log(chalk.gray(' 测试 Docker Hub 连接...'))
|
|
285
|
+
try {
|
|
286
|
+
execSync('docker pull icyfenix/dmla-sandbox:cpu --quiet', { timeout: 10000 })
|
|
287
|
+
console.log(chalk.green(' ✅ Docker Hub 连接正常'))
|
|
288
|
+
} catch {
|
|
289
|
+
console.log(chalk.yellow(' ⚠️ Docker Hub 连接超时或受限'))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 测试 TCR
|
|
293
|
+
console.log(chalk.gray(' 测试腾讯云 TCR 连接...'))
|
|
294
|
+
try {
|
|
295
|
+
execSync('docker pull ccr.ccs.tencentyun.com/icyfenix/dmla-sandbox:cpu --quiet', { timeout: 10000 })
|
|
296
|
+
console.log(chalk.green(' ✅ TCR 连接正常'))
|
|
297
|
+
} catch {
|
|
298
|
+
console.log(chalk.yellow(' ⚠️ TCR 连接超时或受限'))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log()
|
|
302
|
+
|
|
303
|
+
// ───────────────────────────────────────────────────────────
|
|
304
|
+
// 问题汇总
|
|
305
|
+
// ───────────────────────────────────────────────────────────
|
|
306
|
+
if (issues.length > 0) {
|
|
307
|
+
console.log(chalk.bold.red('❌ 发现以下问题:'))
|
|
308
|
+
console.log()
|
|
309
|
+
issues.forEach((issue, i) => {
|
|
310
|
+
console.log(chalk.red(` ${i + 1}. ${issue}`))
|
|
311
|
+
})
|
|
312
|
+
console.log()
|
|
313
|
+
console.log(chalk.yellow('💡 请根据上述提示解决问题后再次运行 dmla doctor'))
|
|
314
|
+
} else {
|
|
315
|
+
console.log(chalk.bold.green('✅ 所有检查通过,环境正常'))
|
|
316
|
+
console.log()
|
|
317
|
+
console.log(chalk.gray('💡 运行 dmla start 启动服务'))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 检查端口是否可用
|
|
323
|
+
*/
|
|
324
|
+
async function checkPortAvailable(port) {
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
const server = http.createServer()
|
|
327
|
+
server.once('error', () => resolve(false))
|
|
328
|
+
server.once('listening', () => {
|
|
329
|
+
server.close()
|
|
330
|
+
resolve(true)
|
|
331
|
+
})
|
|
332
|
+
server.listen(port)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务管理命令
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import Docker from 'dockerode'
|
|
6
|
+
import { spawn } from 'child_process'
|
|
7
|
+
import http from 'http'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import { fileURLToPath } from 'url'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = path.dirname(__filename)
|
|
14
|
+
|
|
15
|
+
const docker = new Docker()
|
|
16
|
+
|
|
17
|
+
// 配置
|
|
18
|
+
const CONFIG = {
|
|
19
|
+
imageCpu: 'dmla-sandbox:cpu',
|
|
20
|
+
imageGpu: 'dmla-sandbox:gpu',
|
|
21
|
+
defaultPort: 3001
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 检查端口是否可用
|
|
26
|
+
*/
|
|
27
|
+
async function checkPortAvailable(port) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const server = http.createServer()
|
|
30
|
+
server.once('error', () => resolve(false))
|
|
31
|
+
server.once('listening', () => {
|
|
32
|
+
server.close()
|
|
33
|
+
resolve(true)
|
|
34
|
+
})
|
|
35
|
+
server.listen(port)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 检查镜像是否存在
|
|
41
|
+
*/
|
|
42
|
+
async function checkImageExists(type) {
|
|
43
|
+
const image = type === 'gpu' ? CONFIG.imageGpu : CONFIG.imageCpu
|
|
44
|
+
try {
|
|
45
|
+
await docker.getImage(image).inspect()
|
|
46
|
+
return true
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 检查 GPU 是否可用
|
|
54
|
+
*/
|
|
55
|
+
async function checkGPUAvailable() {
|
|
56
|
+
try {
|
|
57
|
+
// 尝试运行 nvidia-smi 命令
|
|
58
|
+
const result = await new Promise((resolve, reject) => {
|
|
59
|
+
const proc = spawn('nvidia-smi', ['-L'], { timeout: 5000 })
|
|
60
|
+
let output = ''
|
|
61
|
+
proc.stdout.on('data', (data) => output += data.toString())
|
|
62
|
+
proc.stderr.on('data', (data) => output += data.toString())
|
|
63
|
+
proc.on('close', (code) => {
|
|
64
|
+
if (code === 0) resolve(output)
|
|
65
|
+
else reject(new Error('nvidia-smi failed'))
|
|
66
|
+
})
|
|
67
|
+
proc.on('error', reject)
|
|
68
|
+
})
|
|
69
|
+
return result.includes('GPU')
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 检查服务是否运行
|
|
77
|
+
*/
|
|
78
|
+
async function checkServiceRunning(port) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const req = http.request({
|
|
81
|
+
hostname: 'localhost',
|
|
82
|
+
port: port,
|
|
83
|
+
path: '/api/health',
|
|
84
|
+
method: 'GET',
|
|
85
|
+
timeout: 2000
|
|
86
|
+
}, (res) => {
|
|
87
|
+
resolve(res.statusCode === 200)
|
|
88
|
+
})
|
|
89
|
+
req.on('error', () => resolve(false))
|
|
90
|
+
req.on('timeout', () => {
|
|
91
|
+
req.destroy()
|
|
92
|
+
resolve(false)
|
|
93
|
+
})
|
|
94
|
+
req.end()
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 查找运行中的服务容器
|
|
100
|
+
*/
|
|
101
|
+
async function findServiceContainer() {
|
|
102
|
+
try {
|
|
103
|
+
const containers = await docker.listContainers({ all: true })
|
|
104
|
+
// 查找 dmla 服务容器
|
|
105
|
+
for (const container of containers) {
|
|
106
|
+
if (container.Names.some(name => name.includes('dmla-server'))) {
|
|
107
|
+
return container
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 启动服务
|
|
118
|
+
*/
|
|
119
|
+
export async function startServer(port, useGpu = false) {
|
|
120
|
+
// 检查端口
|
|
121
|
+
const portAvailable = await checkPortAvailable(port)
|
|
122
|
+
if (!portAvailable) {
|
|
123
|
+
console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
|
|
124
|
+
console.log(chalk.yellow('💡 提示: 使用 --port 选项指定其他端口'))
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 检查镜像
|
|
129
|
+
const imageType = useGpu ? 'gpu' : 'cpu'
|
|
130
|
+
const imageExists = await checkImageExists(imageType)
|
|
131
|
+
if (!imageExists) {
|
|
132
|
+
console.log(chalk.red(`❌ 镜像 ${useGpu ? CONFIG.imageGpu : CONFIG.imageCpu} 不存在`))
|
|
133
|
+
console.log(chalk.yellow('💡 提示: 运行 dmla install 安装镜像'))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 检查服务是否已运行
|
|
138
|
+
const alreadyRunning = await checkServiceRunning(port)
|
|
139
|
+
if (alreadyRunning) {
|
|
140
|
+
console.log(chalk.green(`✅ 服务已在端口 ${port} 运行`))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 启动服务
|
|
145
|
+
console.log(chalk.gray(' 正在启动...'))
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// 使用 spawn 启动 server 进程
|
|
149
|
+
const serverPath = path.resolve(__dirname, '../../../local-server/src/index.js')
|
|
150
|
+
|
|
151
|
+
// 如果 server 文件不存在,说明是独立安装模式,需要启动内置服务
|
|
152
|
+
const standaloneServerPath = path.resolve(__dirname, '../server/index.js')
|
|
153
|
+
|
|
154
|
+
const actualServerPath = fs.existsSync(serverPath) ? serverPath :
|
|
155
|
+
fs.existsSync(standaloneServerPath) ? standaloneServerPath : null
|
|
156
|
+
|
|
157
|
+
if (!actualServerPath) {
|
|
158
|
+
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
159
|
+
console.log(chalk.yellow('💡 提示: 确保正确安装了 @icyfenix-dmla/cli'))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const env = {
|
|
164
|
+
...process.env,
|
|
165
|
+
PORT: port.toString(),
|
|
166
|
+
USE_GPU: useGpu ? 'true' : 'false'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const serverProcess = spawn('node', [actualServerPath], {
|
|
170
|
+
env,
|
|
171
|
+
stdio: 'inherit',
|
|
172
|
+
detached: true
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
serverProcess.unref()
|
|
176
|
+
|
|
177
|
+
// 等待服务启动
|
|
178
|
+
console.log(chalk.gray(' 等待服务就绪...'))
|
|
179
|
+
let attempts = 0
|
|
180
|
+
const maxAttempts = 30
|
|
181
|
+
|
|
182
|
+
while (attempts < maxAttempts) {
|
|
183
|
+
const running = await checkServiceRunning(port)
|
|
184
|
+
if (running) {
|
|
185
|
+
console.log(chalk.green(`✅ 服务已启动: http://localhost:${port}`))
|
|
186
|
+
console.log(chalk.gray(` 健康检查: http://localhost:${port}/api/health`))
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
190
|
+
attempts++
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(chalk.yellow('⚠️ 服务启动超时,请检查日志'))
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.log(chalk.red(`❌ 启动失败: ${error.message}`))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 停止服务
|
|
201
|
+
*/
|
|
202
|
+
export async function stopServer() {
|
|
203
|
+
// 查找运行中的容器
|
|
204
|
+
const container = await findServiceContainer()
|
|
205
|
+
|
|
206
|
+
if (container) {
|
|
207
|
+
try {
|
|
208
|
+
const containerObj = docker.getContainer(container.Id)
|
|
209
|
+
await containerObj.stop()
|
|
210
|
+
await containerObj.remove()
|
|
211
|
+
console.log(chalk.green('✅ 服务已停止'))
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.log(chalk.red(`❌ 停止失败: ${error.message}`))
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// 尝试通过端口查找进程
|
|
217
|
+
console.log(chalk.yellow('⚠️ 未找到运行中的服务容器'))
|
|
218
|
+
console.log(chalk.gray(' 提示: 服务可能以非容器模式运行'))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 获取状态
|
|
224
|
+
*/
|
|
225
|
+
export async function getStatus() {
|
|
226
|
+
console.log()
|
|
227
|
+
|
|
228
|
+
// 检查 npm 包版本
|
|
229
|
+
console.log(chalk.bold('📦 npm 包版本'))
|
|
230
|
+
try {
|
|
231
|
+
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
232
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
233
|
+
console.log(chalk.gray(` @icyfenix-dmla/cli: ${pkg.version}`))
|
|
234
|
+
} catch {
|
|
235
|
+
console.log(chalk.gray(' 版本信息不可用'))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log()
|
|
239
|
+
|
|
240
|
+
// 检查镜像
|
|
241
|
+
console.log(chalk.bold('🖼️ Docker 镜像'))
|
|
242
|
+
const cpuExists = await checkImageExists('cpu')
|
|
243
|
+
const gpuExists = await checkImageExists('gpu')
|
|
244
|
+
console.log(chalk.gray(` CPU: ${cpuExists ? chalk.green('已安装') : chalk.red('未安装')}`))
|
|
245
|
+
console.log(chalk.gray(` GPU: ${gpuExists ? chalk.green('已安装') : chalk.red('未安装')}`))
|
|
246
|
+
|
|
247
|
+
console.log()
|
|
248
|
+
|
|
249
|
+
// 检查 GPU
|
|
250
|
+
console.log(chalk.bold('🎮 GPU 状态'))
|
|
251
|
+
const gpuAvailable = await checkGPUAvailable()
|
|
252
|
+
if (gpuAvailable) {
|
|
253
|
+
console.log(chalk.green(' GPU 可用'))
|
|
254
|
+
try {
|
|
255
|
+
const proc = spawn('nvidia-smi', ['-L'])
|
|
256
|
+
proc.stdout.on('data', (data) => {
|
|
257
|
+
const lines = data.toString().split('\n').filter(l => l.trim())
|
|
258
|
+
lines.forEach(line => console.log(chalk.gray(` ${line}`)))
|
|
259
|
+
})
|
|
260
|
+
} catch {}
|
|
261
|
+
} else {
|
|
262
|
+
console.log(chalk.gray(' GPU 不可用'))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log()
|
|
266
|
+
|
|
267
|
+
// 检查服务
|
|
268
|
+
console.log(chalk.bold('🚀 服务状态'))
|
|
269
|
+
const running = await checkServiceRunning(CONFIG.defaultPort)
|
|
270
|
+
if (running) {
|
|
271
|
+
console.log(chalk.green(` 服务运行中 (端口 ${CONFIG.defaultPort})`))
|
|
272
|
+
try {
|
|
273
|
+
// 获取详细状态
|
|
274
|
+
const healthUrl = `http://localhost:${CONFIG.defaultPort}/api/sandbox/health`
|
|
275
|
+
http.get(healthUrl, (res) => {
|
|
276
|
+
let data = ''
|
|
277
|
+
res.on('data', (chunk) => data += chunk)
|
|
278
|
+
res.on('end', () => {
|
|
279
|
+
try {
|
|
280
|
+
const health = JSON.parse(data)
|
|
281
|
+
if (health.images) {
|
|
282
|
+
console.log(chalk.gray(` CPU 镜像: ${health.images.cpu ? '就绪' : '未就绪'}`))
|
|
283
|
+
console.log(chalk.gray(` GPU 镜像: ${health.images.gpu ? '就绪' : '未就绪'}`))
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
} catch {}
|
|
289
|
+
} else {
|
|
290
|
+
console.log(chalk.gray(' 服务未运行'))
|
|
291
|
+
console.log(chalk.yellow(' 提示: 运行 dmla start 启动服务'))
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DMLA CLI 入口
|
|
3
|
+
* 沙箱服务命令行管理工具
|
|
4
|
+
*/
|
|
5
|
+
import { program } from 'commander'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { startServer, stopServer, getStatus } from './commands/server.js'
|
|
8
|
+
import { installImages, updateAll, runDoctor } from './commands/manage.js'
|
|
9
|
+
|
|
10
|
+
const VERSION = '0.0.0' // 将在发布时由 workflow 更新
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('dmla')
|
|
14
|
+
.description('DMLA 沙箱服务命令行管理工具')
|
|
15
|
+
.version(VERSION)
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────
|
|
18
|
+
// start 命令
|
|
19
|
+
// ─────────────────────────────────────────────────────────────
|
|
20
|
+
program
|
|
21
|
+
.command('start')
|
|
22
|
+
.description('启动沙箱服务')
|
|
23
|
+
.option('-p, --port <number>', '服务端口', '3001')
|
|
24
|
+
.option('--gpu', '使用 GPU 镜像')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
const port = parseInt(options.port, 10)
|
|
27
|
+
const useGpu = options.gpu
|
|
28
|
+
console.log(chalk.blue('🚀 启动 DMLA 沙箱服务...'))
|
|
29
|
+
console.log(chalk.gray(` 端口: ${port}`))
|
|
30
|
+
console.log(chalk.gray(` 镜像: ${useGpu ? 'GPU' : 'CPU'}`))
|
|
31
|
+
await startServer(port, useGpu)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────
|
|
35
|
+
// stop 命令
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
program
|
|
38
|
+
.command('stop')
|
|
39
|
+
.description('停止运行中的沙箱服务')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
console.log(chalk.blue('🛑 停止 DMLA 沙箱服务...'))
|
|
42
|
+
await stopServer()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────
|
|
46
|
+
// status 命令
|
|
47
|
+
// ─────────────────────────────────────────────────────────────
|
|
48
|
+
program
|
|
49
|
+
.command('status')
|
|
50
|
+
.description('查看服务状态')
|
|
51
|
+
.action(async () => {
|
|
52
|
+
console.log(chalk.blue('📊 DMLA 沙箱服务状态'))
|
|
53
|
+
await getStatus()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────
|
|
57
|
+
// install 命令
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
program
|
|
60
|
+
.command('install')
|
|
61
|
+
.description('安装 Docker 镜像')
|
|
62
|
+
.option('--cpu', '仅安装 CPU 版本')
|
|
63
|
+
.option('--gpu', '仅安装 GPU 版本')
|
|
64
|
+
.option('--all', '安装所有镜像(默认)')
|
|
65
|
+
.option('-r, --registry <type>', '镜像仓库 (dockerhub/tcr)', 'dockerhub')
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
const registry = options.registry
|
|
68
|
+
let types = []
|
|
69
|
+
|
|
70
|
+
if (options.cpu) types.push('cpu')
|
|
71
|
+
if (options.gpu) types.push('gpu')
|
|
72
|
+
if (types.length === 0 || options.all) types = ['cpu', 'gpu']
|
|
73
|
+
|
|
74
|
+
console.log(chalk.blue('📦 安装 DMLA Docker 镜像...'))
|
|
75
|
+
console.log(chalk.gray(` 仓库: ${registry}`))
|
|
76
|
+
console.log(chalk.gray(` 类型: ${types.join(', ')}`))
|
|
77
|
+
|
|
78
|
+
await installImages(types, registry)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────
|
|
82
|
+
// update 命令
|
|
83
|
+
// ─────────────────────────────────────────────────────────────
|
|
84
|
+
program
|
|
85
|
+
.command('update')
|
|
86
|
+
.description('更新 npm 包和 Docker 镜像')
|
|
87
|
+
.option('-r, --registry <type>', '镜像仓库 (dockerhub/tcr)', 'dockerhub')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
console.log(chalk.blue('🔄 更新 DMLA...'))
|
|
90
|
+
await updateAll(options.registry)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────────
|
|
94
|
+
// doctor 命令
|
|
95
|
+
// ─────────────────────────────────────────────────────────────
|
|
96
|
+
program
|
|
97
|
+
.command('doctor')
|
|
98
|
+
.description('诊断安装环境')
|
|
99
|
+
.action(async () => {
|
|
100
|
+
console.log(chalk.blue('🔍 DMLA 环境诊断'))
|
|
101
|
+
await runDoctor()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
program.parse()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 命令单元测试
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeAll } from '@jest/globals'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
|
|
9
|
+
// ESM 中获取 __dirname
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
|
|
13
|
+
// 基础测试:确保模块可导入
|
|
14
|
+
describe('CLI Module', () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
// 设置测试环境
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should have correct package.json', async () => {
|
|
20
|
+
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
22
|
+
|
|
23
|
+
expect(pkg.name).toBe('@icyfenix-dmla/cli')
|
|
24
|
+
expect(pkg.bin).toBeDefined()
|
|
25
|
+
expect(pkg.bin.dmla).toBe('./bin/dmla.js')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should have commander dependency', async () => {
|
|
29
|
+
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
31
|
+
|
|
32
|
+
expect(pkg.dependencies.commander).toBeDefined()
|
|
33
|
+
})
|
|
34
|
+
})
|