@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/LICENSE +21 -0
- package/README.md +103 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +86 -0
- package/lib/backend-package.js +664 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands/core.js +233 -0
- package/lib/cli/commands/db.js +239 -0
- package/lib/cli/commands/deploy.js +76 -0
- package/lib/cli/commands/export.js +34 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/stack.js +451 -0
- package/lib/cli/commands/start.js +83 -0
- package/lib/cli/commands/worktree.js +149 -0
- package/lib/cli/dx-cli.js +864 -0
- package/lib/cli/flags.js +96 -0
- package/lib/cli/help.js +209 -0
- package/lib/cli/index.js +4 -0
- package/lib/confirm.js +213 -0
- package/lib/env.js +296 -0
- package/lib/exec.js +643 -0
- package/lib/logger.js +188 -0
- package/lib/run-with-version-env.js +173 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +134 -0
- package/lib/validate-env.js +284 -0
- package/lib/vercel-deploy.js +237 -0
- package/lib/worktree.js +1032 -0
- package/package.json +34 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PM2 开发服务栈交互式管理器
|
|
5
|
+
* 启动 backend、front、admin 三个服务,并提供交互式命令界面
|
|
6
|
+
*
|
|
7
|
+
* 使用方法:
|
|
8
|
+
* dx dev stack
|
|
9
|
+
*
|
|
10
|
+
* 可用命令:
|
|
11
|
+
* r <service> - 重启服务 (r backend / r front / r admin)
|
|
12
|
+
* l <service> - 查看日志 (l backend / l front / l admin)
|
|
13
|
+
* s <service> - 停止服务 (s backend / s front / s admin)
|
|
14
|
+
* list - 显示服务状态
|
|
15
|
+
* monit - 打开实时监控
|
|
16
|
+
* q / quit - 停止所有服务并退出
|
|
17
|
+
* help - 显示帮助信息
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { exec, spawn } from 'node:child_process'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
import readline from 'node:readline'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import { existsSync, rmSync, readdirSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { logger } from '../../logger.js'
|
|
27
|
+
|
|
28
|
+
const execPromise = promisify(exec)
|
|
29
|
+
|
|
30
|
+
class PM2StackManager {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.configPath = join(process.cwd(), 'ecosystem.config.cjs')
|
|
33
|
+
this.services = ['backend', 'front', 'admin']
|
|
34
|
+
this.isRunning = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start() {
|
|
38
|
+
// 检查配置文件
|
|
39
|
+
if (!existsSync(this.configPath)) {
|
|
40
|
+
logger.error('未找到 ecosystem.config.cjs 配置文件')
|
|
41
|
+
logger.info('请确保配置文件存在于项目根目录')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.step('启动 PM2 开发服务栈')
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// 启动前清理 PM2 状态(确保干净启动)
|
|
49
|
+
await this.preparePM2State()
|
|
50
|
+
|
|
51
|
+
// 清理前端缓存
|
|
52
|
+
this.cleanFrontendCache()
|
|
53
|
+
|
|
54
|
+
// 启动 PM2 服务
|
|
55
|
+
await this.pm2Start()
|
|
56
|
+
this.isRunning = true
|
|
57
|
+
|
|
58
|
+
// 显示初始状态
|
|
59
|
+
await this.showStatus()
|
|
60
|
+
|
|
61
|
+
// 启动交互式命令行
|
|
62
|
+
this.startInteractive()
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error('启动失败')
|
|
65
|
+
logger.error(error.message)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 准备 PM2 状态
|
|
72
|
+
* 启动前清理 PM2 状态,确保干净启动
|
|
73
|
+
*/
|
|
74
|
+
async preparePM2State() {
|
|
75
|
+
logger.info('正在准备 PM2 环境...')
|
|
76
|
+
await this.fixPM2State()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 清理 PM2 状态
|
|
81
|
+
* 停止守护进程并清理状态文件,确保干净启动
|
|
82
|
+
*/
|
|
83
|
+
async fixPM2State() {
|
|
84
|
+
try {
|
|
85
|
+
// 1. 停止 PM2 守护进程
|
|
86
|
+
logger.info('正在停止 PM2 守护进程...')
|
|
87
|
+
try {
|
|
88
|
+
// 先尝试删除所有进程
|
|
89
|
+
try {
|
|
90
|
+
await execPromise('pnpm pm2 delete all', { timeout: 5000 })
|
|
91
|
+
} catch {
|
|
92
|
+
// 忽略删除失败
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 然后停止守护进程
|
|
96
|
+
await execPromise('pnpm pm2 kill', { timeout: 5000 })
|
|
97
|
+
logger.success('PM2 守护进程已停止')
|
|
98
|
+
} catch (killError) {
|
|
99
|
+
// 如果 kill 失败(可能已经停止),继续执行清理
|
|
100
|
+
logger.info('PM2 守护进程可能已停止')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. 清理 PM2 状态文件
|
|
104
|
+
logger.info('正在清理 PM2 状态文件...')
|
|
105
|
+
const pm2Home = join(homedir(), '.pm2')
|
|
106
|
+
const stateFiles = ['dump.pm2', 'pm2.log', 'pm2.pid']
|
|
107
|
+
|
|
108
|
+
for (const file of stateFiles) {
|
|
109
|
+
const filePath = join(pm2Home, file)
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(filePath)) {
|
|
112
|
+
rmSync(filePath, { force: true })
|
|
113
|
+
logger.success(`已清理: ${file}`)
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// 忽略清理失败,继续执行
|
|
117
|
+
logger.warn(`清理 ${file} 失败: ${error.message}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. 确保日志目录存在
|
|
122
|
+
const projectRoot = process.cwd()
|
|
123
|
+
const logDir = join(projectRoot, 'logs', 'pm2')
|
|
124
|
+
if (!existsSync(logDir)) {
|
|
125
|
+
mkdirSync(logDir, { recursive: true })
|
|
126
|
+
logger.success('已创建日志目录')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
logger.success('PM2 环境准备完成')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.warn(`准备 PM2 环境时出现警告: ${error.message}`)
|
|
132
|
+
// 不抛出错误,继续尝试启动
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 清理前端缓存
|
|
138
|
+
* 清理 Next.js 用户端和 Vite 管理后台的构建缓存
|
|
139
|
+
* 注意:此方法使用同步文件系统 API,无需 async/await
|
|
140
|
+
*/
|
|
141
|
+
cleanFrontendCache() {
|
|
142
|
+
logger.info('正在清理前端缓存...')
|
|
143
|
+
|
|
144
|
+
const projectRoot = process.cwd()
|
|
145
|
+
const cachePaths = [
|
|
146
|
+
// Next.js 用户端 (apps/front)
|
|
147
|
+
{ path: join(projectRoot, 'apps/front/.next'), type: 'dir', name: 'Next.js 构建缓存' },
|
|
148
|
+
{ path: join(projectRoot, 'dist/front'), type: 'dir', name: 'Next.js 构建输出' },
|
|
149
|
+
{ path: join(projectRoot, 'apps/front/.eslintcache'), type: 'file', name: 'ESLint 缓存' },
|
|
150
|
+
// Vite 管理后台 (apps/admin-front)
|
|
151
|
+
{
|
|
152
|
+
path: join(projectRoot, 'apps/admin-front/node_modules/.vite'),
|
|
153
|
+
type: 'dir',
|
|
154
|
+
name: 'Vite 开发服务器缓存',
|
|
155
|
+
},
|
|
156
|
+
{ path: join(projectRoot, 'dist/admin-front'), type: 'dir', name: '管理后台构建输出' },
|
|
157
|
+
{
|
|
158
|
+
path: join(projectRoot, 'apps/admin-front/.eslintcache'),
|
|
159
|
+
type: 'file',
|
|
160
|
+
name: 'ESLint 缓存',
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
// 清理目录和文件
|
|
165
|
+
for (const { path: cachePath, type, name } of cachePaths) {
|
|
166
|
+
try {
|
|
167
|
+
if (existsSync(cachePath)) {
|
|
168
|
+
if (type === 'dir') {
|
|
169
|
+
rmSync(cachePath, { recursive: true, force: true })
|
|
170
|
+
logger.success(`已清理: ${name}`)
|
|
171
|
+
} else {
|
|
172
|
+
unlinkSync(cachePath)
|
|
173
|
+
logger.success(`已清理: ${name}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.warn(`清理 ${name} 失败: ${error.message}`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 清理 TypeScript 构建信息文件 (*.tsbuildinfo)
|
|
182
|
+
const tsBuildInfoPaths = [
|
|
183
|
+
join(projectRoot, 'apps/front'),
|
|
184
|
+
join(projectRoot, 'apps/admin-front'),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
for (const dirPath of tsBuildInfoPaths) {
|
|
188
|
+
try {
|
|
189
|
+
if (existsSync(dirPath)) {
|
|
190
|
+
const files = readdirSync(dirPath)
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (file.endsWith('.tsbuildinfo')) {
|
|
193
|
+
const filePath = join(dirPath, file)
|
|
194
|
+
try {
|
|
195
|
+
unlinkSync(filePath)
|
|
196
|
+
logger.success(`已清理: TypeScript 构建信息 (${file})`)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.warn(`清理 ${file} 失败: ${error.message}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// 忽略目录不存在的错误
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logger.success('前端缓存清理完成')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async pm2Start() {
|
|
212
|
+
logger.info('正在启动服务...')
|
|
213
|
+
try {
|
|
214
|
+
const { stderr } = await execPromise(`pnpm pm2 start ${this.configPath}`, { timeout: 30000 })
|
|
215
|
+
if (stderr && !stderr.includes('[PM2]')) {
|
|
216
|
+
logger.warn(stderr)
|
|
217
|
+
}
|
|
218
|
+
logger.success('服务启动成功')
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`启动失败: ${error.message || error.stderr || error.stdout || '未知错误'}`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async showStatus() {
|
|
225
|
+
try {
|
|
226
|
+
const { stdout } = await execPromise('pnpm pm2 list', { timeout: 5000 })
|
|
227
|
+
console.log(`\n${stdout}`)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error(`获取状态失败: ${error.message}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async restart(service) {
|
|
234
|
+
if (!this.services.includes(service)) {
|
|
235
|
+
logger.error(`未知服务: ${service}`)
|
|
236
|
+
logger.info(`可用服务: ${this.services.join(', ')}`)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.info(`正在重启 ${service}...`)
|
|
241
|
+
try {
|
|
242
|
+
await execPromise(`pnpm pm2 restart ${service}`)
|
|
243
|
+
logger.success(`${service} 重启成功`)
|
|
244
|
+
await this.showStatus()
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.error(`重启失败: ${error.message}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async logs(service) {
|
|
251
|
+
if (!this.services.includes(service)) {
|
|
252
|
+
logger.error(`未知服务: ${service}`)
|
|
253
|
+
logger.info(`可用服务: ${this.services.join(', ')}`)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
logger.info(`查看 ${service} 日志(按 Ctrl+C 返回)...`)
|
|
258
|
+
console.log('')
|
|
259
|
+
|
|
260
|
+
// 使用 spawn 以便实时显示日志
|
|
261
|
+
const pm2Logs = spawn('pnpm', ['pm2', 'logs', service], {
|
|
262
|
+
stdio: 'inherit',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// 等待用户按 Ctrl+C
|
|
266
|
+
await new Promise(resolve => {
|
|
267
|
+
pm2Logs.on('exit', resolve)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
console.log('')
|
|
271
|
+
this.showPrompt()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async stop(service) {
|
|
275
|
+
if (!this.services.includes(service)) {
|
|
276
|
+
logger.error(`未知服务: ${service}`)
|
|
277
|
+
logger.info(`可用服务: ${this.services.join(', ')}`)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logger.info(`正在停止 ${service}...`)
|
|
282
|
+
try {
|
|
283
|
+
await execPromise(`pnpm pm2 stop ${service}`)
|
|
284
|
+
logger.success(`${service} 已停止`)
|
|
285
|
+
await this.showStatus()
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logger.error(`停止失败: ${error.message}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async monit() {
|
|
292
|
+
logger.info('启动实时监控(按 Ctrl+C 返回)...')
|
|
293
|
+
console.log('')
|
|
294
|
+
|
|
295
|
+
const pm2Monit = spawn('pnpm', ['pm2', 'monit'], {
|
|
296
|
+
stdio: 'inherit',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
await new Promise(resolve => {
|
|
300
|
+
pm2Monit.on('exit', resolve)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
console.log('')
|
|
304
|
+
this.showPrompt()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async stopAll() {
|
|
308
|
+
logger.info('正在停止所有服务...')
|
|
309
|
+
try {
|
|
310
|
+
await execPromise('pnpm pm2 stop all')
|
|
311
|
+
await execPromise('pnpm pm2 delete all')
|
|
312
|
+
logger.success('所有服务已停止')
|
|
313
|
+
this.isRunning = false
|
|
314
|
+
} catch (error) {
|
|
315
|
+
logger.error(`停止失败: ${error.message}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
showHelp() {
|
|
320
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
321
|
+
console.log('🚀 服务访问链接:')
|
|
322
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
323
|
+
console.log(' 📦 Backend (后端 API) → http://localhost:3000')
|
|
324
|
+
console.log(' 📦 API 文档 (Swagger) → http://localhost:3000/doc')
|
|
325
|
+
console.log(' 🌐 Front (用户端) → http://localhost:3001')
|
|
326
|
+
console.log(' ⚙️ Admin (管理后台) → http://localhost:3500')
|
|
327
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
328
|
+
console.log('\n📋 可用命令:')
|
|
329
|
+
console.log(' r <service> - 重启服务 (例: r backend)')
|
|
330
|
+
console.log(' l <service> - 查看日志 (例: l backend)')
|
|
331
|
+
console.log(' s <service> - 停止服务 (例: s backend)')
|
|
332
|
+
console.log(' list - 显示服务状态')
|
|
333
|
+
console.log(' monit - 打开实时监控')
|
|
334
|
+
console.log(' q / quit - 停止所有服务并退出')
|
|
335
|
+
console.log(' help - 显示此帮助信息')
|
|
336
|
+
console.log('\n📦 可用服务: backend, front, admin\n')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
showPrompt() {
|
|
340
|
+
process.stdout.write('dx> ')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
startInteractive() {
|
|
344
|
+
// 自动显示帮助信息和访问链接
|
|
345
|
+
this.showHelp()
|
|
346
|
+
this.showPrompt()
|
|
347
|
+
|
|
348
|
+
const rl = readline.createInterface({
|
|
349
|
+
input: process.stdin,
|
|
350
|
+
output: process.stdout,
|
|
351
|
+
prompt: '',
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
rl.on('line', async line => {
|
|
355
|
+
const input = line.trim()
|
|
356
|
+
if (!input) {
|
|
357
|
+
this.showPrompt()
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const [cmd, ...args] = input.split(/\s+/)
|
|
362
|
+
|
|
363
|
+
switch (cmd.toLowerCase()) {
|
|
364
|
+
case 'r':
|
|
365
|
+
case 'restart':
|
|
366
|
+
if (args[0]) {
|
|
367
|
+
await this.restart(args[0])
|
|
368
|
+
} else {
|
|
369
|
+
logger.error('请指定服务名称,例如: r backend')
|
|
370
|
+
}
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
case 'l':
|
|
374
|
+
case 'logs':
|
|
375
|
+
if (args[0]) {
|
|
376
|
+
await this.logs(args[0])
|
|
377
|
+
} else {
|
|
378
|
+
logger.error('请指定服务名称,例如: l backend')
|
|
379
|
+
}
|
|
380
|
+
return // logs 命令会自己处理 prompt
|
|
381
|
+
|
|
382
|
+
case 's':
|
|
383
|
+
case 'stop':
|
|
384
|
+
if (args[0]) {
|
|
385
|
+
await this.stop(args[0])
|
|
386
|
+
} else {
|
|
387
|
+
logger.error('请指定服务名称,例如: s backend')
|
|
388
|
+
}
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
case 'list':
|
|
392
|
+
case 'ls':
|
|
393
|
+
await this.showStatus()
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
case 'monit':
|
|
397
|
+
case 'monitor':
|
|
398
|
+
await this.monit()
|
|
399
|
+
return // monit 命令会自己处理 prompt
|
|
400
|
+
|
|
401
|
+
case 'q':
|
|
402
|
+
case 'quit':
|
|
403
|
+
case 'exit':
|
|
404
|
+
await this.stopAll()
|
|
405
|
+
rl.close()
|
|
406
|
+
process.exit(0)
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
case 'help':
|
|
410
|
+
case 'h':
|
|
411
|
+
case '?':
|
|
412
|
+
this.showHelp()
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
default:
|
|
416
|
+
logger.warn(`未知命令: ${cmd}`)
|
|
417
|
+
logger.info('输入 help 查看可用命令')
|
|
418
|
+
break
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.showPrompt()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
rl.on('close', async () => {
|
|
425
|
+
if (this.isRunning) {
|
|
426
|
+
console.log('\n')
|
|
427
|
+
await this.stopAll()
|
|
428
|
+
}
|
|
429
|
+
process.exit(0)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// 处理 Ctrl+C
|
|
433
|
+
process.on('SIGINT', async () => {
|
|
434
|
+
console.log('\n')
|
|
435
|
+
await this.stopAll()
|
|
436
|
+
process.exit(0)
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 主函数
|
|
442
|
+
async function main() {
|
|
443
|
+
const manager = new PM2StackManager()
|
|
444
|
+
await manager.start()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
main().catch(error => {
|
|
448
|
+
logger.error('启动失败')
|
|
449
|
+
logger.error(error.message)
|
|
450
|
+
process.exit(1)
|
|
451
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
|
|
3
|
+
export async function handleStart(cli, args) {
|
|
4
|
+
const service = args[0] || 'dev'
|
|
5
|
+
|
|
6
|
+
// 处理 stack 子命令 - PM2 交互式管理
|
|
7
|
+
if (service === 'stack') {
|
|
8
|
+
await import('./stack.js')
|
|
9
|
+
return // stack.js 会接管执行流程
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const environment = cli.determineEnvironment()
|
|
13
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
14
|
+
|
|
15
|
+
let rawConfig = cli.commands.start[service]
|
|
16
|
+
let configNamespace = 'start'
|
|
17
|
+
|
|
18
|
+
if (!rawConfig && cli.commands.dev?.[service]) {
|
|
19
|
+
if (envKey !== 'dev') {
|
|
20
|
+
logger.error(`目标 ${service} 仅支持开发环境启动,请使用 --dev 或省略环境标志。`)
|
|
21
|
+
process.exitCode = 1
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
rawConfig = cli.commands.dev[service]
|
|
25
|
+
configNamespace = 'dev'
|
|
26
|
+
logger.info(`检测到 legacy "dev" 配置,已自动回退至 ${service} 开发脚本。`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!rawConfig) {
|
|
30
|
+
logger.error(`未找到启动配置: ${service}`)
|
|
31
|
+
process.exitCode = 1
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let startConfig = rawConfig
|
|
36
|
+
if (configNamespace === 'start' && rawConfig && typeof rawConfig === 'object') {
|
|
37
|
+
if (rawConfig[envKey]) startConfig = rawConfig[envKey]
|
|
38
|
+
else if (envKey === 'staging' && rawConfig?.prod) startConfig = rawConfig.prod
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!startConfig) {
|
|
42
|
+
logger.error(`启动目标 ${service} 未提供 ${environment} 环境配置。`)
|
|
43
|
+
process.exitCode = 1
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logger.step(`启动 ${service} 服务 (${environment})`)
|
|
48
|
+
|
|
49
|
+
if (startConfig.concurrent && Array.isArray(startConfig.commands)) {
|
|
50
|
+
await cli.handleConcurrentCommands(startConfig.commands, configNamespace, envKey)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (startConfig.sequential && Array.isArray(startConfig.commands)) {
|
|
55
|
+
await cli.handleSequentialCommands(startConfig.commands, envKey)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ports = cli.collectStartPorts(service, startConfig, envKey)
|
|
60
|
+
|
|
61
|
+
if (envKey === 'dev' && ports.length > 0) {
|
|
62
|
+
logger.info(`开发环境自动清理端口: ${ports.join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const configToExecute = {
|
|
66
|
+
...startConfig,
|
|
67
|
+
...(ports.length > 0 ? { ports } : {}),
|
|
68
|
+
...(envKey === 'dev' ? { forcePortCleanup: true } : {}),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 为执行阶段构造环境标志,确保 dotenv 选择正确层
|
|
72
|
+
const execFlags = { ...cli.flags }
|
|
73
|
+
;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
|
|
74
|
+
key => delete execFlags[key]
|
|
75
|
+
)
|
|
76
|
+
if (envKey === 'prod') execFlags.prod = true
|
|
77
|
+
else if (envKey === 'dev') execFlags.dev = true
|
|
78
|
+
else if (envKey === 'test') execFlags.test = true
|
|
79
|
+
else if (envKey === 'e2e') execFlags.e2e = true
|
|
80
|
+
else if (envKey === 'staging') execFlags.staging = true
|
|
81
|
+
|
|
82
|
+
await cli.executeCommand(configToExecute, execFlags)
|
|
83
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import { confirmManager } from '../../confirm.js'
|
|
3
|
+
|
|
4
|
+
export async function handleWorktree(cli, args) {
|
|
5
|
+
const worktreeManager = await cli.getWorktreeManager()
|
|
6
|
+
logger.warn('注意:该封装与原生 git worktree 行为不同,勿混用')
|
|
7
|
+
const action = args[0]
|
|
8
|
+
const issueNumber = args[1]
|
|
9
|
+
// 解析可选的基础分支(位置参数或 --base/-b 标志)
|
|
10
|
+
let baseBranch = null
|
|
11
|
+
// 位置参数作为第3个无标志参数传入
|
|
12
|
+
if (args[2] && !String(args[2]).startsWith('-')) {
|
|
13
|
+
baseBranch = args[2]
|
|
14
|
+
}
|
|
15
|
+
// 支持 --base/-b 标志(从原始参数中解析,包含所有标志)
|
|
16
|
+
const allArgs = cli.args
|
|
17
|
+
const baseIdx = allArgs.indexOf('--base')
|
|
18
|
+
const shortBaseIdx = allArgs.indexOf('-b')
|
|
19
|
+
if (!baseBranch) {
|
|
20
|
+
if (baseIdx !== -1 && baseIdx + 1 < allArgs.length) baseBranch = allArgs[baseIdx + 1]
|
|
21
|
+
else if (shortBaseIdx !== -1 && shortBaseIdx + 1 < allArgs.length) baseBranch = allArgs[shortBaseIdx + 1]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!action) {
|
|
25
|
+
logger.error('请指定 worktree 操作: make, del, list, clean')
|
|
26
|
+
logger.info('用法:')
|
|
27
|
+
logger.info(' dx worktree make <issue_number> [base] - 创建新的 worktree(可选基础分支)')
|
|
28
|
+
logger.info(' dx worktree del <issue_number> [issue_number2] ... - 删除指定 worktree(支持批量)')
|
|
29
|
+
logger.info(' dx worktree del --all - 删除所有 issue 相关 worktree')
|
|
30
|
+
logger.info(' dx worktree list - 列出所有 worktree')
|
|
31
|
+
logger.info(' dx worktree clean - 清理无效的 worktree')
|
|
32
|
+
logger.info('')
|
|
33
|
+
logger.info('选项:')
|
|
34
|
+
logger.info(' --base <branch>, -b <branch> - 指定基础分支(make 命令专用)')
|
|
35
|
+
logger.info(' --all - 删除所有 worktree(del 命令专用)')
|
|
36
|
+
logger.info(' -Y, --yes - 跳过所有确认提示(非交互式)')
|
|
37
|
+
logger.info('')
|
|
38
|
+
logger.info('示例:')
|
|
39
|
+
logger.info(' dx worktree make 88 - 从 main 分支创建 worktree')
|
|
40
|
+
logger.info(' dx worktree make 88 dev - 从 dev 分支创建 worktree')
|
|
41
|
+
logger.info(' dx worktree make 88 --base dev - 使用标志指定基础分支')
|
|
42
|
+
logger.info(' dx worktree del 88 - 删除单个 worktree')
|
|
43
|
+
logger.info(' dx worktree del 88 89 90 - 批量删除多个 worktree')
|
|
44
|
+
logger.info(' dx worktree del --all - 删除所有 worktree(需确认)')
|
|
45
|
+
logger.info(' dx worktree del --all -Y - 删除所有(跳过确认)')
|
|
46
|
+
logger.warn('注意:该封装与原生 git worktree 行为不同,勿混用')
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (action) {
|
|
52
|
+
case 'make':
|
|
53
|
+
if (!issueNumber) {
|
|
54
|
+
logger.error('请指定 issue 编号')
|
|
55
|
+
logger.info('用法: dx worktree make <issue_number> [base] 或 dx worktree make <issue_number> --base <branch>')
|
|
56
|
+
logger.info('示例: dx worktree make 88 dev 或 dx worktree make 88 --base dev')
|
|
57
|
+
process.exitCode = 1
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
await worktreeManager.make(issueNumber, {
|
|
61
|
+
force: Boolean(cli.flags.Y),
|
|
62
|
+
baseBranch,
|
|
63
|
+
})
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
case 'del':
|
|
67
|
+
case 'delete':
|
|
68
|
+
case 'rm':
|
|
69
|
+
// 互斥校验:--all 不能与 issue 编号同时使用
|
|
70
|
+
// args[0] 是 action,args[1] 开始才是 issue 编号
|
|
71
|
+
if (cli.flags.all && args.length > 1) {
|
|
72
|
+
logger.error('--all 标志不能与 issue 编号同时使用')
|
|
73
|
+
logger.info('用法: dx worktree del --all 或 dx worktree del <issue_number> ...')
|
|
74
|
+
process.exitCode = 1
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 批量删除所有 worktree
|
|
79
|
+
if (cli.flags.all) {
|
|
80
|
+
const allIssues = await worktreeManager.getAllIssueWorktrees()
|
|
81
|
+
|
|
82
|
+
if (allIssues.length === 0) {
|
|
83
|
+
logger.info('没有找到 issue 相关的 worktree')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.info(`\n找到 ${allIssues.length} 个 issue worktree:`)
|
|
88
|
+
allIssues.forEach(issue => {
|
|
89
|
+
logger.info(` - issue-${issue}`)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// 安全确认(除非 -Y)
|
|
93
|
+
if (!cli.flags.Y) {
|
|
94
|
+
const confirmed = await confirmManager.confirm(
|
|
95
|
+
`\n确定要删除所有 ${allIssues.length} 个 worktree 吗?(这将永久删除工作目录)`,
|
|
96
|
+
false,
|
|
97
|
+
false,
|
|
98
|
+
)
|
|
99
|
+
if (!confirmed) {
|
|
100
|
+
logger.info('操作已取消')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await worktreeManager.del(allIssues, { force: Boolean(cli.flags.Y) })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 删除指定 issue 编号(原逻辑保持不变)
|
|
110
|
+
if (!issueNumber) {
|
|
111
|
+
logger.error('请指定一个或多个 issue 编号,或使用 --all 删除所有')
|
|
112
|
+
logger.info('用法: dx worktree del <issue_number> [issue_number2] ...')
|
|
113
|
+
logger.info(' dx worktree del --all # 删除所有 issue 相关 worktree')
|
|
114
|
+
logger.info('示例: dx worktree del 123 456 789 # 批量删除指定 worktree')
|
|
115
|
+
logger.info(' dx worktree del --all -Y # 删除所有(跳过确认)')
|
|
116
|
+
logger.info('选项: -Y, --yes # 跳过所有确认提示')
|
|
117
|
+
process.exitCode = 1
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 收集所有 issue 编号(从第二个参数开始的所有非标志参数)
|
|
122
|
+
const issueNumbers = [issueNumber]
|
|
123
|
+
for (let i = 2; i < args.length; i++) {
|
|
124
|
+
const arg = args[i]
|
|
125
|
+
if (arg && !arg.startsWith('-')) {
|
|
126
|
+
issueNumbers.push(arg)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await worktreeManager.del(issueNumbers, { force: Boolean(cli.flags.Y) })
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
case 'list':
|
|
134
|
+
case 'ls':
|
|
135
|
+
await worktreeManager.list()
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
case 'clean':
|
|
139
|
+
case 'prune':
|
|
140
|
+
await worktreeManager.clean()
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
logger.error(`未知的 worktree 操作: ${action}`)
|
|
145
|
+
logger.info('可用操作: make, del, list, clean')
|
|
146
|
+
logger.info('使用 dx worktree --help 查看详细用法')
|
|
147
|
+
logger.warn('注意:该封装与原生 git worktree 行为不同,勿混用')
|
|
148
|
+
}
|
|
149
|
+
}
|