@quicktvui/web-cli 1.0.8 → 2.1.1

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/qt-web-cli.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * QuickTVUI Web Dev CLI
5
- * 启动 web 开发服务器,无需手动配置入口文件
4
+ * QuickTVUI Web CLI v2
5
+ *
6
+ * v2 架构:委托构建 + Bundle 加载模式
7
+ * - 调用项目的 dev 脚本构建
8
+ * - 监听 dist/dev 目录
9
+ * - 启动轻量静态服务器加载 dist/dev 的 bundle
10
+ *
11
+ * v1 回退:当项目没有 dev 脚本时,使用原有的 webpack 自构建模式
6
12
  */
7
13
 
8
14
  const path = require('path')
@@ -13,8 +19,8 @@ const { exec } = require('shelljs')
13
19
 
14
20
  // 解析命令行参数
15
21
  const args = minimist(process.argv.slice(2), {
16
- string: ['port', 'config'],
17
- boolean: ['help', 'open', 'v', 'info', 'version', 'watch'],
22
+ string: ['port', 'config', 'devScript', 'distDir'],
23
+ boolean: ['help', 'open', 'v', 'info', 'version', 'watch', 'noAutoBuild', 'v1'],
18
24
  alias: {
19
25
  p: 'port',
20
26
  c: 'config',
@@ -26,6 +32,8 @@ const args = minimist(process.argv.slice(2), {
26
32
  port: 39001,
27
33
  open: process.env.QUICKTVUI_NO_OPEN !== 'true',
28
34
  watch: false,
35
+ noAutoBuild: false,
36
+ v1: false,
29
37
  },
30
38
  })
31
39
 
@@ -36,12 +44,12 @@ if (args.v || args.version) {
36
44
  process.exit(0)
37
45
  }
38
46
 
39
- // 显示详细信息(包括 web-renderer 版本)
47
+ // 显示详细信息
40
48
  if (args.info) {
41
49
  const cliPkg = require(path.join(__dirname, '../package.json'))
42
50
  console.log(`@quicktvui/web-cli v${cliPkg.version}`)
51
+ console.log(` Mode: v2 (delegate build)`)
43
52
  try {
44
- // 尝试多种路径查找 web-renderer
45
53
  let rendererPkg
46
54
  const possiblePaths = [
47
55
  '@quicktvui/web-renderer/package.json',
@@ -52,9 +60,7 @@ if (args.info) {
52
60
  try {
53
61
  rendererPkg = require(pkgPath)
54
62
  break
55
- } catch (e) {
56
- // 继续尝试下一个路径
57
- }
63
+ } catch (e) {}
58
64
  }
59
65
  if (rendererPkg) {
60
66
  console.log(`@quicktvui/web-renderer v${rendererPkg.version}`)
@@ -70,57 +76,274 @@ if (args.info) {
70
76
  // 显示帮助信息
71
77
  if (args.help) {
72
78
  console.log(`
73
- QuickTVUI Web CLI
79
+ QuickTVUI Web CLI v2
74
80
 
75
81
  Usage:
76
82
  qt-web-cli [options]
77
83
 
78
84
  Options:
79
- -p, --port <port> 开发服务器端口 (默认: 39001)
80
- -c, --config <path> 自定义 webpack 配置文件路径
81
- -o, --open 自动打开浏览器
82
- -w, --watch 监听文件变化自动重启服务器
83
- -h, --help 显示帮助信息
84
- -v, --version 显示 CLI 版本
85
- --info 显示 CLI web-renderer 版本
85
+ -p, --port <port> 开发服务器端口 (默认: 39001)
86
+ -o, --open 自动打开浏览器
87
+ -h, --help 显示帮助信息
88
+ -v, --version 显示 CLI 版本
89
+ --info 显示 CLI 和 web-renderer 版本
90
+ --dev-script <name> 指定要运行的 npm script (默认: dev)
91
+ --dist-dir <path> 指定构建产物目录 (默认: dist/dev)
92
+ --no-auto-build 跳过自动调用 dev 脚本,仅启动服务器
93
+ --v1 使用 v1 模式 (webpack 自构建,已废弃)
94
+
95
+ v2 模式 (默认):
96
+ 自动检测项目的 dev 脚本,调用 npm run dev 构建产物
97
+ 监听 dist/dev/ 目录变化,通过 web-runtime 加载 bundle
98
+ 支持热更新 (SSE)
99
+
100
+ v1 模式 (--v1):
101
+ 使用内置 webpack 配置自行构建项目 (已废弃)
86
102
 
87
103
  Examples:
88
- qt-web-cli
89
- qt-web-cli --port 8080
90
- qt-web-cli --config ./my.webpack.js
91
- qt-web-cli --watch
92
- qt-web-cli -v
93
- qt-web-cli --info
94
-
95
- 特点:
96
- - 自动检测项目入口 (main-native.ts/js, main.ts/js)
97
- - 零配置启动 web 开发服务器
98
- - 支持文件变化自动重启 (--watch)
104
+ qt-web-cli # v2 模式,自动构建+启动
105
+ qt-web-cli --no-auto-build # v2 模式,仅启动服务器(需先手动 npm run dev)
106
+ qt-web-cli --dev-script build # 使用 build 脚本替代 dev
107
+ qt-web-cli --port 8080 # 指定端口
108
+ qt-web-cli --v1 # 使用 v1 模式
99
109
  `)
100
110
  process.exit(0)
101
111
  }
102
112
 
113
+ // ============================================================================
114
+ // 主入口
115
+ // ============================================================================
116
+
103
117
  async function main() {
104
- // 如果启用了 watch 模式,使用 watcher 脚本
105
- if (args.watch) {
106
- const { spawn } = require('child_process')
107
- const watcherPath = path.join(__dirname, 'qt-web-cli-watch.js')
108
- const watcherArgs = process.argv.slice(2).filter((arg) => arg !== '--watch' && arg !== '-w')
109
-
110
- const watcher = spawn('node', [watcherPath, ...watcherArgs], {
111
- cwd: process.cwd(),
112
- stdio: 'inherit',
113
- env: process.env,
114
- })
118
+ // 强制使用 v1 模式
119
+ if (args.v1) {
120
+ signale.warn('v1 模式已废弃,建议迁移到 v2 模式')
121
+ return runV1()
122
+ }
123
+
124
+ signale.pending('正在启动 QuickTVUI Web CLI v2 ...')
125
+
126
+ // 查找项目根目录
127
+ const projectRoot = findProjectRoot()
128
+ if (!projectRoot) {
129
+ signale.error('无法找到项目根目录')
130
+ process.exit(1)
131
+ }
132
+
133
+ signale.info(`项目根目录: ${projectRoot}`)
134
+
135
+ // 读取 package.json
136
+ let pkg = {}
137
+ try {
138
+ pkg = require(path.join(projectRoot, 'package.json'))
139
+ } catch (e) {}
140
+
141
+ // 检测是否支持 v2 模式
142
+ const devScript = detectDevScript(pkg, args.devScript)
143
+ const distDir = args.distDir || 'dist/dev'
144
+ const distFullPath = path.join(projectRoot, distDir)
145
+
146
+ if (!devScript && !fs.existsSync(distFullPath)) {
147
+ signale.warn('项目未配置 dev 脚本且 dist/dev 目录不存在,回退到 v1 模式')
148
+ return runV1()
149
+ }
150
+
151
+ // ============================================================================
152
+ // v2 模式
153
+ // ============================================================================
154
+
155
+ const DevBuildManager = require('../lib/DevBuildManager')
156
+ const BundleWatcher = require('../lib/BundleWatcher')
157
+ const DevServer = require('../lib/DevServer')
158
+ const HotReloader = require('../lib/HotReloader')
159
+
160
+ // 初始化各模块
161
+ const hotReloader = new HotReloader()
162
+ const bundleWatcher = new BundleWatcher(projectRoot, {
163
+ onChange: (event) => {
164
+ if (event.isBundle) {
165
+ // 通知浏览器刷新
166
+ hotReloader.notifyBundleUpdate({
167
+ file: event.file,
168
+ bundleEntry: event.bundleEntry,
169
+ bundleUrlPath: event.bundleUrlPath,
170
+ })
171
+ }
172
+ },
173
+ })
174
+
175
+ const devServer = new DevServer(projectRoot, {
176
+ port: args.port,
177
+ hotReloader,
178
+ bundleUrlPath: `/dist/dev/index.bundle`,
179
+ })
180
+
181
+ // 记录是否已打开浏览器(避免重复打开)
182
+ let browserOpened = false
183
+
184
+ const devBuild = new DevBuildManager(projectRoot, {
185
+ pkg,
186
+ devScript: args.devScript || 'dev',
187
+ onOutput: (text, stream) => {
188
+ // 将构建输出透传到控制台
189
+ if (stream === 'stdout') {
190
+ process.stdout.write(text)
191
+ } else {
192
+ process.stderr.write(text)
193
+ }
194
+ },
195
+ onReady: () => {
196
+ // 构建完成后,通知浏览器
197
+ hotReloader.notifyBuildStatus('ready', '构建完成')
198
+
199
+ // 更新 bundle URL
200
+ const bundleUrlPath = bundleWatcher.getBundleUrlPath()
201
+ if (bundleUrlPath) {
202
+ devServer.setBundleUrlPath(bundleUrlPath)
203
+ }
115
204
 
116
- watcher.on('exit', (code) => {
117
- process.exit(code || 0)
205
+ // 构建就绪后打开浏览器(只打开一次)
206
+ if (args.open && !browserOpened) {
207
+ browserOpened = true
208
+ const urlPath = bundleUrlPath || '/dist/dev/index.bundle'
209
+ const url = `http://localhost:${args.port}?bundle=${urlPath}`
210
+ openBrowser(url)
211
+ }
212
+
213
+ // 清除 HotReloader 中缓存的 bundle-update 事件
214
+ // 防止 SSE 重连后再次触发页面刷新导致无限循环
215
+ hotReloader.clearLastEvent()
216
+ },
217
+ })
218
+
219
+ // 检测已有构建产物
220
+ const existingBundle = devBuild.detectExistingBundle()
221
+ const shouldAutoBuild = !args.noAutoBuild && devScript
222
+
223
+ if (existingBundle) {
224
+ signale.success(
225
+ `检测到已有构建产物: ${distDir}/${existingBundle.entry} (${formatSize(existingBundle.size)})`
226
+ )
227
+
228
+ // 更新 bundle URL
229
+ devServer.setBundleUrlPath(`/dist/dev/${existingBundle.entry}`)
230
+ } else if (shouldAutoBuild) {
231
+ signale.info('未检测到构建产物,将自动启动 dev 构建')
232
+ } else {
233
+ signale.warn('未检测到构建产物,且未启用自动构建')
234
+ signale.warn('请先手动运行: npm run dev')
235
+ }
236
+
237
+ // 启动文件监听
238
+ bundleWatcher.start()
239
+
240
+ // ============================================================================
241
+ // 端口预检测
242
+ // ============================================================================
243
+ const net = require('net')
244
+
245
+ // 检测 web-cli 端口(39001)
246
+ const webCliPortAvailable = await checkPortAvailable(net, args.port)
247
+ if (!webCliPortAvailable) {
248
+ const processInfo = getPortProcessInfo(args.port)
249
+ const pidInfo = processInfo
250
+ ? processInfo.name
251
+ ? `PID: ${processInfo.pid} (${processInfo.name})`
252
+ : `PID: ${processInfo.pid}`
253
+ : '无法获取进程信息'
254
+ signale.warn(`Web CLI 端口 ${args.port} 已被占用: ${pidInfo}`)
255
+
256
+ const killed = await promptKillPort(args.port, processInfo)
257
+ if (!killed) {
258
+ signale.error(`端口 ${args.port} 已被占用,请使用 --port 指定其他端口`)
259
+ process.exit(1)
260
+ }
261
+ }
262
+
263
+ // 检测 debug-server 端口(38989)— 仅在使用 qt-dev 的项目中提示
264
+ // Vue3 项目使用 qt-dev,会启动 debug-server 监听 38989
265
+ // Vue2 项目使用纯 webpack,不涉及 38989 端口
266
+ const isQtDevProject =
267
+ devScript &&
268
+ (devScript.script.includes('qt-dev') ||
269
+ devScript.script.includes('es3-debug-server') ||
270
+ devScript.script.includes('hippy-debug-server'))
271
+ if (isQtDevProject) {
272
+ const debugServerPort = 38989
273
+ const debugPortAvailable = await checkPortAvailable(net, debugServerPort)
274
+ if (!debugPortAvailable) {
275
+ const processInfo = getPortProcessInfo(debugServerPort)
276
+ const pidInfo = processInfo
277
+ ? processInfo.name
278
+ ? `PID: ${processInfo.pid} (${processInfo.name})`
279
+ : `PID: ${processInfo.pid}`
280
+ : '无法获取进程信息'
281
+ signale.warn(`Debug Server 端口 ${debugServerPort} 已被占用: ${pidInfo}`)
282
+ signale.warn('如遇 dev 启动失败,请先关闭占用该端口的进程')
283
+ }
284
+ }
285
+
286
+ // 先启动开发服务器(确保浏览器打开时服务器已就绪)
287
+ try {
288
+ await devServer.start()
289
+ signale.success(`开发服务器已启动: http://localhost:${args.port}`)
290
+
291
+ // 构建状态通知
292
+ hotReloader.notifyBuildStatus(
293
+ existingBundle ? 'ready' : 'building',
294
+ existingBundle ? 'Bundle 已就绪' : '等待构建完成...'
295
+ )
296
+
297
+ // 仅在不需要自动构建且已有产物时立即打开浏览器
298
+ // 否则等 dev 构建完成后再打开(在 onReady 回调中)
299
+ if (args.open && existingBundle && !shouldAutoBuild && !browserOpened) {
300
+ browserOpened = true
301
+ const bundleUrlPath = bundleWatcher.getBundleUrlPath() || '/dist/dev/index.bundle'
302
+ const url = `http://localhost:${args.port}?bundle=${bundleUrlPath}`
303
+ openBrowser(url)
304
+ }
305
+ } catch (err) {
306
+ signale.error('开发服务器启动失败:', err.message)
307
+ process.exit(1)
308
+ }
309
+
310
+ // 启动 dev 构建(服务器已就绪,构建完成后在 onReady 回调中打开浏览器)
311
+ if (shouldAutoBuild && !existingBundle) {
312
+ try {
313
+ await devBuild.start()
314
+ } catch (err) {
315
+ signale.error('dev 构建启动失败:', err.message)
316
+ signale.warn('你可以手动运行 npm run dev,然后使用 --no-auto-build 重新启动 CLI')
317
+ }
318
+ } else if (shouldAutoBuild && existingBundle) {
319
+ // 已有构建产物,在后台启动 dev 构建
320
+ // 等待 webpack 编译完成后再打开浏览器(而不是立即打开)
321
+ signale.info('已有构建产物,后台启动 dev 构建用于热更新...')
322
+ devBuild.start().catch((err) => {
323
+ signale.warn('后台 dev 构建启动失败:', err.message)
118
324
  })
325
+ }
119
326
 
120
- return
327
+ // 优雅退出
328
+ const cleanup = () => {
329
+ signale.info('正在停止...')
330
+ devBuild.stop()
331
+ bundleWatcher.stop()
332
+ devServer.stop()
333
+ signale.success('已停止')
334
+ process.exit(0)
121
335
  }
122
336
 
123
- signale.pending('正在启动 QuickTVUI Web 开发服务器...')
337
+ process.on('SIGINT', cleanup)
338
+ process.on('SIGTERM', cleanup)
339
+ }
340
+
341
+ // ============================================================================
342
+ // v1 模式 (webpack 自构建,已废弃)
343
+ // ============================================================================
344
+
345
+ function runV1() {
346
+ signale.pending('正在启动 QuickTVUI Web CLI v1 (webpack 模式)...')
124
347
 
125
348
  // 查找项目根目录
126
349
  const projectRoot = findProjectRoot()
@@ -141,7 +364,6 @@ async function main() {
141
364
  let mainEntry
142
365
  let entryType
143
366
 
144
- // 入口候选列表
145
367
  const entryCandidates = [
146
368
  { name: 'src/main-native.ts', path: './src/main-native.ts', type: 'package' },
147
369
  { name: 'src/main-native.js', path: './src/main-native.js', type: 'package' },
@@ -162,10 +384,7 @@ async function main() {
162
384
  }
163
385
 
164
386
  if (!mainEntry) {
165
- signale.error('无法找到有效的入口文件,请检查以下路径:')
166
- signale.error(' - src/main-native.ts 或 src/main-native.js')
167
- signale.error(' - src/main.ts 或 src/main.js')
168
- signale.error(' - package.json 中的 main 字段')
387
+ signale.error('无法找到有效的入口文件')
169
388
  process.exit(1)
170
389
  }
171
390
 
@@ -194,17 +413,15 @@ async function main() {
194
413
  }
195
414
 
196
415
  /**
197
- * 使用 webpack Node API 启动开发服务器
416
+ * 使用 webpack Node API 启动开发服务器 (v1)
198
417
  */
199
418
  function startDevServer(configPath, port, shouldOpen, userConfigPath) {
200
419
  const webpack = require('webpack')
201
420
  const WebpackDevServer = require('webpack-dev-server')
202
421
  const { merge } = require('webpack-merge')
203
422
 
204
- // 加载内置配置
205
423
  let config = require(configPath)
206
424
 
207
- // 如果用户指定了自定义配置,进行合并
208
425
  if (userConfigPath) {
209
426
  if (!fs.existsSync(userConfigPath)) {
210
427
  signale.error(`自定义配置文件不存在: ${userConfigPath}`)
@@ -230,8 +447,7 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
230
447
  signale.success(`开发服务器已启动: http://localhost:${port}`)
231
448
 
232
449
  if (shouldOpen) {
233
- const url = `http://localhost:${port}`
234
- openBrowser(url)
450
+ openBrowser(`http://localhost:${port}`)
235
451
  }
236
452
  })
237
453
 
@@ -243,45 +459,45 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
243
459
  })
244
460
  }
245
461
 
246
- /**
247
- * 等待服务器就绪
248
- */
249
- function waitForServer(url, timeout = 30000) {
250
- const http = require('http')
251
- const startTime = Date.now()
252
-
253
- return new Promise((resolve, reject) => {
254
- function check() {
255
- if (Date.now() - startTime > timeout) {
256
- reject(new Error('等待服务器超时'))
257
- return
258
- }
462
+ // ============================================================================
463
+ // 工具函数
464
+ // ============================================================================
259
465
 
260
- const req = http.get(url, (res) => {
261
- if (res.statusCode === 200 || res.statusCode === 304) {
262
- resolve()
263
- } else {
264
- setTimeout(check, 500)
265
- }
266
- })
267
-
268
- req.on('error', () => {
269
- setTimeout(check, 500)
270
- })
466
+ function findProjectRoot(startDir = process.cwd()) {
467
+ let currentDir = startDir
468
+ while (currentDir !== path.dirname(currentDir)) {
469
+ if (fs.existsSync(path.join(currentDir, 'package.json'))) {
470
+ return currentDir
471
+ }
472
+ currentDir = path.dirname(currentDir)
473
+ }
474
+ return null
475
+ }
271
476
 
272
- req.setTimeout(1000, () => {
273
- req.destroy()
274
- setTimeout(check, 500)
275
- })
477
+ function detectDevScript(pkg, scriptName) {
478
+ const scripts = pkg.scripts || {}
479
+ const candidates = [scriptName, 'dev', 'dev:android', 'build:dev']
480
+ for (const name of candidates) {
481
+ if (name && scripts[name]) {
482
+ return { name, script: scripts[name] }
276
483
  }
484
+ }
485
+ return null
486
+ }
277
487
 
278
- check()
279
- })
488
+ function checkOpenSSLVersion(version) {
489
+ if (!version) return false
490
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
491
+ if (!match) return false
492
+ return parseInt(match[1], 10) >= 3
493
+ }
494
+
495
+ function formatSize(bytes) {
496
+ if (bytes < 1024) return bytes + ' B'
497
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
498
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
280
499
  }
281
500
 
282
- /**
283
- * 打开浏览器
284
- */
285
501
  function openBrowser(url) {
286
502
  const platform = process.platform
287
503
  const command =
@@ -294,30 +510,119 @@ function openBrowser(url) {
294
510
 
295
511
  if (result.code === 0) {
296
512
  signale.success('已打开浏览器')
297
- return
513
+ } else {
514
+ signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
298
515
  }
516
+ }
299
517
 
300
- signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
518
+ /**
519
+ * 检测端口是否可用(通过尝试绑定)
520
+ */
521
+ function checkPortAvailable(net, port) {
522
+ return new Promise((resolve) => {
523
+ const tester = net.createServer()
524
+ tester.once('error', (err) => {
525
+ if (err.code === 'EADDRINUSE') {
526
+ resolve(false)
527
+ } else {
528
+ resolve(false)
529
+ }
530
+ })
531
+ tester.once('listening', () => {
532
+ tester.close()
533
+ resolve(true)
534
+ })
535
+ tester.listen(port)
536
+ })
301
537
  }
302
538
 
303
- function findProjectRoot(startDir = process.cwd()) {
304
- let currentDir = startDir
305
- while (currentDir !== path.dirname(currentDir)) {
306
- if (fs.existsSync(path.join(currentDir, 'package.json'))) {
307
- return currentDir
539
+ /**
540
+ * 获取占用端口的进程信息
541
+ */
542
+ function getPortProcessInfo(port) {
543
+ try {
544
+ const { execSync } = require('child_process')
545
+ const isWindows = process.platform === 'win32'
546
+ let cmd
547
+ if (isWindows) {
548
+ cmd = `netstat -ano | findstr :${port} | findstr LISTENING`
549
+ } else {
550
+ cmd = `lsof -i :${port} -P -n -t 2>/dev/null`
308
551
  }
309
- currentDir = path.dirname(currentDir)
552
+ const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim()
553
+ if (!result) return null
554
+
555
+ if (isWindows) {
556
+ const match = result.match(/LISTENING\s+(\d+)/)
557
+ return match ? { pid: match[1], name: '' } : null
558
+ } else {
559
+ const pid = result.split('\n')[0].trim()
560
+ if (!pid) return null
561
+ let name = ''
562
+ try {
563
+ name = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, { encoding: 'utf-8' }).trim()
564
+ } catch (e) {
565
+ /* ignore */
566
+ }
567
+ return { pid, name: name || 'unknown' }
568
+ }
569
+ } catch (e) {
570
+ return null
310
571
  }
311
- return null
312
572
  }
313
573
 
314
- function checkOpenSSLVersion(version) {
315
- if (!version) return false
316
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
317
- if (!match) return false
318
- return parseInt(match[1], 10) >= 3
574
+ /**
575
+ * 提示用户是否杀掉占用端口的进程
576
+ */
577
+ function promptKillPort(port, processInfo) {
578
+ return new Promise((resolve) => {
579
+ const readline = require('readline')
580
+ const rl = readline.createInterface({
581
+ input: process.stdin,
582
+ output: process.stdout,
583
+ })
584
+
585
+ const timeout = setTimeout(() => {
586
+ rl.close()
587
+ resolve(false)
588
+ }, 30000)
589
+
590
+ rl.question(` 是否杀掉该进程并继续? [y/N] `, (answer) => {
591
+ clearTimeout(timeout)
592
+ rl.close()
593
+ const confirmed = (answer || '').trim().toLowerCase() === 'y'
594
+
595
+ if (confirmed) {
596
+ if (!processInfo || !processInfo.pid) {
597
+ signale.error('无法获取占用端口的进程 PID,请手动查找并关闭')
598
+ resolve(false)
599
+ return
600
+ }
601
+ try {
602
+ const { execSync } = require('child_process')
603
+ const isWindows = process.platform === 'win32'
604
+ if (isWindows) {
605
+ execSync(`taskkill /PID ${processInfo.pid} /F`, { encoding: 'utf-8' })
606
+ } else {
607
+ execSync(`kill -9 ${processInfo.pid}`, { encoding: 'utf-8' })
608
+ }
609
+ signale.success(`已杀掉进程 ${processInfo.pid}`)
610
+ setTimeout(() => resolve(true), 500)
611
+ } catch (e) {
612
+ signale.error(`杀掉进程失败: ${e.message}`)
613
+ resolve(false)
614
+ }
615
+ } else {
616
+ resolve(false)
617
+ }
618
+ })
619
+ })
319
620
  }
320
621
 
622
+ // ============================================================================
623
+ // 启动
624
+ // ============================================================================
625
+
321
626
  main().catch((err) => {
322
627
  signale.error('启动失败:', err)
323
628
  process.exit(1)