@weotro/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 +755 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +187 -0
- package/lib/artifact-deploy/artifact-builder.js +144 -0
- package/lib/artifact-deploy/config.js +180 -0
- package/lib/artifact-deploy/remote-script.js +301 -0
- package/lib/artifact-deploy/remote-transport.js +86 -0
- package/lib/artifact-deploy.js +70 -0
- package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
- package/lib/backend-artifact-deploy/config.js +218 -0
- package/lib/backend-artifact-deploy/path-utils.js +18 -0
- package/lib/backend-artifact-deploy/remote-phases.js +14 -0
- package/lib/backend-artifact-deploy/remote-result.js +44 -0
- package/lib/backend-artifact-deploy/remote-script.js +507 -0
- package/lib/backend-artifact-deploy/remote-transport.js +123 -0
- package/lib/backend-artifact-deploy/rollback.js +5 -0
- package/lib/backend-artifact-deploy/runtime-package.js +46 -0
- package/lib/backend-artifact-deploy.js +91 -0
- package/lib/backend-package.js +674 -0
- package/lib/cli/args.js +38 -0
- package/lib/cli/command-result.js +1 -0
- package/lib/cli/commands/contracts.js +60 -0
- package/lib/cli/commands/core.js +533 -0
- package/lib/cli/commands/db.js +231 -0
- package/lib/cli/commands/deploy.js +175 -0
- package/lib/cli/commands/env.js +120 -0
- package/lib/cli/commands/export.js +39 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/release.js +55 -0
- package/lib/cli/commands/stack.js +427 -0
- package/lib/cli/commands/start.js +58 -0
- package/lib/cli/commands/worktree.js +145 -0
- package/lib/cli/dx-cli.js +1072 -0
- package/lib/cli/flags.js +123 -0
- package/lib/cli/help-model.js +222 -0
- package/lib/cli/help-renderer.js +137 -0
- package/lib/cli/help-schema.js +552 -0
- package/lib/cli/help.js +141 -0
- package/lib/cli/index.js +4 -0
- package/lib/cli/nx-command.js +13 -0
- package/lib/codex-initial.js +271 -0
- package/lib/confirm.js +213 -0
- package/lib/env-policy.js +134 -0
- package/lib/env-profile.js +435 -0
- package/lib/env.js +261 -0
- package/lib/exec.js +692 -0
- package/lib/logger.js +239 -0
- package/lib/nx-ignore.js +45 -0
- package/lib/run-with-version-env.js +163 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +431 -0
- package/lib/validate-env.js +317 -0
- package/lib/vercel-deploy.js +549 -0
- package/lib/version.js +14 -0
- package/lib/worktree.js +1052 -0
- package/package.json +45 -0
- package/skills/create-issue/SKILL.md +90 -0
- package/skills/delivering-design-handoff/SKILL.md +290 -0
- package/skills/doctor/SKILL.md +76 -0
- package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
- package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
- package/skills/git-release/SKILL.md +194 -0
- package/skills/git-release/agents/openai.yaml +7 -0
- package/skills/online-debug-guard/SKILL.md +111 -0
- package/skills/ship-issue-pr/SKILL.md +676 -0
- package/skills/stagewise-ui-debugging/SKILL.md +48 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { exec, spawn } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import readline from 'node:readline'
|
|
4
|
+
import { isAbsolute, join } from 'node:path'
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { logger } from '../../logger.js'
|
|
8
|
+
import { execManager } from '../../exec.js'
|
|
9
|
+
|
|
10
|
+
const execPromise = promisify(exec)
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SERVICES = ['backend', 'front', 'admin']
|
|
13
|
+
const DEFAULT_PRE_FLIGHT = {
|
|
14
|
+
pm2Reset: true,
|
|
15
|
+
killPorts: [3000, 3001, 3500],
|
|
16
|
+
forcePortCleanup: true,
|
|
17
|
+
cleanPaths: [
|
|
18
|
+
'apps/front/.next',
|
|
19
|
+
'dist/front',
|
|
20
|
+
'apps/front/.eslintcache',
|
|
21
|
+
'apps/admin-front/node_modules/.vite',
|
|
22
|
+
'dist/admin-front',
|
|
23
|
+
'apps/admin-front/.eslintcache',
|
|
24
|
+
],
|
|
25
|
+
cleanTsBuildInfo: true,
|
|
26
|
+
cleanTsBuildInfoDirs: ['apps/front', 'apps/admin-front'],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class PM2StackManager {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.projectRoot = process.cwd()
|
|
32
|
+
this.pm2Bin = String(options.pm2Bin || 'pnpm pm2').trim()
|
|
33
|
+
|
|
34
|
+
const ecosystemConfig = options.ecosystemConfig || 'ecosystem.config.cjs'
|
|
35
|
+
this.configPath = this.resolvePath(ecosystemConfig)
|
|
36
|
+
|
|
37
|
+
this.services = Array.isArray(options.services) && options.services.length > 0
|
|
38
|
+
? options.services.map(item => String(item).trim()).filter(Boolean)
|
|
39
|
+
: [...DEFAULT_SERVICES]
|
|
40
|
+
|
|
41
|
+
this.urls = options.urls && typeof options.urls === 'object' ? options.urls : {}
|
|
42
|
+
|
|
43
|
+
const incomingPreflight = options.preflight && typeof options.preflight === 'object'
|
|
44
|
+
? options.preflight
|
|
45
|
+
: {}
|
|
46
|
+
|
|
47
|
+
this.preflight = {
|
|
48
|
+
pm2Reset: incomingPreflight.pm2Reset ?? DEFAULT_PRE_FLIGHT.pm2Reset,
|
|
49
|
+
killPorts: this.normalizePorts(incomingPreflight.killPorts, DEFAULT_PRE_FLIGHT.killPorts),
|
|
50
|
+
forcePortCleanup:
|
|
51
|
+
incomingPreflight.forcePortCleanup ?? DEFAULT_PRE_FLIGHT.forcePortCleanup,
|
|
52
|
+
cleanPaths: this.normalizeStringArray(
|
|
53
|
+
incomingPreflight.cleanPaths,
|
|
54
|
+
DEFAULT_PRE_FLIGHT.cleanPaths,
|
|
55
|
+
),
|
|
56
|
+
cleanTsBuildInfo:
|
|
57
|
+
incomingPreflight.cleanTsBuildInfo ?? DEFAULT_PRE_FLIGHT.cleanTsBuildInfo,
|
|
58
|
+
cleanTsBuildInfoDirs: this.normalizeStringArray(
|
|
59
|
+
incomingPreflight.cleanTsBuildInfoDirs,
|
|
60
|
+
DEFAULT_PRE_FLIGHT.cleanTsBuildInfoDirs,
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.isRunning = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resolvePath(targetPath) {
|
|
68
|
+
if (isAbsolute(targetPath)) return targetPath
|
|
69
|
+
return join(this.projectRoot, targetPath)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
normalizePorts(input, fallback) {
|
|
73
|
+
const source = Array.isArray(input) ? input : fallback
|
|
74
|
+
return source
|
|
75
|
+
.map(port => Number(port))
|
|
76
|
+
.filter(port => Number.isFinite(port) && port > 0)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
normalizeStringArray(input, fallback) {
|
|
80
|
+
const source = Array.isArray(input) ? input : fallback
|
|
81
|
+
return source.map(item => String(item).trim()).filter(Boolean)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async start() {
|
|
85
|
+
if (!existsSync(this.configPath)) {
|
|
86
|
+
logger.error(`未找到 PM2 配置文件: ${this.configPath}`)
|
|
87
|
+
logger.info('请在 commands.json 的 start.stack.stack.ecosystemConfig 中配置正确路径')
|
|
88
|
+
throw new Error('PM2 配置文件不存在')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
logger.step('启动 PM2 交互式服务栈')
|
|
92
|
+
|
|
93
|
+
await this.prepareBeforeStart()
|
|
94
|
+
await this.pm2Start()
|
|
95
|
+
this.isRunning = true
|
|
96
|
+
await this.showStatus()
|
|
97
|
+
this.startInteractive()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async prepareBeforeStart() {
|
|
101
|
+
logger.info('正在执行 stack 启动前检查...')
|
|
102
|
+
|
|
103
|
+
if (this.preflight.pm2Reset) {
|
|
104
|
+
await this.resetPm2State()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.preflight.killPorts.length > 0) {
|
|
108
|
+
logger.info(`正在清理端口占用: ${this.preflight.killPorts.join(', ')}`)
|
|
109
|
+
await execManager.handlePortConflicts(
|
|
110
|
+
this.preflight.killPorts,
|
|
111
|
+
Boolean(this.preflight.forcePortCleanup),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.cleanConfiguredPaths()
|
|
116
|
+
|
|
117
|
+
if (this.preflight.cleanTsBuildInfo) {
|
|
118
|
+
this.cleanTsBuildInfoFiles()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async resetPm2State() {
|
|
123
|
+
logger.info('正在重置 PM2 状态...')
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
try {
|
|
127
|
+
await this.pm2Exec('delete all', { timeout: 5000 })
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this.pm2Exec('kill', { timeout: 5000 })
|
|
133
|
+
logger.success('PM2 守护进程已停止')
|
|
134
|
+
} catch {
|
|
135
|
+
logger.info('PM2 守护进程可能已停止')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const pm2Home = join(homedir(), '.pm2')
|
|
139
|
+
const stateFiles = ['dump.pm2', 'pm2.log', 'pm2.pid']
|
|
140
|
+
|
|
141
|
+
for (const file of stateFiles) {
|
|
142
|
+
const filePath = join(pm2Home, file)
|
|
143
|
+
try {
|
|
144
|
+
if (existsSync(filePath)) {
|
|
145
|
+
rmSync(filePath, { force: true })
|
|
146
|
+
logger.success(`已清理 PM2 文件: ${file}`)
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.warn(`清理 PM2 文件失败 (${file}): ${error.message}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const logDir = join(this.projectRoot, 'logs', 'pm2')
|
|
154
|
+
if (!existsSync(logDir)) {
|
|
155
|
+
mkdirSync(logDir, { recursive: true })
|
|
156
|
+
logger.success('已创建 PM2 日志目录')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cleanConfiguredPaths() {
|
|
161
|
+
if (this.preflight.cleanPaths.length === 0) return
|
|
162
|
+
|
|
163
|
+
logger.info('正在清理缓存路径...')
|
|
164
|
+
|
|
165
|
+
for (const rawPath of this.preflight.cleanPaths) {
|
|
166
|
+
const targetPath = this.resolvePath(rawPath)
|
|
167
|
+
try {
|
|
168
|
+
if (existsSync(targetPath)) {
|
|
169
|
+
rmSync(targetPath, { recursive: true, force: true })
|
|
170
|
+
logger.success(`已清理: ${rawPath}`)
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.warn(`清理失败 (${rawPath}): ${error.message}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cleanTsBuildInfoFiles() {
|
|
179
|
+
if (this.preflight.cleanTsBuildInfoDirs.length === 0) return
|
|
180
|
+
|
|
181
|
+
logger.info('正在清理 TypeScript 构建缓存...')
|
|
182
|
+
|
|
183
|
+
for (const rawDirPath of this.preflight.cleanTsBuildInfoDirs) {
|
|
184
|
+
const dirPath = this.resolvePath(rawDirPath)
|
|
185
|
+
if (!existsSync(dirPath)) continue
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const files = readdirSync(dirPath)
|
|
189
|
+
for (const file of files) {
|
|
190
|
+
if (!file.endsWith('.tsbuildinfo')) continue
|
|
191
|
+
const filePath = join(dirPath, file)
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(filePath)
|
|
194
|
+
logger.success(`已清理: ${join(rawDirPath, file)}`)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.warn(`清理失败 (${join(rawDirPath, file)}): ${error.message}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn(`读取目录失败 (${rawDirPath}): ${error.message}`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async pm2Exec(args, options = {}) {
|
|
206
|
+
return execPromise(`${this.pm2Bin} ${args}`, options)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async pm2Start() {
|
|
210
|
+
logger.info('正在启动 PM2 服务...')
|
|
211
|
+
const { stderr } = await this.pm2Exec(`start "${this.configPath}"`, { timeout: 30000 })
|
|
212
|
+
if (stderr && !stderr.includes('[PM2]')) {
|
|
213
|
+
logger.warn(stderr)
|
|
214
|
+
}
|
|
215
|
+
logger.success('服务启动成功')
|
|
216
|
+
this.printServiceUrls()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
printServiceUrls() {
|
|
220
|
+
const entries = Object.entries(this.urls)
|
|
221
|
+
if (entries.length === 0) return
|
|
222
|
+
|
|
223
|
+
console.log('')
|
|
224
|
+
logger.info('服务访问链接:')
|
|
225
|
+
for (const [service, url] of entries) {
|
|
226
|
+
console.log(` ${service.padEnd(12)} → ${url}`)
|
|
227
|
+
}
|
|
228
|
+
console.log('')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async showStatus() {
|
|
232
|
+
try {
|
|
233
|
+
const { stdout } = await this.pm2Exec('list', { timeout: 5000 })
|
|
234
|
+
console.log(`\n${stdout}`)
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.error(`获取状态失败: ${error.message}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ensureKnownService(service) {
|
|
241
|
+
if (this.services.includes(service)) return true
|
|
242
|
+
logger.error(`未知服务: ${service}`)
|
|
243
|
+
logger.info(`可用服务: ${this.services.join(', ')}`)
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async restart(service) {
|
|
248
|
+
if (!this.ensureKnownService(service)) return
|
|
249
|
+
|
|
250
|
+
logger.info(`正在重启 ${service}...`)
|
|
251
|
+
try {
|
|
252
|
+
await this.pm2Exec(`restart ${service}`)
|
|
253
|
+
logger.success(`${service} 重启成功`)
|
|
254
|
+
await this.showStatus()
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger.error(`重启失败: ${error.message}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async logs(service) {
|
|
261
|
+
if (!this.ensureKnownService(service)) return
|
|
262
|
+
|
|
263
|
+
logger.info(`查看 ${service} 日志(按 Ctrl+C 返回)...`)
|
|
264
|
+
console.log('')
|
|
265
|
+
|
|
266
|
+
const pm2Logs = spawn('bash', ['-lc', `${this.pm2Bin} logs ${service}`], {
|
|
267
|
+
stdio: 'inherit',
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
await new Promise(resolve => {
|
|
271
|
+
pm2Logs.on('exit', resolve)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
console.log('')
|
|
275
|
+
this.showPrompt()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async stop(service) {
|
|
279
|
+
if (!this.ensureKnownService(service)) return
|
|
280
|
+
|
|
281
|
+
logger.info(`正在停止 ${service}...`)
|
|
282
|
+
try {
|
|
283
|
+
await this.pm2Exec(`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('打开 PM2 实时监控(按 q 退出)...')
|
|
293
|
+
console.log('')
|
|
294
|
+
|
|
295
|
+
const pm2Monit = spawn('bash', ['-lc', `${this.pm2Bin} 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
|
+
if (!this.isRunning) return
|
|
309
|
+
|
|
310
|
+
logger.step('正在停止所有服务...')
|
|
311
|
+
try {
|
|
312
|
+
await this.pm2Exec('stop all')
|
|
313
|
+
await this.pm2Exec('delete all')
|
|
314
|
+
logger.success('所有服务已停止')
|
|
315
|
+
this.isRunning = false
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.error(`停止服务失败: ${error.message}`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
showHelp() {
|
|
322
|
+
console.log('\n可用命令:')
|
|
323
|
+
console.log(' r <service> - 重启服务')
|
|
324
|
+
console.log(' l <service> - 查看日志')
|
|
325
|
+
console.log(' s <service> - 停止服务')
|
|
326
|
+
console.log(' list - 显示服务状态')
|
|
327
|
+
console.log(' monit - 打开实时监控')
|
|
328
|
+
console.log(' q / quit - 停止所有服务并退出')
|
|
329
|
+
console.log(' help - 显示此帮助信息')
|
|
330
|
+
console.log(`\n可用服务: ${this.services.join(', ')}\n`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
showPrompt() {
|
|
334
|
+
process.stdout.write('dx> ')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
startInteractive() {
|
|
338
|
+
this.showHelp()
|
|
339
|
+
this.showPrompt()
|
|
340
|
+
|
|
341
|
+
const rl = readline.createInterface({
|
|
342
|
+
input: process.stdin,
|
|
343
|
+
output: process.stdout,
|
|
344
|
+
prompt: '',
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
rl.on('line', async line => {
|
|
348
|
+
const input = line.trim()
|
|
349
|
+
if (!input) {
|
|
350
|
+
this.showPrompt()
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const [cmd, ...args] = input.split(/\s+/)
|
|
355
|
+
|
|
356
|
+
switch (cmd.toLowerCase()) {
|
|
357
|
+
case 'r':
|
|
358
|
+
case 'restart':
|
|
359
|
+
if (args[0]) await this.restart(args[0])
|
|
360
|
+
else logger.error('请指定服务名称,例如: r backend')
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
case 'l':
|
|
364
|
+
case 'logs':
|
|
365
|
+
if (args[0]) await this.logs(args[0])
|
|
366
|
+
else logger.error('请指定服务名称,例如: l backend')
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
case 's':
|
|
370
|
+
case 'stop':
|
|
371
|
+
if (args[0]) await this.stop(args[0])
|
|
372
|
+
else logger.error('请指定服务名称,例如: s backend')
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
case 'list':
|
|
376
|
+
case 'ls':
|
|
377
|
+
await this.showStatus()
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
case 'monit':
|
|
381
|
+
case 'monitor':
|
|
382
|
+
await this.monit()
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
case 'q':
|
|
386
|
+
case 'quit':
|
|
387
|
+
case 'exit':
|
|
388
|
+
await this.stopAll()
|
|
389
|
+
rl.close()
|
|
390
|
+
process.exit(0)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
case 'help':
|
|
394
|
+
case 'h':
|
|
395
|
+
case '?':
|
|
396
|
+
this.showHelp()
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
default:
|
|
400
|
+
logger.warn(`未知命令: ${cmd}`)
|
|
401
|
+
logger.info('输入 help 查看可用命令')
|
|
402
|
+
break
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.showPrompt()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
rl.on('close', async () => {
|
|
409
|
+
if (this.isRunning) {
|
|
410
|
+
console.log('\n')
|
|
411
|
+
await this.stopAll()
|
|
412
|
+
}
|
|
413
|
+
process.exit(0)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
process.on('SIGINT', async () => {
|
|
417
|
+
console.log('\n')
|
|
418
|
+
await this.stopAll()
|
|
419
|
+
process.exit(0)
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function runPm2Stack(options = {}) {
|
|
425
|
+
const manager = new PM2StackManager(options)
|
|
426
|
+
await manager.start()
|
|
427
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
|
|
3
|
+
export async function handleStart(cli, args) {
|
|
4
|
+
const service = args[0] || 'development'
|
|
5
|
+
|
|
6
|
+
const environment = cli.determineEnvironment()
|
|
7
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
8
|
+
const rawConfig = cli.commands.start[service]
|
|
9
|
+
|
|
10
|
+
if (!rawConfig) {
|
|
11
|
+
logger.error(`未找到启动配置: ${service}`)
|
|
12
|
+
process.exitCode = 1
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let startConfig = rawConfig
|
|
17
|
+
const isRunnableConfig =
|
|
18
|
+
rawConfig &&
|
|
19
|
+
typeof rawConfig === 'object' &&
|
|
20
|
+
(rawConfig.command || rawConfig.internal || rawConfig.concurrent || rawConfig.sequential)
|
|
21
|
+
|
|
22
|
+
if (!isRunnableConfig && rawConfig && typeof rawConfig === 'object') {
|
|
23
|
+
startConfig = rawConfig[envKey] || null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!startConfig) {
|
|
27
|
+
logger.error(`启动目标 ${service} 未提供 ${cli.getEnvironmentFlagExample(envKey) || envKey} 环境配置。`)
|
|
28
|
+
process.exitCode = 1
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.step(`启动 ${service} 服务 (${environment})`)
|
|
33
|
+
|
|
34
|
+
if (startConfig.concurrent && Array.isArray(startConfig.commands)) {
|
|
35
|
+
await cli.handleConcurrentCommands(startConfig.commands, 'start', envKey)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (startConfig.sequential && Array.isArray(startConfig.commands)) {
|
|
40
|
+
await cli.handleSequentialCommands(startConfig.commands, envKey)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ports = cli.collectStartPorts(service, startConfig, envKey)
|
|
45
|
+
|
|
46
|
+
if (envKey === 'development' && ports.length > 0) {
|
|
47
|
+
logger.info(`开发环境自动清理端口: ${ports.join(', ')}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const configToExecute = {
|
|
51
|
+
...startConfig,
|
|
52
|
+
...(ports.length > 0 ? { ports } : {}),
|
|
53
|
+
...(envKey === 'development' ? { forcePortCleanup: true } : {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 为执行阶段构造环境标志,确保 dotenv 选择正确层
|
|
57
|
+
await cli.executeCommand(configToExecute, cli.createExecutionFlags(environment))
|
|
58
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
// 互斥校验:--all 不能与 issue 编号同时使用
|
|
68
|
+
// args[0] 是 action,args[1] 开始才是 issue 编号
|
|
69
|
+
if (cli.flags.all && args.length > 1) {
|
|
70
|
+
logger.error('--all 标志不能与 issue 编号同时使用')
|
|
71
|
+
logger.info('用法: dx worktree del --all 或 dx worktree del <issue_number> ...')
|
|
72
|
+
process.exitCode = 1
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 批量删除所有 worktree
|
|
77
|
+
if (cli.flags.all) {
|
|
78
|
+
const allIssues = await worktreeManager.getAllIssueWorktrees()
|
|
79
|
+
|
|
80
|
+
if (allIssues.length === 0) {
|
|
81
|
+
logger.info('没有找到 issue 相关的 worktree')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logger.info(`\n找到 ${allIssues.length} 个 issue worktree:`)
|
|
86
|
+
allIssues.forEach(issue => {
|
|
87
|
+
logger.info(` - issue-${issue}`)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// 安全确认(除非 -Y)
|
|
91
|
+
if (!cli.flags.Y) {
|
|
92
|
+
const confirmed = await confirmManager.confirm(
|
|
93
|
+
`\n确定要删除所有 ${allIssues.length} 个 worktree 吗?(这将永久删除工作目录)`,
|
|
94
|
+
false,
|
|
95
|
+
false,
|
|
96
|
+
)
|
|
97
|
+
if (!confirmed) {
|
|
98
|
+
logger.info('操作已取消')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await worktreeManager.del(allIssues, { force: Boolean(cli.flags.Y) })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 删除指定 issue 编号(原逻辑保持不变)
|
|
108
|
+
if (!issueNumber) {
|
|
109
|
+
logger.error('请指定一个或多个 issue 编号,或使用 --all 删除所有')
|
|
110
|
+
logger.info('用法: dx worktree del <issue_number> [issue_number2] ...')
|
|
111
|
+
logger.info(' dx worktree del --all # 删除所有 issue 相关 worktree')
|
|
112
|
+
logger.info('示例: dx worktree del 123 456 789 # 批量删除指定 worktree')
|
|
113
|
+
logger.info(' dx worktree del --all -Y # 删除所有(跳过确认)')
|
|
114
|
+
logger.info('选项: -Y, --yes # 跳过所有确认提示')
|
|
115
|
+
process.exitCode = 1
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 收集所有 issue 编号(从第二个参数开始的所有非标志参数)
|
|
120
|
+
const issueNumbers = [issueNumber]
|
|
121
|
+
for (let i = 2; i < args.length; i++) {
|
|
122
|
+
const arg = args[i]
|
|
123
|
+
if (arg && !arg.startsWith('-')) {
|
|
124
|
+
issueNumbers.push(arg)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await worktreeManager.del(issueNumbers, { force: Boolean(cli.flags.Y) })
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
case 'list':
|
|
132
|
+
await worktreeManager.list()
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
case 'clean':
|
|
136
|
+
await worktreeManager.clean()
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
default:
|
|
140
|
+
logger.error(`未知的 worktree 操作: ${action}`)
|
|
141
|
+
logger.info('可用操作: make, del, list, clean')
|
|
142
|
+
logger.info('使用 dx worktree --help 查看详细用法')
|
|
143
|
+
logger.warn('注意:该封装与原生 git worktree 行为不同,勿混用')
|
|
144
|
+
}
|
|
145
|
+
}
|