@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.
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 开发环境启动模块
5
+ * 集成原 start-dev.sh 的功能到 Node.js
6
+ * 支持智能端口检测、进程管理和服务启动
7
+ */
8
+
9
+ import { spawn, exec as nodeExec } from 'node:child_process'
10
+ import { promisify } from 'node:util'
11
+ import { logger } from './logger.js'
12
+ import { execManager } from './exec.js'
13
+ // removed unused imports: confirmManager, envManager
14
+
15
+ const execPromise = promisify(nodeExec)
16
+
17
+ class DevStarter {
18
+ constructor() {
19
+ this.services = [
20
+ {
21
+ name: 'backend',
22
+ command: 'pnpm exec nx dev backend',
23
+ port: 3000,
24
+ healthCheck: 'http://localhost:3000/health',
25
+ description: '后端服务',
26
+ },
27
+ {
28
+ name: 'admin-front',
29
+ command: 'pnpm exec nx dev admin-front',
30
+ port: 3500,
31
+ description: '管理后台前端',
32
+ },
33
+ {
34
+ name: 'front',
35
+ command: 'pnpm exec nx dev front',
36
+ port: 3001,
37
+ description: '用户前端',
38
+ dependsOn: ['backend'],
39
+ },
40
+ ]
41
+
42
+ this.runningProcesses = new Map()
43
+ this.setupSignalHandlers()
44
+ }
45
+
46
+ // 设置信号处理
47
+ setupSignalHandlers() {
48
+ process.on('SIGINT', () => this.cleanup())
49
+ process.on('SIGTERM', () => this.cleanup())
50
+ process.on('exit', () => this.cleanup())
51
+ }
52
+
53
+ // 启动开发环境
54
+ async start(options = {}) {
55
+ try {
56
+ logger.step('开发环境启动脚本')
57
+
58
+ // 检查必要命令
59
+ await this.checkCommands()
60
+
61
+ // 检查项目根目录
62
+ await this.checkProjectRoot()
63
+
64
+ // 步骤 1: 检测并清理端口占用
65
+ await this.cleanupPorts()
66
+
67
+ // 步骤 2: 启动后端服务
68
+ await this.startService('backend')
69
+
70
+ // 步骤 3: 启动管理后台前端
71
+ await this.startService('admin-front')
72
+
73
+ // 步骤 4: 等待后端服务启动完成
74
+ await this.waitForBackend()
75
+
76
+ // 步骤 5: 启动用户前端
77
+ await this.startService('front')
78
+
79
+ // 显示服务信息
80
+ await this.showServiceInfo()
81
+
82
+ // 保持进程运行
83
+ await this.keepAlive()
84
+ } catch (error) {
85
+ logger.error('开发环境启动失败')
86
+ logger.error(error.message)
87
+ await this.cleanup()
88
+ throw error
89
+ }
90
+ }
91
+
92
+ // 检查必要命令
93
+ async checkCommands() {
94
+ const commands = ['pnpm', 'lsof', 'curl']
95
+
96
+ for (const cmd of commands) {
97
+ try {
98
+ await execPromise(`command -v ${cmd}`)
99
+ } catch (error) {
100
+ throw new Error(`错误: ${cmd} 命令未找到,请确保已安装`)
101
+ }
102
+ }
103
+
104
+ logger.success('必要命令检查通过')
105
+ }
106
+
107
+ // 检查项目根目录
108
+ async checkProjectRoot() {
109
+ try {
110
+ await execPromise('test -f package.json')
111
+ logger.success('项目根目录检查通过')
112
+ } catch (error) {
113
+ throw new Error('错误: 请在项目根目录运行此脚本')
114
+ }
115
+ }
116
+
117
+ // 清理端口占用
118
+ async cleanupPorts() {
119
+ logger.step('检测并清理端口占用')
120
+
121
+ const ports = this.services.map(s => s.port)
122
+ await execManager.handlePortConflicts(ports, true)
123
+
124
+ logger.success('端口清理完成')
125
+ }
126
+
127
+ // 启动单个服务
128
+ async startService(serviceName) {
129
+ const service = this.services.find(s => s.name === serviceName)
130
+ if (!service) {
131
+ throw new Error(`未找到服务配置: ${serviceName}`)
132
+ }
133
+
134
+ logger.step(`启动${service.description}`)
135
+
136
+ // 检查依赖服务
137
+ if (service.dependsOn) {
138
+ for (const dep of service.dependsOn) {
139
+ if (!this.runningProcesses.has(dep)) {
140
+ throw new Error(`服务 ${serviceName} 依赖的服务 ${dep} 未启动`)
141
+ }
142
+ }
143
+ }
144
+
145
+ // 在新终端窗口启动服务
146
+ if (this.shouldUseNewTerminal()) {
147
+ await this.startInNewTerminal(service)
148
+ } else {
149
+ // 后台启动
150
+ await this.startInBackground(service)
151
+ }
152
+
153
+ logger.success(`${service.description}启动命令已发送`)
154
+ }
155
+
156
+ // 判断是否应该使用新终端
157
+ shouldUseNewTerminal() {
158
+ // 如果是 CI 环境或者没有图形界面,使用后台模式
159
+ return !process.env.CI && !process.env.GITHUB_ACTIONS && process.env.DISPLAY !== undefined
160
+ }
161
+
162
+ // 在新终端窗口启动
163
+ async startInNewTerminal(service) {
164
+ const title = service.description
165
+ const command = service.command
166
+ const currentDir = process.cwd()
167
+
168
+ if (process.platform === 'darwin') {
169
+ // macOS
170
+ const script = `cd "${currentDir}" && echo "${title}" && ${command}`
171
+ spawn('osascript', ['-e', `tell app "Terminal" to do script "${script}"`], { detached: true })
172
+ } else if (process.platform === 'linux') {
173
+ // Linux
174
+ try {
175
+ // 尝试使用 gnome-terminal
176
+ spawn(
177
+ 'gnome-terminal',
178
+ [
179
+ '--title',
180
+ title,
181
+ '--',
182
+ 'bash',
183
+ '-c',
184
+ `cd "${currentDir}" && echo "${title}" && ${command}; exec bash`,
185
+ ],
186
+ { detached: true },
187
+ )
188
+ } catch (error) {
189
+ try {
190
+ // 尝试使用 xterm
191
+ spawn(
192
+ 'xterm',
193
+ [
194
+ '-title',
195
+ title,
196
+ '-e',
197
+ `bash -c "cd '${currentDir}' && echo '${title}' && ${command}; exec bash"`,
198
+ ],
199
+ { detached: true },
200
+ )
201
+ } catch (error2) {
202
+ logger.warn(`未找到合适的终端,使用后台运行: ${command}`)
203
+ await this.startInBackground(service)
204
+ }
205
+ }
206
+ } else {
207
+ logger.warn(`不支持的操作系统,使用后台运行: ${command}`)
208
+ await this.startInBackground(service)
209
+ }
210
+
211
+ // 记录服务为已启动
212
+ this.runningProcesses.set(service.name, {
213
+ service,
214
+ startTime: Date.now(),
215
+ method: 'terminal',
216
+ })
217
+ }
218
+
219
+ // 在后台启动
220
+ async startInBackground(service) {
221
+ const childProcess = spawn('bash', ['-c', service.command], {
222
+ stdio: ['ignore', 'pipe', 'pipe'],
223
+ detached: true,
224
+ })
225
+
226
+ // 记录进程
227
+ this.runningProcesses.set(service.name, {
228
+ service,
229
+ process: childProcess,
230
+ startTime: Date.now(),
231
+ method: 'background',
232
+ })
233
+
234
+ // 处理进程输出
235
+ if (childProcess.stdout) {
236
+ childProcess.stdout.on('data', data => {
237
+ console.log(`[${service.name}] ${data.toString()}`)
238
+ })
239
+ }
240
+
241
+ if (childProcess.stderr) {
242
+ childProcess.stderr.on('data', data => {
243
+ console.error(`[${service.name}] ${data.toString()}`)
244
+ })
245
+ }
246
+
247
+ childProcess.on('exit', code => {
248
+ if (code !== 0) {
249
+ logger.error(`${service.description} 异常退出,退出码: ${code}`)
250
+ }
251
+ this.runningProcesses.delete(service.name)
252
+ })
253
+
254
+ logger.info(`${service.description} 在后台启动,PID: ${childProcess.pid}`)
255
+ }
256
+
257
+ // 等待后端服务启动
258
+ async waitForBackend() {
259
+ logger.step('等待后端服务启动完成')
260
+
261
+ const backend = this.services.find(s => s.name === 'backend')
262
+ if (!backend || !backend.healthCheck) {
263
+ logger.warn('后端服务未配置健康检查,跳过等待')
264
+ return
265
+ }
266
+
267
+ const maxAttempts = 60 // 60次尝试,每次2秒 = 120秒
268
+ let attempt = 0
269
+
270
+ logger.progress('检查后端服务')
271
+
272
+ while (attempt < maxAttempts) {
273
+ try {
274
+ await execPromise(`curl -s ${backend.healthCheck}`)
275
+ logger.progressDone()
276
+ logger.success('后端服务已启动成功!')
277
+ return true
278
+ } catch (error) {
279
+ attempt++
280
+ if (attempt < maxAttempts) {
281
+ await new Promise(resolve => setTimeout(resolve, 2000))
282
+ process.stdout.write('.')
283
+ }
284
+ }
285
+ }
286
+
287
+ logger.progressDone()
288
+ throw new Error('后端服务启动超时,请检查日志')
289
+ }
290
+
291
+ // 显示服务信息
292
+ async showServiceInfo() {
293
+ logger.step('所有服务启动完成')
294
+
295
+ const serviceInfo = [
296
+ { service: '后端服务', port: 3000, url: 'http://localhost:3000' },
297
+ { service: '管理后台', port: 3500, url: 'http://localhost:3500' },
298
+ { service: '用户前端', port: 3001, url: 'http://localhost:3001' },
299
+ ]
300
+
301
+ logger.ports(serviceInfo)
302
+
303
+ logger.info('注意事项:')
304
+ logger.info('- front 固定 3001,admin-front 固定 3500')
305
+ logger.info('- 若端口被占用,脚本会自动清理后再启动')
306
+ logger.info('- 按 Ctrl+C 可以停止所有服务')
307
+ }
308
+
309
+ // 保持进程运行
310
+ async keepAlive() {
311
+ logger.info('\n开发环境已启动,按 Ctrl+C 停止所有服务...\n')
312
+
313
+ // 监控后台进程
314
+ setInterval(() => {
315
+ this.checkBackgroundProcesses()
316
+ }, 10000) // 每10秒检查一次
317
+
318
+ // 保持主进程运行
319
+ return new Promise(() => {
320
+ // 这个 Promise 永不 resolve,保持进程运行
321
+ })
322
+ }
323
+
324
+ // 检查后台进程状态
325
+ checkBackgroundProcesses() {
326
+ for (const [name, info] of this.runningProcesses) {
327
+ if (info.method === 'background' && info.process) {
328
+ if (info.process.killed) {
329
+ logger.warn(`检测到 ${name} 服务进程已停止`)
330
+ this.runningProcesses.delete(name)
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ // 获取运行状态
337
+ getStatus() {
338
+ const status = {
339
+ running: this.runningProcesses.size,
340
+ services: [],
341
+ }
342
+
343
+ for (const [name, info] of this.runningProcesses) {
344
+ status.services.push({
345
+ name,
346
+ description: info.service.description,
347
+ port: info.service.port,
348
+ method: info.method,
349
+ uptime: Math.round((Date.now() - info.startTime) / 1000),
350
+ pid: info.process ? info.process.pid : 'N/A',
351
+ })
352
+ }
353
+
354
+ return status
355
+ }
356
+
357
+ // 清理资源
358
+ async cleanup() {
359
+ if (this.runningProcesses.size === 0) return
360
+
361
+ logger.info('正在清理开发服务...')
362
+
363
+ for (const [name, info] of this.runningProcesses) {
364
+ if (info.method === 'background' && info.process) {
365
+ try {
366
+ logger.info(`停止 ${name} 服务 (PID: ${info.process.pid})`)
367
+ info.process.kill('SIGTERM')
368
+
369
+ // 如果 5 秒后还没停止,强制杀死
370
+ setTimeout(() => {
371
+ if (!info.process.killed) {
372
+ info.process.kill('SIGKILL')
373
+ }
374
+ }, 5000)
375
+ } catch (error) {
376
+ logger.debug(`清理 ${name} 时出错: ${error.message}`)
377
+ }
378
+ }
379
+ }
380
+
381
+ this.runningProcesses.clear()
382
+ logger.info('清理完成')
383
+ }
384
+ }
385
+
386
+ export async function runStartDev(argv = []) {
387
+ void argv
388
+ const starter = new DevStarter()
389
+ await starter.start()
390
+ }
391
+
392
+ // 如果直接执行此脚本
393
+ if (import.meta.url === `file://${process.argv[1]}`) {
394
+ runStartDev(process.argv.slice(2)).catch(error => {
395
+ logger.error('开发环境启动失败')
396
+ console.error(error)
397
+ process.exit(1)
398
+ })
399
+ }
400
+
401
+ export { DevStarter }
@@ -0,0 +1,134 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { logger } from './logger.js'
3
+ import { envManager } from './env.js'
4
+
5
+ /**
6
+ * 处理 Telegram Bot 部署后的 Webhook 配置
7
+ */
8
+ export async function handleTelegramBotDeploy(environment, projectId, orgId, token) {
9
+ logger.step('配置 Telegram Webhook...')
10
+
11
+ // 1. 验证必需环境变量
12
+ const botToken = process.env.TELEGRAM_BOT_TOKEN
13
+ const webhookSecret = process.env.TELEGRAM_BOT_WEBHOOK_SECRET
14
+
15
+ const missingVars = []
16
+ if (!botToken || envManager.isPlaceholderEnvValue(botToken)) {
17
+ missingVars.push('TELEGRAM_BOT_TOKEN')
18
+ }
19
+ if (!webhookSecret || envManager.isPlaceholderEnvValue(webhookSecret)) {
20
+ missingVars.push('TELEGRAM_BOT_WEBHOOK_SECRET')
21
+ }
22
+
23
+ if (missingVars.length > 0) {
24
+ logger.error('缺少以下 Telegram Bot 环境变量:')
25
+ missingVars.forEach(v => logger.error(` - ${v}`))
26
+ logger.warn('跳过 Webhook 配置,请手动设置')
27
+ return
28
+ }
29
+
30
+ try {
31
+ // 2. 获取 Vercel 部署 URL
32
+ const deploymentUrl = await getLatestDeploymentUrl(projectId, orgId, token, environment)
33
+ if (!deploymentUrl) {
34
+ logger.error('无法获取 Vercel 部署 URL,跳过 Webhook 配置')
35
+ return
36
+ }
37
+
38
+ const webhookUrl = `${deploymentUrl}/api/webhook`
39
+ logger.info(`Webhook URL: ${webhookUrl}`)
40
+
41
+ // 3. 调用 Telegram API 设置 Webhook
42
+ const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
43
+ const payload = JSON.stringify({
44
+ url: webhookUrl,
45
+ secret_token: webhookSecret,
46
+ drop_pending_updates: false,
47
+ })
48
+
49
+ const curlCmd = [
50
+ 'curl',
51
+ '-X POST',
52
+ `"${telegramApiUrl}"`,
53
+ '-H "Content-Type: application/json"',
54
+ `-d '${payload}'`,
55
+ '--silent',
56
+ ].join(' ')
57
+
58
+ const response = execSync(curlCmd, { encoding: 'utf8' })
59
+ const result = JSON.parse(response)
60
+
61
+ if (result.ok) {
62
+ logger.success('Telegram Webhook 设置成功')
63
+ logger.info(`Webhook URL: ${webhookUrl}`)
64
+
65
+ // 4. 验证 Webhook 状态
66
+ await verifyWebhook(botToken)
67
+ }
68
+ else {
69
+ logger.error(`Telegram Webhook 设置失败: ${result.description}`)
70
+ logger.info('请手动执行以下命令:')
71
+ logger.info(curlCmd)
72
+ }
73
+ }
74
+ catch (error) {
75
+ logger.error(`Webhook 配置失败: ${error.message}`)
76
+ logger.warn('请手动设置 Webhook(参考 apps/telegram-bot/README.md)')
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 获取最新部署的 URL
82
+ */
83
+ async function getLatestDeploymentUrl(projectId, orgId, token, environment) {
84
+ try {
85
+ const cmd = [
86
+ 'vercel',
87
+ 'ls',
88
+ `--token=${token}`,
89
+ orgId ? `--scope=${orgId}` : '',
90
+ '--json',
91
+ ].filter(Boolean).join(' ')
92
+
93
+ const output = execSync(cmd, { encoding: 'utf8' })
94
+ const deployments = JSON.parse(output)
95
+
96
+ // 根据环境筛选部署
97
+ const targetEnv = environment === 'production' ? 'production' : 'preview'
98
+ const latest = deployments.find(d =>
99
+ d.projectId === projectId
100
+ && d.target === targetEnv
101
+ && d.state === 'READY',
102
+ )
103
+
104
+ return latest ? `https://${latest.url}` : null
105
+ }
106
+ catch (error) {
107
+ logger.warn(`获取部署 URL 失败: ${error.message}`)
108
+ return null
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 验证 Webhook 配置
114
+ */
115
+ async function verifyWebhook(botToken) {
116
+ try {
117
+ const cmd = `curl -s "https://api.telegram.org/bot${botToken}/getWebhookInfo"`
118
+ const response = execSync(cmd, { encoding: 'utf8' })
119
+ const result = JSON.parse(response)
120
+
121
+ if (result.ok && result.result) {
122
+ const info = result.result
123
+ logger.info('Webhook 状态:')
124
+ logger.info(` URL: ${info.url}`)
125
+ logger.info(` Pending Updates: ${info.pending_update_count}`)
126
+ if (info.last_error_message) {
127
+ logger.warn(` 最后错误: ${info.last_error_message}`)
128
+ }
129
+ }
130
+ }
131
+ catch (error) {
132
+ logger.warn('无法验证 Webhook 状态')
133
+ }
134
+ }