@ranger1/dx 0.1.0

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/lib/exec.js ADDED
@@ -0,0 +1,643 @@
1
+ import { spawn, exec as nodeExec } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+ import { logger } from './logger.js'
4
+ import { envManager } from './env.js'
5
+ import { validateEnvironment } from './validate-env.js'
6
+ import { confirmManager } from './confirm.js'
7
+
8
+ const execPromise = promisify(nodeExec)
9
+
10
+ export class ExecManager {
11
+ constructor() {
12
+ this.runningProcesses = new Map()
13
+ this.processCounter = 0
14
+ this.setupSignalHandlers()
15
+ }
16
+
17
+ // 设置信号处理
18
+ setupSignalHandlers() {
19
+ const safeCleanup = () => {
20
+ try {
21
+ this.cleanup()
22
+ } catch {
23
+ // 忽略清理中的错误,避免影响主进程退出
24
+ }
25
+ }
26
+ process.on('SIGINT', safeCleanup)
27
+ process.on('SIGTERM', safeCleanup)
28
+ process.on('exit', safeCleanup)
29
+ }
30
+
31
+ // 执行单个命令
32
+ async executeCommand(command, options = {}) {
33
+ const {
34
+ app,
35
+ flags = {},
36
+ cwd,
37
+ stdio = 'inherit',
38
+ env: extraEnv = {},
39
+ timeout = 0,
40
+ retries = 0,
41
+ ports = [],
42
+ skipEnvValidation = false,
43
+ forcePortCleanup = false,
44
+ } = options
45
+
46
+ // 在执行前同步环境,确保 Nx/构建工具拿到规范的 NODE_ENV
47
+ if (process.env.APP_ENV && !process.env.NODE_ENV) {
48
+ envManager.syncEnvironments(process.env.APP_ENV)
49
+ }
50
+
51
+ const isVercelEnv = String(process.env.VERCEL || '').toLowerCase() === '1'
52
+ const skipValidation =
53
+ skipEnvValidation ||
54
+ Boolean(flags?.noEnvCheck) ||
55
+ String(process.env.AI_SKIP_ENV_CHECK || '').toLowerCase() === 'true' ||
56
+ isVercelEnv
57
+
58
+ // 检测环境(用于 dotenv 层选择)
59
+ const environment = envManager.detectEnvironment(flags)
60
+ logger.debug(`执行环境: ${environment}`)
61
+
62
+ let layeredEnv = {}
63
+ if (!skipValidation) {
64
+ validateEnvironment()
65
+ layeredEnv = envManager.collectEnvFromLayers(app, environment)
66
+ if (envManager.latestEnvWarnings && envManager.latestEnvWarnings.length > 0) {
67
+ envManager.latestEnvWarnings.forEach(message => {
68
+ logger.warn(message)
69
+ })
70
+ }
71
+
72
+ const effectiveEnv = { ...process.env, ...layeredEnv }
73
+ // CI 环境跳过后端环境变量校验(CI 中 build backend 只生成 OpenAPI,不需要数据库连接)
74
+ // 根据 app 参数确定需要检查的环境变量组
75
+ const isCI = process.env.CI === '1'
76
+ const appType = isCI ? null : (app === 'backend' ? 'backend' : app ? 'frontend' : null)
77
+ const requiredVars = envManager.getRequiredEnvVars(environment, appType)
78
+ if (requiredVars.length > 0) {
79
+ const { valid, missing, placeholders } = envManager.validateRequiredVars(
80
+ requiredVars,
81
+ effectiveEnv,
82
+ )
83
+ if (!valid) {
84
+ const problems = ['环境变量校验未通过']
85
+ if (missing.length > 0) {
86
+ problems.push(`缺少必填环境变量: ${missing.join(', ')}`)
87
+ }
88
+ if (placeholders.length > 0) {
89
+ problems.push(`以下环境变量仍为占位值或空串: ${placeholders.join(', ')}`)
90
+ }
91
+ if (missing.length > 0 || placeholders.length > 0) {
92
+ problems.push(`请在 .env.${environment} / .env.${environment}.local 中补齐配置`)
93
+ }
94
+ throw new Error(problems.join('\n'))
95
+ }
96
+ }
97
+ } else if (app) {
98
+ // 即便跳过校验,对于需要应用层的命令仍保留层级加载能力
99
+ layeredEnv = envManager.collectEnvFromLayers(app, environment)
100
+ }
101
+
102
+ // 处理端口冲突(开发服务自动清理,无需交互)
103
+ const autoSkipPortCleanup = this.isDevServerCommandString(command)
104
+ if (ports.length > 0) {
105
+ await this.handlePortConflicts(ports, flags.Y || autoSkipPortCleanup || forcePortCleanup)
106
+ }
107
+
108
+ // 在构建完整命令前判断是否为构建命令
109
+ // 构建类命令需要确保 NODE_ENV 为 production,且不允许 .env 覆盖该变量
110
+ // 识别构建类命令(包括 Nx 构建)以便:
111
+ // 1) 对 dotenv 传参时不使用 --override(避免覆盖我们显式传入的 NODE_ENV)
112
+ // 2) 在 spawn 时将 NODE_ENV 强制为 production
113
+ const isBuildCmdForWrapping =
114
+ /(?:^|\s)(?:pnpm|npm|yarn)\b.+\brun\s+build\b/.test(command) ||
115
+ /\bnext\s+build\b/.test(command) ||
116
+ // Nx 常见构建形式:nx build <proj> / nx run <proj>:build / nx run-many -t build
117
+ /\bnx\s+build\b/.test(command) ||
118
+ /\bnx\s+run\s+[^\s:]+:build\b/.test(command) ||
119
+ /\bnx\s+run-many\b[\s\S]*?(?:-t|--target)\s+build\b/.test(command)
120
+
121
+ // 构建完整命令
122
+ let fullCommand = command
123
+ if (app) {
124
+ const resolvedLayers = envManager.getResolvedEnvLayers(app, environment)
125
+ if (resolvedLayers.length > 0) {
126
+ const layerSummary = resolvedLayers.join(' -> ')
127
+ const envLabel = `${app}@${environment}`
128
+ logger.info(`dotenv层 ${envLabel}: ${layerSummary}`, '🌱')
129
+ }
130
+
131
+ const envFlags = envManager.buildEnvFlags(app, environment)
132
+ if (envFlags) {
133
+ // 对 build 命令禁用 dotenv 的 --override,避免覆盖我们显式传入的 NODE_ENV
134
+ const overrideFlag = isBuildCmdForWrapping ? '' : '--override'
135
+ const space = overrideFlag ? ' ' : ''
136
+ fullCommand = `pnpm exec dotenv ${overrideFlag}${space}${envFlags} -- ${command}`
137
+ }
138
+ }
139
+
140
+ logger.command(fullCommand)
141
+
142
+ // 执行命令(可能重试)
143
+ let lastError = null
144
+ for (let attempt = 0; attempt <= retries; attempt++) {
145
+ try {
146
+ if (attempt > 0) {
147
+ logger.info(`重试第 ${attempt} 次...`)
148
+ }
149
+
150
+ // next build 必须在 NODE_ENV=production 下运行;
151
+ // 但我们仍然按 environment 加载 .env.* 层(通过 dotenv flags 已处理)
152
+ const isBuildCommand =
153
+ /(?:^|\s)(?:pnpm|npm|yarn)\b.+\brun\s+build\b/.test(fullCommand) ||
154
+ /\bnext\s+build\b/.test(fullCommand) ||
155
+ /\bnx\s+build\b/.test(fullCommand) ||
156
+ /\bnx\s+run\s+[^\s:]+:build\b/.test(fullCommand) ||
157
+ /\bnx\s+run-many\b[\s\S]*?(?:-t|--target)\s+build\b/.test(fullCommand)
158
+ const isDevServerCommand = /\b(?:next\s+dev|run\s+start:dev|start:dev)\b/.test(fullCommand)
159
+ const baseNodeEnv = envManager.mapAppEnvToNodeEnv(process.env.APP_ENV || environment)
160
+ const nodeEnvForProcess = isBuildCommand
161
+ ? 'production'
162
+ : isDevServerCommand
163
+ ? 'development'
164
+ : baseNodeEnv
165
+
166
+ // 在 CI/非交互/生产层或构建命令下,强制使用轮询以避免 inotify 限制(无需 root)
167
+ const nonInteractive =
168
+ !(process.stdout && process.stdout.isTTY) || !(process.stdin && process.stdin.isTTY)
169
+ const inCI =
170
+ String(process.env.CI || '').toLowerCase() === 'true' ||
171
+ String(process.env.GITHUB_ACTIONS || '').toLowerCase() === 'true'
172
+ const isProdLayer =
173
+ ['production', 'staging'].includes(String(environment).toLowerCase()) ||
174
+ !!flags.prod ||
175
+ !!flags.production ||
176
+ !!flags.staging
177
+ const shouldForcePolling = isBuildCommand || inCI || nonInteractive || isProdLayer
178
+
179
+ const forcedEnv = {}
180
+ if (shouldForcePolling) {
181
+ if (process.env.NX_DAEMON === undefined && extraEnv.NX_DAEMON === undefined)
182
+ forcedEnv.NX_DAEMON = 'false'
183
+ if (
184
+ process.env.CHOKIDAR_USEPOLLING === undefined &&
185
+ extraEnv.CHOKIDAR_USEPOLLING === undefined
186
+ )
187
+ forcedEnv.CHOKIDAR_USEPOLLING = '1'
188
+ if (
189
+ process.env.WATCHPACK_POLLING === undefined &&
190
+ extraEnv.WATCHPACK_POLLING === undefined
191
+ )
192
+ forcedEnv.WATCHPACK_POLLING = 'true'
193
+ if (
194
+ process.env.CHOKIDAR_INTERVAL === undefined &&
195
+ extraEnv.CHOKIDAR_INTERVAL === undefined
196
+ )
197
+ forcedEnv.CHOKIDAR_INTERVAL = '1000'
198
+
199
+ const forcedPairs = Object.entries(forcedEnv)
200
+ .map(([k, v]) => `${k}=${v}`)
201
+ .join(', ')
202
+ if (forcedPairs) {
203
+ logger.info(`已启用轮询模式: ${forcedPairs}`)
204
+ }
205
+ }
206
+
207
+ const result = await this.spawnCommand(fullCommand, {
208
+ cwd: cwd || process.cwd(),
209
+ stdio,
210
+ env: {
211
+ ...process.env,
212
+ NODE_ENV: nodeEnvForProcess,
213
+ ...forcedEnv,
214
+ ...extraEnv,
215
+ },
216
+ timeout,
217
+ })
218
+
219
+ logger.success(`命令执行成功: ${command}`)
220
+ return result
221
+ } catch (error) {
222
+ lastError = error
223
+
224
+ // 尝试智能错误修复
225
+ const fixApplied = await this.tryAutoFix(error, {
226
+ command,
227
+ app,
228
+ environment,
229
+ ports,
230
+ skipConfirm: flags.Y || autoSkipPortCleanup || forcePortCleanup,
231
+ })
232
+
233
+ if (fixApplied && attempt < retries) {
234
+ continue // 重试
235
+ }
236
+
237
+ if (attempt === retries) {
238
+ logger.error(`命令执行失败 (${attempt + 1}/${retries + 1} 次尝试): ${command}`)
239
+ throw error
240
+ }
241
+ }
242
+ }
243
+
244
+ throw lastError
245
+ }
246
+
247
+ // Spawn 命令
248
+ async spawnCommand(command, options = {}) {
249
+ return new Promise((resolve, reject) => {
250
+ const processId = ++this.processCounter
251
+
252
+ // 强制默认使用独立的 stdio 配置,防止在父进程管道关闭时向已关闭的管道写入
253
+ const spawnOptions = {
254
+ stdio: options.stdio ?? 'inherit',
255
+ cwd: options.cwd,
256
+ env: options.env,
257
+ detached: options.detached ?? false,
258
+ timeout: options.timeout,
259
+ }
260
+ const childProcess = spawn('bash', ['-c', command], spawnOptions)
261
+ this.runningProcesses.set(processId, {
262
+ process: childProcess,
263
+ command,
264
+ startTime: Date.now(),
265
+ })
266
+
267
+ // 超时处理
268
+ let timeoutId = null
269
+ let isKilled = false
270
+ if (options.timeout > 0) {
271
+ timeoutId = setTimeout(() => {
272
+ isKilled = true
273
+ childProcess.kill('SIGTERM')
274
+ // 给进程一些时间优雅退出
275
+ setTimeout(() => {
276
+ if (!childProcess.killed) {
277
+ childProcess.kill('SIGKILL')
278
+ }
279
+ }, 2000)
280
+ reject(new Error(`命令执行超时 (${options.timeout}ms): ${command}`))
281
+ }, options.timeout)
282
+ }
283
+
284
+ childProcess.on('exit', code => {
285
+ if (timeoutId) clearTimeout(timeoutId)
286
+ this.runningProcesses.delete(processId)
287
+
288
+ if (isKilled) return // 已经被超时处理了
289
+
290
+ if (code === 0) {
291
+ resolve({ success: true, code, processId })
292
+ } else {
293
+ reject(new Error(`进程退出码: ${code}`))
294
+ }
295
+ })
296
+
297
+ childProcess.on('error', error => {
298
+ if (timeoutId) clearTimeout(timeoutId)
299
+ this.runningProcesses.delete(processId)
300
+ // 忽略 EPIPE/ERR_STREAM_WRITE_AFTER_END 错误,避免清理阶段卡住
301
+ const code = error?.code
302
+ if (code === 'EPIPE' || code === 'ERR_STREAM_WRITE_AFTER_END') {
303
+ return resolve({ success: true, code: 0, processId })
304
+ }
305
+ reject(error)
306
+ })
307
+ })
308
+ }
309
+
310
+ // 并发执行命令
311
+ async executeConcurrent(commands, options = {}) {
312
+ logger.step('并发执行多个命令')
313
+
314
+ const processes = commands.map((cmd, index) => {
315
+ const cmdOptions = typeof cmd === 'string' ? { ...options } : { ...options, ...cmd.options }
316
+
317
+ const command = typeof cmd === 'string' ? cmd : cmd.command
318
+
319
+ logger.info(`启动进程 ${index + 1}: ${command}`)
320
+
321
+ return this.executeCommand(command, cmdOptions).catch(error => ({
322
+ error,
323
+ command,
324
+ index,
325
+ }))
326
+ })
327
+
328
+ const results = await Promise.allSettled(processes)
329
+
330
+ // 分析结果
331
+ const successful = results.filter(r => r.status === 'fulfilled' && !r.value.error)
332
+ const failed = results.filter(r => r.status === 'rejected' || r.value?.error)
333
+
334
+ logger.info(`并发执行完成: ${successful.length} 成功, ${failed.length} 失败`)
335
+
336
+ if (failed.length > 0) {
337
+ logger.error('失败的命令:')
338
+ failed.forEach((failure, index) => {
339
+ const cmd = commands[failure.value?.index || index]
340
+ const command = typeof cmd === 'string' ? cmd : cmd.command
341
+ logger.error(` - ${command}`)
342
+ })
343
+ }
344
+
345
+ return { results, successful: successful.length, failed: failed.length }
346
+ }
347
+
348
+ // 顺序执行命令
349
+ async executeSequential(commands, options = {}) {
350
+ logger.step('顺序执行多个命令')
351
+
352
+ const results = []
353
+
354
+ for (let i = 0; i < commands.length; i++) {
355
+ const cmd = commands[i]
356
+ const cmdOptions = typeof cmd === 'string' ? { ...options } : { ...options, ...cmd.options }
357
+
358
+ const command = typeof cmd === 'string' ? cmd : cmd.command
359
+
360
+ logger.info(`执行命令 ${i + 1}/${commands.length}: ${command}`)
361
+
362
+ try {
363
+ const result = await this.executeCommand(command, cmdOptions)
364
+ results.push({ success: true, result, command, index: i })
365
+ } catch (error) {
366
+ logger.error(`命令 ${i + 1} 执行失败,停止后续执行`)
367
+ results.push({ success: false, error, command, index: i })
368
+
369
+ // 顺序执行遇到错误时停止
370
+ if (!options.continueOnError) {
371
+ throw error
372
+ }
373
+ }
374
+ }
375
+
376
+ const successful = results.filter(r => r.success).length
377
+ const failed = results.length - successful
378
+
379
+ logger.info(`顺序执行完成: ${successful} 成功, ${failed} 失败`)
380
+ return { results, successful, failed }
381
+ }
382
+
383
+ // 端口冲突处理
384
+ async handlePortConflicts(ports, skipConfirm = false) {
385
+ for (const port of ports) {
386
+ try {
387
+ const processes = await this.getPortProcesses(port)
388
+ if (processes.length > 0) {
389
+ const shouldKill = await confirmManager.confirmPortCleanup(port, processes, skipConfirm)
390
+
391
+ if (shouldKill) {
392
+ await this.killPortProcesses(port)
393
+ logger.success(`端口 ${port} 已清理`)
394
+
395
+ // 等待端口释放
396
+ await this.waitForPortFree(port)
397
+ } else {
398
+ logger.warn(`跳过端口 ${port} 清理,可能会导致启动失败`)
399
+ }
400
+ }
401
+ } catch (error) {
402
+ logger.warn(`检查端口 ${port} 时出错: ${error.message}`)
403
+ }
404
+ }
405
+ }
406
+
407
+ // 获取占用端口的进程
408
+ async getPortProcesses(port) {
409
+ try {
410
+ const { stdout } = await execPromise(`lsof -t -i:${port}`)
411
+ return stdout.trim() ? stdout.trim().split('\n') : []
412
+ } catch (error) {
413
+ return []
414
+ }
415
+ }
416
+
417
+ // 杀死端口进程
418
+ async killPortProcesses(port) {
419
+ try {
420
+ const { stdout } = await execPromise(`lsof -t -i:${port}`)
421
+ const pids = stdout
422
+ .trim()
423
+ .split('\n')
424
+ .filter(pid => pid)
425
+
426
+ if (pids.length > 0) {
427
+ logger.info(`开发环境清理(不会在 CI/生产执行): kill -9 ${pids.join(', ')}`)
428
+ await execPromise(`kill -9 ${pids.join(' ')}`)
429
+ }
430
+
431
+ // 额外清理:若为后端端口 3000,同时尝试终止可能存活的 nodemon 后台进程
432
+ if (Number(port) === 3000) {
433
+ try {
434
+ logger.info('开发环境扩展清理: pkill -f "nodemon.*backend"')
435
+ await execPromise('pkill -f "nodemon.*backend" || true')
436
+ } catch (extraError) {
437
+ // 忽略该步骤失败,以免影响主流程
438
+ logger.debug(`扩展清理(pkill nodemon backend)时出错: ${extraError.message}`)
439
+ }
440
+ }
441
+
442
+ if (pids.length > 0) {
443
+ return { killed: true, pids }
444
+ }
445
+ } catch (error) {
446
+ logger.debug(`杀死端口 ${port} 进程时出错: ${error.message}`)
447
+ }
448
+ return { killed: false, pids: [] }
449
+ }
450
+
451
+ // 等待端口释放
452
+ async waitForPortFree(port, maxWait = 10000) {
453
+ const startTime = Date.now()
454
+
455
+ while (Date.now() - startTime < maxWait) {
456
+ const processes = await this.getPortProcesses(port)
457
+ if (processes.length === 0) {
458
+ return true
459
+ }
460
+ await new Promise(resolve => setTimeout(resolve, 500))
461
+ }
462
+
463
+ throw new Error(`端口 ${port} 未能在 ${maxWait}ms 内释放`)
464
+ }
465
+
466
+ // 等待端口就绪
467
+ async waitForPort(port, maxWait = 60000) {
468
+ logger.progress(`等待端口 ${port} 就绪`)
469
+
470
+ const startTime = Date.now()
471
+ while (Date.now() - startTime < maxWait) {
472
+ try {
473
+ await execPromise(`nc -z localhost ${port}`)
474
+ logger.progressDone()
475
+ return true
476
+ } catch (error) {
477
+ await new Promise(resolve => setTimeout(resolve, 2000))
478
+ process.stdout.write('.')
479
+ }
480
+ }
481
+
482
+ logger.progressDone()
483
+ throw new Error(`端口 ${port} 在 ${maxWait}ms 内未就绪`)
484
+ }
485
+
486
+ // 智能错误修复
487
+ async tryAutoFix(error, context) {
488
+ const { app, environment, ports, skipConfirm } = context
489
+
490
+ // 端口占用错误
491
+ if (this.isPortInUseError(error)) {
492
+ logger.warn('检测到端口占用错误,尝试自动修复...')
493
+ if (ports && ports.length > 0) {
494
+ await this.handlePortConflicts(ports, skipConfirm)
495
+ return true
496
+ }
497
+ }
498
+
499
+ // 缺少环境变量错误
500
+ if (this.isMissingEnvError(error)) {
501
+ logger.warn('检测到环境变量缺失错误')
502
+ return await this.fixMissingEnv(environment)
503
+ }
504
+
505
+ // Prisma 客户端未生成错误
506
+ if (this.isPrismaNotGeneratedError(error)) {
507
+ logger.warn('检测到 Prisma 客户端未生成,尝试自动修复...')
508
+ return await this.fixPrismaGenerate(app, environment, skipConfirm)
509
+ }
510
+
511
+ // 依赖缺失错误
512
+ if (this.isMissingDependencyError(error)) {
513
+ logger.warn('检测到依赖缺失,尝试自动修复...')
514
+ return await this.fixMissingDependency(skipConfirm)
515
+ }
516
+
517
+ return false
518
+ }
519
+
520
+ // 判断是否为开发服务器命令(Next/Vite/Nest 开发模式)
521
+ isDevServerCommandString(cmd) {
522
+ // 采用多条简单规则组合,避免复杂正则导致误判
523
+ const rules = [
524
+ /\bnext\s+dev\b/,
525
+ /\bvite(?:\s|$)/,
526
+ /\b(?:pnpm|npm|yarn)\s+(?:--filter\s+\S+\s+)?(?:run\s+)?start:dev\b/,
527
+ /\bstart:dev\b/,
528
+ ]
529
+ return rules.some(r => r.test(cmd))
530
+ }
531
+
532
+ // 错误类型检测方法
533
+ isPortInUseError(error) {
534
+ const message = error.message || ''
535
+ return (
536
+ message.includes('EADDRINUSE') ||
537
+ message.includes('address already in use') ||
538
+ (message.includes('端口') && message.includes('占用'))
539
+ )
540
+ }
541
+
542
+ isMissingEnvError(error) {
543
+ const message = error.message || ''
544
+ return (
545
+ message.includes('environment variable') ||
546
+ (message.includes('env') && message.includes('undefined'))
547
+ )
548
+ }
549
+
550
+ isPrismaNotGeneratedError(error) {
551
+ const message = error.message || ''
552
+ return (
553
+ message.includes('Prisma') && (message.includes('generate') || message.includes('client'))
554
+ )
555
+ }
556
+
557
+ isMissingDependencyError(error) {
558
+ const message = error.message || ''
559
+ return message.includes('Cannot find module') || message.includes('command not found')
560
+ }
561
+
562
+ // 错误修复方法
563
+ async fixMissingEnv(environment) {
564
+ logger.info('环境变量修复建议:')
565
+ logger.info(`1. 检查 .env.${environment} 文件`)
566
+ logger.info('2. 检查对应环境的 .env.<env>.local(例如 .env.development.local)')
567
+ logger.info('3. 参考 .env.example(模板示例,不参与运行时加载)')
568
+ return false // 需要用户手动修复
569
+ }
570
+
571
+ async fixPrismaGenerate(app, environment, skipConfirm = false) {
572
+ if (!skipConfirm) {
573
+ const shouldFix = await confirmManager.confirm('是否自动生成 Prisma 客户端?', true)
574
+ if (!shouldFix) return false
575
+ }
576
+
577
+ try {
578
+ // 直接使用 Nx 执行 prisma:generate,避免依赖根 package.json scripts
579
+ await this.executeCommand('npx nx prisma:generate backend', {
580
+ app: 'backend',
581
+ flags: { [environment]: true },
582
+ // 禁用 Nx 缓存,确保实际执行生成步骤
583
+ env: { NX_CACHE: 'false' },
584
+ })
585
+ logger.success('Prisma 客户端生成成功')
586
+ return true
587
+ } catch (error) {
588
+ logger.error('Prisma 客户端生成失败')
589
+ return false
590
+ }
591
+ }
592
+
593
+ async fixMissingDependency(skipConfirm = false) {
594
+ if (!skipConfirm) {
595
+ const shouldInstall = await confirmManager.confirm('是否自动安装缺失的依赖?', true)
596
+ if (!shouldInstall) return false
597
+ }
598
+
599
+ try {
600
+ await this.executeCommand('pnpm install')
601
+ logger.success('依赖安装成功')
602
+ return true
603
+ } catch (error) {
604
+ logger.error('依赖安装失败')
605
+ return false
606
+ }
607
+ }
608
+
609
+ // 清理所有进程
610
+ cleanup() {
611
+ if (this.runningProcesses.size === 0) return
612
+
613
+ logger.info(`清理 ${this.runningProcesses.size} 个运行中的进程...`)
614
+
615
+ for (const [id, { process, command }] of this.runningProcesses) {
616
+ try {
617
+ logger.debug(`终止进程 ${id}: ${command}`)
618
+ // 直接使用 SIGKILL,不等待优雅退出
619
+ try {
620
+ process.kill('SIGKILL')
621
+ } catch {}
622
+ } catch (error) {
623
+ logger.debug(`清理进程 ${id} 时出错: ${error.message}`)
624
+ }
625
+ }
626
+
627
+ this.runningProcesses.clear()
628
+ }
629
+
630
+ // 获取运行状态
631
+ getStatus() {
632
+ return {
633
+ runningProcesses: this.runningProcesses.size,
634
+ processes: Array.from(this.runningProcesses.entries()).map(([id, info]) => ({
635
+ id,
636
+ command: info.command,
637
+ duration: Date.now() - info.startTime,
638
+ })),
639
+ }
640
+ }
641
+ }
642
+
643
+ export const execManager = new ExecManager()