@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,1072 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { logger } from '../logger.js'
|
|
5
|
+
import { envManager } from '../env.js'
|
|
6
|
+
import { execManager } from '../exec.js'
|
|
7
|
+
import { validateEnvironment } from '../validate-env.js'
|
|
8
|
+
import {
|
|
9
|
+
loadEnvPolicy,
|
|
10
|
+
resolvePolicyTargetId,
|
|
11
|
+
resolveTargetRequiredVars,
|
|
12
|
+
} from '../env-policy.js'
|
|
13
|
+
import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
|
|
14
|
+
import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
|
|
15
|
+
import { showHelp, showCommandHelp } from './help.js'
|
|
16
|
+
import { buildStrictHelpValidationContext, validateHelpConfig } from './help-schema.js'
|
|
17
|
+
import { appendNxVerboseFlag } from './nx-command.js'
|
|
18
|
+
import { getPackageVersion } from '../version.js'
|
|
19
|
+
import { confirmManager } from '../confirm.js'
|
|
20
|
+
import {
|
|
21
|
+
handleHelp,
|
|
22
|
+
handleBuild,
|
|
23
|
+
handleTest,
|
|
24
|
+
handleLint,
|
|
25
|
+
handleClean,
|
|
26
|
+
handleCache,
|
|
27
|
+
handleInstall,
|
|
28
|
+
handleStatus,
|
|
29
|
+
} from './commands/core.js'
|
|
30
|
+
import { handleStart } from './commands/start.js'
|
|
31
|
+
import { handleDeploy } from './commands/deploy.js'
|
|
32
|
+
import { handleDatabase } from './commands/db.js'
|
|
33
|
+
import { handleWorktree } from './commands/worktree.js'
|
|
34
|
+
import { handlePackage } from './commands/package.js'
|
|
35
|
+
import { handleExport } from './commands/export.js'
|
|
36
|
+
import { handleContracts } from './commands/contracts.js'
|
|
37
|
+
import { handleRelease } from './commands/release.js'
|
|
38
|
+
import { handleEnv } from './commands/env.js'
|
|
39
|
+
import { COMMAND_NOT_HANDLED } from './command-result.js'
|
|
40
|
+
|
|
41
|
+
class DxCli {
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.projectRoot = options.projectRoot || process.env.DX_PROJECT_ROOT || process.cwd()
|
|
44
|
+
this.configDir = options.configDir || process.env.DX_CONFIG_DIR || join(this.projectRoot, 'dx', 'config')
|
|
45
|
+
this.invocation = options.invocation || 'dx'
|
|
46
|
+
|
|
47
|
+
this.commands = this.loadCommands()
|
|
48
|
+
|
|
49
|
+
this.args = process.argv.slice(2)
|
|
50
|
+
this.flags = parseFlags(this.args)
|
|
51
|
+
this.command = this.args[0]
|
|
52
|
+
this.subcommand = this.args[1]
|
|
53
|
+
this.environment = this.args[2]
|
|
54
|
+
this.worktreeManager = null
|
|
55
|
+
this.envCache = null
|
|
56
|
+
this.commandHandlers = {
|
|
57
|
+
help: args => handleHelp(this, args),
|
|
58
|
+
start: args => handleStart(this, args),
|
|
59
|
+
build: args => handleBuild(this, args),
|
|
60
|
+
test: args => handleTest(this, args),
|
|
61
|
+
lint: args => handleLint(this, args),
|
|
62
|
+
clean: args => handleClean(this, args),
|
|
63
|
+
cache: args => handleCache(this, args),
|
|
64
|
+
install: args => handleInstall(this, args),
|
|
65
|
+
status: args => handleStatus(this, args),
|
|
66
|
+
deploy: args => handleDeploy(this, args),
|
|
67
|
+
db: args => handleDatabase(this, args),
|
|
68
|
+
worktree: args => handleWorktree(this, args),
|
|
69
|
+
package: args => handlePackage(this, args),
|
|
70
|
+
export: args => handleExport(this, args),
|
|
71
|
+
contracts: args => handleContracts(this, args),
|
|
72
|
+
release: args => handleRelease(this, args),
|
|
73
|
+
}
|
|
74
|
+
if (this.hasEnvProfileConfig()) {
|
|
75
|
+
this.commandHandlers.env = args => handleEnv(this, args)
|
|
76
|
+
}
|
|
77
|
+
this.registerConfiguredCommandHandlers()
|
|
78
|
+
|
|
79
|
+
this.flagDefinitions = FLAG_DEFINITIONS
|
|
80
|
+
this.validateLoadedHelpConfig()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 加载命令配置
|
|
84
|
+
loadCommands() {
|
|
85
|
+
try {
|
|
86
|
+
const configPath = join(this.configDir, 'commands.json')
|
|
87
|
+
return JSON.parse(readFileSync(configPath, 'utf8'))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error(`无法加载命令配置文件: ${join(this.configDir, 'commands.json')}`)
|
|
90
|
+
logger.error(error?.message || String(error))
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
validateLoadedHelpConfig() {
|
|
96
|
+
validateHelpConfig(this.commands, buildStrictHelpValidationContext(this))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 检测并安装依赖
|
|
100
|
+
async ensureDependencies() {
|
|
101
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules')
|
|
102
|
+
|
|
103
|
+
// 检查 node_modules 是否存在且包含关键依赖
|
|
104
|
+
if (!existsSync(nodeModulesPath) || !existsSync(join(nodeModulesPath, '.pnpm'))) {
|
|
105
|
+
logger.warn('检测到依赖未安装,正在自动安装...')
|
|
106
|
+
logger.info('将以 NODE_ENV=development 安装完整依赖(含 devDependencies)')
|
|
107
|
+
try {
|
|
108
|
+
execSync('pnpm install --frozen-lockfile', {
|
|
109
|
+
stdio: 'inherit',
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
env: {
|
|
112
|
+
...process.env,
|
|
113
|
+
NODE_ENV: 'development', // 确保安装完整依赖(含 devDependencies)
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
logger.success('依赖安装完成')
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error('依赖安装失败,请手动执行: pnpm install')
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
requiresProjectDependencies() {
|
|
125
|
+
if (this.command !== 'deploy') return true
|
|
126
|
+
const targetConfig = this.commands?.deploy?.[this.subcommand]
|
|
127
|
+
if (targetConfig?.internal === 'artifact-deploy') return false
|
|
128
|
+
if (!this.flags.artifact) return true
|
|
129
|
+
return targetConfig?.internal !== 'backend-artifact-deploy'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 每次启动时执行的检查流程
|
|
133
|
+
async runStartupChecks() {
|
|
134
|
+
// 0. 跳过 db 命令的启动检查
|
|
135
|
+
// 原因:ensurePrismaClient() 会调用 `dx db generate`,如果不跳过会导致无限递归
|
|
136
|
+
// db 命令本身不需要 Prisma Client 或环境变量验证即可执行
|
|
137
|
+
if (this.command === 'db') return
|
|
138
|
+
// 通用 artifact target 的构建与部署生命周期完全由自身命令定义,
|
|
139
|
+
// 不应隐式安装 pnpm 依赖、生成 Prisma Client 或校验 backend 环境。
|
|
140
|
+
if (
|
|
141
|
+
this.command === 'deploy'
|
|
142
|
+
&& this.commands?.deploy?.[this.subcommand]?.internal === 'artifact-deploy'
|
|
143
|
+
) return
|
|
144
|
+
|
|
145
|
+
// 1. 在 worktree 中自动同步根目录的 .env.*.local 文件
|
|
146
|
+
try {
|
|
147
|
+
const worktreeManager = await this.getWorktreeManager()
|
|
148
|
+
worktreeManager.syncEnvFilesFromMainRoot()
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.warn(`自动同步 env 文件失败: ${error.message}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. 检测 Prisma Client 是否存在,不存在则执行 db generate
|
|
154
|
+
await this.ensurePrismaClient()
|
|
155
|
+
|
|
156
|
+
// 3. 验证环境变量(尊重多种跳过机制)
|
|
157
|
+
// 跳过条件:
|
|
158
|
+
// - 命令配置了 skipEnvValidation: true(如 lint)
|
|
159
|
+
// - 子命令配置了 skipEnvValidation: true(如 build.shared)
|
|
160
|
+
// - 用户指定了 --no-env-check 标志
|
|
161
|
+
// - 环境变量 AI_SKIP_ENV_CHECK=true(CI 场景)
|
|
162
|
+
// - CI 环境(CI=1)- 由 exec.js 中的 executeCommand 进行精细检查
|
|
163
|
+
// - Vercel 构建环境(VERCEL=1)
|
|
164
|
+
const commandConfig = this.getCommandConfig(this.command)
|
|
165
|
+
const subcommandConfig = this.subcommand ? commandConfig?.[this.subcommand] : null
|
|
166
|
+
const isVercelEnv = String(process.env.VERCEL || '').toLowerCase() === '1'
|
|
167
|
+
// 兼容 GitHub Actions (CI=true) 和其他 CI 系统 (CI=1)
|
|
168
|
+
const isCIEnv = ['true', '1'].includes(String(process.env.CI || '').toLowerCase())
|
|
169
|
+
const skipEnvValidation =
|
|
170
|
+
commandConfig?.skipEnvValidation ||
|
|
171
|
+
subcommandConfig?.skipEnvValidation ||
|
|
172
|
+
this.flags.noEnvCheck ||
|
|
173
|
+
String(process.env.AI_SKIP_ENV_CHECK || '').toLowerCase() === 'true' ||
|
|
174
|
+
isCIEnv ||
|
|
175
|
+
isVercelEnv
|
|
176
|
+
|
|
177
|
+
if (!skipEnvValidation) {
|
|
178
|
+
await this.validateEnvVars()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 获取命令配置
|
|
183
|
+
getCommandConfig(command) {
|
|
184
|
+
return this.commands[command]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
hasEnvProfileConfig() {
|
|
188
|
+
return existsSync(join(this.configDir, 'env-profiles.json'))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
registerConfiguredCommandHandlers() {
|
|
192
|
+
for (const [commandName, config] of Object.entries(this.commands || {})) {
|
|
193
|
+
if (commandName === 'help') continue
|
|
194
|
+
if (this.commandHandlers[commandName]) continue
|
|
195
|
+
if (!this.isExecutableConfigTree(config)) continue
|
|
196
|
+
|
|
197
|
+
this.commandHandlers[commandName] = args => this.handleConfiguredCommand(commandName, args)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
isExecutableConfigTree(node) {
|
|
202
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false
|
|
203
|
+
if (node.command || node.internal || node.concurrent || node.sequential) return true
|
|
204
|
+
|
|
205
|
+
return Object.entries(node).some(([key, value]) => {
|
|
206
|
+
if (key === 'help' || key === 'description' || key === 'args') return false
|
|
207
|
+
if (key === 'app' || key === 'ports' || key === 'dangerous') return false
|
|
208
|
+
return this.isExecutableConfigTree(value)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async handleConfiguredCommand(commandName, args = []) {
|
|
213
|
+
const environment = this.determineEnvironment()
|
|
214
|
+
const config = this.resolveConfiguredCommand(commandName, args, environment)
|
|
215
|
+
|
|
216
|
+
if (!config) {
|
|
217
|
+
const path = [commandName, ...args].join('.')
|
|
218
|
+
logger.error(`未找到命令配置: ${path}`)
|
|
219
|
+
process.exitCode = 1
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (config.dangerous) {
|
|
224
|
+
const operation = config.description || [commandName, ...args].join(' ')
|
|
225
|
+
const confirmed = await confirmManager.confirmDangerous(operation, environment, this.flags.Y)
|
|
226
|
+
if (!confirmed) {
|
|
227
|
+
logger.warn('操作已取消')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config.concurrent && Array.isArray(config.commands)) {
|
|
233
|
+
await this.handleConcurrentCommands(config.commands, null, environment)
|
|
234
|
+
} else if (config.sequential && Array.isArray(config.commands)) {
|
|
235
|
+
await this.handleSequentialCommands(config.commands, environment)
|
|
236
|
+
} else {
|
|
237
|
+
await this.executeCommand(config)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resolveConfiguredCommand(commandName, args = [], environment) {
|
|
242
|
+
let config = this.commands?.[commandName]
|
|
243
|
+
if (!config) return null
|
|
244
|
+
|
|
245
|
+
for (const arg of args) {
|
|
246
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return null
|
|
247
|
+
config = config[arg]
|
|
248
|
+
if (!config) return null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (environment && config && typeof config === 'object' && !Array.isArray(config)) {
|
|
252
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
253
|
+
if (config[envKey]) config = config[envKey]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return config
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 检测并生成 Prisma Client
|
|
260
|
+
async ensurePrismaClient() {
|
|
261
|
+
// 仅对真正安装了 @prisma/client 的项目执行检查。
|
|
262
|
+
// 依据「node_modules/@prisma/client/package.json 是否存在」判断项目是否使用 Prisma,
|
|
263
|
+
// 避免在纯文档站、纯前端等未引入 Prisma 的项目上误触发生成流程,
|
|
264
|
+
// 进而报出形如「未找到 db.generate 命令配置」的误导性错误。
|
|
265
|
+
const prismaClientDir = join(process.cwd(), 'node_modules', '@prisma', 'client')
|
|
266
|
+
if (!existsSync(join(prismaClientDir, 'package.json'))) {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// pnpm 结构下检测 @prisma/client 生成的 default.js 文件
|
|
271
|
+
const prismaClientPath = join(prismaClientDir, 'default.js')
|
|
272
|
+
|
|
273
|
+
if (!existsSync(prismaClientPath)) {
|
|
274
|
+
const environment = this.determineEnvironment()
|
|
275
|
+
logger.step('检测到 Prisma Client 未生成,正在生成...')
|
|
276
|
+
try {
|
|
277
|
+
const generateConfig = this.commands?.db?.generate
|
|
278
|
+
if (!generateConfig?.command) {
|
|
279
|
+
throw new Error('未找到 db.generate 命令配置,请检查 dx/config/commands.json')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await execManager.executeCommand(generateConfig.command, {
|
|
283
|
+
app: generateConfig.app || 'backend',
|
|
284
|
+
flags: this.createExecutionFlags(environment),
|
|
285
|
+
// Prisma generate 不应卡在环境变量校验上
|
|
286
|
+
skipEnvValidation: true,
|
|
287
|
+
})
|
|
288
|
+
logger.success('Prisma Client 生成完成')
|
|
289
|
+
} catch (error) {
|
|
290
|
+
logger.error('Prisma Client 生成失败')
|
|
291
|
+
logger.error(error?.message || String(error))
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 验证环境变量
|
|
298
|
+
async validateEnvVars() {
|
|
299
|
+
const environment = this.determineEnvironment()
|
|
300
|
+
|
|
301
|
+
if (this.envCache?.environment === environment) {
|
|
302
|
+
return this.envCache.layeredEnv
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
validateEnvironment()
|
|
307
|
+
|
|
308
|
+
const layeredEnv = envManager.collectEnvFromLayers('backend', environment)
|
|
309
|
+
if (envManager.latestEnvWarnings && envManager.latestEnvWarnings.length > 0) {
|
|
310
|
+
envManager.latestEnvWarnings.forEach(message => {
|
|
311
|
+
logger.warn(message)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// CI 环境跳过后端环境变量校验(CI 中 build backend 只生成 OpenAPI,不需要数据库连接)
|
|
316
|
+
// 非 CI 环境下,检查 backend 组的环境变量
|
|
317
|
+
if (process.env.CI !== '1') {
|
|
318
|
+
const effectiveEnv = { ...process.env, ...layeredEnv }
|
|
319
|
+
const policy = loadEnvPolicy(envManager.configDir)
|
|
320
|
+
const targetId = resolvePolicyTargetId(policy, 'backend')
|
|
321
|
+
if (!targetId) {
|
|
322
|
+
throw new Error('缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)')
|
|
323
|
+
}
|
|
324
|
+
const requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
325
|
+
if (requiredVars.length > 0) {
|
|
326
|
+
const { valid, missing, placeholders } = envManager.validateRequiredVars(
|
|
327
|
+
requiredVars,
|
|
328
|
+
effectiveEnv,
|
|
329
|
+
)
|
|
330
|
+
if (!valid) {
|
|
331
|
+
const problems = ['环境变量校验未通过']
|
|
332
|
+
if (missing.length > 0) {
|
|
333
|
+
problems.push(`缺少必填环境变量: ${missing.join(', ')}`)
|
|
334
|
+
}
|
|
335
|
+
if (placeholders.length > 0) {
|
|
336
|
+
problems.push(`以下环境变量仍为占位值或空串: ${placeholders.join(', ')}`)
|
|
337
|
+
}
|
|
338
|
+
if (missing.length > 0 || placeholders.length > 0) {
|
|
339
|
+
problems.push(`请在 .env.${environment} / .env.${environment}.local 中补齐配置`)
|
|
340
|
+
}
|
|
341
|
+
throw new Error(problems.join('\n'))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.envCache = { environment, layeredEnv }
|
|
347
|
+
return layeredEnv
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logger.error('环境变量验证失败')
|
|
350
|
+
logger.error(error.message)
|
|
351
|
+
process.exit(1)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 获取环境对应的命令行 flag
|
|
356
|
+
getEnvironmentFlag(environment) {
|
|
357
|
+
switch (environment) {
|
|
358
|
+
case 'production':
|
|
359
|
+
return '--prod'
|
|
360
|
+
case 'staging':
|
|
361
|
+
return '--staging'
|
|
362
|
+
case 'test':
|
|
363
|
+
return '--test'
|
|
364
|
+
case 'e2e':
|
|
365
|
+
return '--e2e'
|
|
366
|
+
case 'development':
|
|
367
|
+
default:
|
|
368
|
+
return '--dev'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 主执行方法
|
|
373
|
+
async run() {
|
|
374
|
+
try {
|
|
375
|
+
if (this.flags.version) {
|
|
376
|
+
console.log(getPackageVersion())
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 显示帮助
|
|
381
|
+
if (this.flags.help || !this.command) {
|
|
382
|
+
if (this.flags.help && this.command && this.command !== 'help') {
|
|
383
|
+
showCommandHelp(this.command, this)
|
|
384
|
+
} else {
|
|
385
|
+
showHelp(this)
|
|
386
|
+
}
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.validateInputs()
|
|
391
|
+
|
|
392
|
+
// Fail fast for unknown commands before dependency/env startup checks.
|
|
393
|
+
if (this.command && !this.commandHandlers[this.command]) {
|
|
394
|
+
logger.error(`未知命令: ${this.command}`)
|
|
395
|
+
showHelp(this)
|
|
396
|
+
process.exit(1)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Commands that should work without env/dependency checks.
|
|
400
|
+
// - help: printing help should never require env vars.
|
|
401
|
+
// - status: should be available even when env is incomplete.
|
|
402
|
+
// - release: only edits package.json versions.
|
|
403
|
+
const skipStartupChecks = new Set(['help', 'status', 'release'])
|
|
404
|
+
if (this.command === 'env' && this.hasEnvProfileConfig()) skipStartupChecks.add('env')
|
|
405
|
+
if (skipStartupChecks.has(this.command)) {
|
|
406
|
+
await this.routeCommand()
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 在执行命令前先校验参数与选项
|
|
411
|
+
if (this.requiresProjectDependencies()) await this.ensureDependencies()
|
|
412
|
+
await this.runStartupChecks()
|
|
413
|
+
|
|
414
|
+
// 设置详细模式
|
|
415
|
+
if (this.flags.verbose) {
|
|
416
|
+
logger.debug('启用详细输出模式')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 路由到对应的命令处理器
|
|
420
|
+
await this.routeCommand()
|
|
421
|
+
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logger.error('命令执行失败')
|
|
424
|
+
logger.error(error.message)
|
|
425
|
+
|
|
426
|
+
if (this.flags.verbose) {
|
|
427
|
+
console.error(error.stack)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
process.exit(1)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 命令路由
|
|
435
|
+
async routeCommand() {
|
|
436
|
+
const command = getCleanArgs(this.args)[0]
|
|
437
|
+
|
|
438
|
+
const allowedFlags = this.getAllowedFlags(command)
|
|
439
|
+
const consumedFlagValueIndexes = this.validateFlags(command, allowedFlags)
|
|
440
|
+
const cleanArgs = getCleanArgsWithConsumedValues(this.args, consumedFlagValueIndexes)
|
|
441
|
+
const [, ...subArgs] = cleanArgs
|
|
442
|
+
|
|
443
|
+
if (!command) {
|
|
444
|
+
showHelp(this)
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const handler = this.commandHandlers[command]
|
|
449
|
+
if (!handler) return
|
|
450
|
+
|
|
451
|
+
const result = await handler(subArgs)
|
|
452
|
+
if (result === COMMAND_NOT_HANDLED) {
|
|
453
|
+
if (this.isExecutableConfigTree(this.commands?.[command])) {
|
|
454
|
+
await this.handleConfiguredCommand(command, subArgs)
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
logger.error(`未找到命令: ${[command, ...subArgs].join(' ')}`)
|
|
459
|
+
showCommandHelp(command, this)
|
|
460
|
+
process.exitCode = 1
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 校验原始输入,禁止未识别的选项或多余参数
|
|
465
|
+
validateInputs() {
|
|
466
|
+
const cleanArgs = getCleanArgs(this.args)
|
|
467
|
+
const command = cleanArgs[0]
|
|
468
|
+
const allowedFlags = this.getAllowedFlags(command)
|
|
469
|
+
const consumedFlagValueIndexes = this.validateFlags(command, allowedFlags)
|
|
470
|
+
|
|
471
|
+
// 收集所有位置参数(不含命令本身、选项及其值)
|
|
472
|
+
const positionalArgs = []
|
|
473
|
+
let commandConsumed = false
|
|
474
|
+
let afterDoubleDash = false
|
|
475
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
476
|
+
const token = this.args[i]
|
|
477
|
+
if (token === '--') {
|
|
478
|
+
afterDoubleDash = true
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
if (afterDoubleDash) continue
|
|
482
|
+
if (token.startsWith('-')) continue
|
|
483
|
+
if (consumedFlagValueIndexes.has(i)) continue
|
|
484
|
+
|
|
485
|
+
if (!commandConsumed && command) {
|
|
486
|
+
// 跳过命令本身
|
|
487
|
+
commandConsumed = true
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
positionalArgs.push(token)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!command) {
|
|
495
|
+
if (positionalArgs.length > 0) {
|
|
496
|
+
this.reportExtraPositionals('全局', positionalArgs)
|
|
497
|
+
}
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this.validatePositionalArgs(command, positionalArgs)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 获取命令允许的选项
|
|
505
|
+
getAllowedFlags(command) {
|
|
506
|
+
const allowed = new Map()
|
|
507
|
+
const applyDefs = defs => {
|
|
508
|
+
defs?.forEach(({ flag, expectsValue }) => {
|
|
509
|
+
if (!flag) return
|
|
510
|
+
allowed.set(flag, { expectsValue: Boolean(expectsValue) })
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
applyDefs(this.flagDefinitions._global)
|
|
515
|
+
if (command && this.flagDefinitions[command]) {
|
|
516
|
+
applyDefs(this.flagDefinitions[command])
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return allowed
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 校验选项合法性并返回被选项消耗的参数下标集合
|
|
523
|
+
validateFlags(command, allowedFlags) {
|
|
524
|
+
const consumedIndexes = new Set()
|
|
525
|
+
const doubleDashIndex = this.args.indexOf('--')
|
|
526
|
+
|
|
527
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
528
|
+
if (doubleDashIndex !== -1 && i >= doubleDashIndex) break
|
|
529
|
+
const token = this.args[i]
|
|
530
|
+
if (!token.startsWith('-')) continue
|
|
531
|
+
|
|
532
|
+
const spec = allowedFlags.get(token)
|
|
533
|
+
if (!spec) {
|
|
534
|
+
this.reportUnknownFlag(command, token, allowedFlags)
|
|
535
|
+
process.exit(1)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (spec.expectsValue) {
|
|
539
|
+
const next = this.args[i + 1]
|
|
540
|
+
if (next === undefined || next.startsWith('-')) {
|
|
541
|
+
logger.error(`选项 ${token} 需要提供参数值`)
|
|
542
|
+
process.exit(1)
|
|
543
|
+
}
|
|
544
|
+
consumedIndexes.add(i + 1)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return consumedIndexes
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 根据命令定义校验位置参数
|
|
552
|
+
validatePositionalArgs(command, positionalArgs) {
|
|
553
|
+
const ensureMax = (max) => {
|
|
554
|
+
if (positionalArgs.length > max) {
|
|
555
|
+
this.reportExtraPositionals(command, positionalArgs.slice(max))
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
switch (command) {
|
|
560
|
+
case 'help':
|
|
561
|
+
ensureMax(1)
|
|
562
|
+
break
|
|
563
|
+
case 'build': {
|
|
564
|
+
this.validateBuildPositionals(positionalArgs)
|
|
565
|
+
if (positionalArgs.length >= 2 && this.isEnvironmentToken(positionalArgs[1])) {
|
|
566
|
+
this.reportEnvironmentFlagRequired(command, positionalArgs[1], positionalArgs)
|
|
567
|
+
}
|
|
568
|
+
ensureMax(1)
|
|
569
|
+
break
|
|
570
|
+
}
|
|
571
|
+
case 'package': {
|
|
572
|
+
if (positionalArgs.length >= 2 && this.isEnvironmentToken(positionalArgs[1])) {
|
|
573
|
+
this.reportEnvironmentFlagRequired(command, positionalArgs[1], positionalArgs)
|
|
574
|
+
}
|
|
575
|
+
ensureMax(1)
|
|
576
|
+
break
|
|
577
|
+
}
|
|
578
|
+
case 'db': {
|
|
579
|
+
if (positionalArgs.length === 0) return
|
|
580
|
+
const action = positionalArgs[0]
|
|
581
|
+
const extras = positionalArgs.slice(1)
|
|
582
|
+
const envToken = extras.find(token => this.isEnvironmentToken(token))
|
|
583
|
+
if (envToken) {
|
|
584
|
+
this.reportEnvironmentFlagRequired(command, envToken, positionalArgs)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (action === 'migrate') {
|
|
588
|
+
if (extras.length > 0) {
|
|
589
|
+
this.reportExtraPositionals(command, extras)
|
|
590
|
+
}
|
|
591
|
+
} else if (action === 'deploy') {
|
|
592
|
+
if (extras.length > 0) {
|
|
593
|
+
this.reportExtraPositionals(command, extras)
|
|
594
|
+
}
|
|
595
|
+
} else if (action === 'script') {
|
|
596
|
+
// script 子命令需要一个脚本名称参数
|
|
597
|
+
if (extras.length > 1) {
|
|
598
|
+
this.reportExtraPositionals(command, extras.slice(1))
|
|
599
|
+
}
|
|
600
|
+
} else if (extras.length > 0) {
|
|
601
|
+
this.reportExtraPositionals(command, extras)
|
|
602
|
+
}
|
|
603
|
+
break
|
|
604
|
+
}
|
|
605
|
+
case 'test':
|
|
606
|
+
this.validateTestPositionals(positionalArgs)
|
|
607
|
+
if ((positionalArgs[0] || 'e2e') !== 'unit') {
|
|
608
|
+
ensureMax(3)
|
|
609
|
+
}
|
|
610
|
+
break
|
|
611
|
+
case 'worktree': {
|
|
612
|
+
if (positionalArgs.length === 0) return
|
|
613
|
+
const action = positionalArgs[0]
|
|
614
|
+
if (action === 'del') {
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
if (action === 'make') {
|
|
618
|
+
ensureMax(3)
|
|
619
|
+
break
|
|
620
|
+
}
|
|
621
|
+
if (action === 'list' || action === 'clean') {
|
|
622
|
+
ensureMax(1)
|
|
623
|
+
break
|
|
624
|
+
}
|
|
625
|
+
break
|
|
626
|
+
}
|
|
627
|
+
case 'start': {
|
|
628
|
+
this.validateStartPositionals(positionalArgs)
|
|
629
|
+
const extras = positionalArgs.slice(1)
|
|
630
|
+
const envToken = extras.find(token => this.isEnvironmentToken(token))
|
|
631
|
+
if (envToken) {
|
|
632
|
+
this.reportEnvironmentFlagRequired(command, envToken, positionalArgs)
|
|
633
|
+
}
|
|
634
|
+
ensureMax(1)
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
case 'lint':
|
|
638
|
+
ensureMax(0)
|
|
639
|
+
break
|
|
640
|
+
case 'clean':
|
|
641
|
+
ensureMax(1)
|
|
642
|
+
break
|
|
643
|
+
case 'cache':
|
|
644
|
+
ensureMax(1)
|
|
645
|
+
break
|
|
646
|
+
case 'status':
|
|
647
|
+
ensureMax(0)
|
|
648
|
+
break
|
|
649
|
+
default:
|
|
650
|
+
// 默认放行,具体命令内部再校验
|
|
651
|
+
break
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
isEnvironmentToken(token) {
|
|
656
|
+
if (!token) return false
|
|
657
|
+
const value = String(token).toLowerCase()
|
|
658
|
+
return (
|
|
659
|
+
value === 'dev' ||
|
|
660
|
+
value === 'prod' ||
|
|
661
|
+
value === 'staging' ||
|
|
662
|
+
value === 'test' ||
|
|
663
|
+
value === 'e2e'
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
reportEnvironmentFlagRequired(command, token, positionalArgs = []) {
|
|
668
|
+
const normalizedFlag = this.getEnvironmentFlagExample(token)
|
|
669
|
+
logger.error(`命令 ${command} 不再支持通过位置参数指定环境: ${token}`)
|
|
670
|
+
logger.info('请使用带前缀的环境标志,例如 --dev、--staging、--prod、--test 或 --e2e。')
|
|
671
|
+
const suggestion = normalizedFlag
|
|
672
|
+
? this.buildEnvironmentSuggestion(command, normalizedFlag, positionalArgs, token)
|
|
673
|
+
: null
|
|
674
|
+
if (suggestion) {
|
|
675
|
+
logger.info(`建议命令: ${suggestion}`)
|
|
676
|
+
} else if (normalizedFlag) {
|
|
677
|
+
logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
|
|
678
|
+
}
|
|
679
|
+
logger.info('未显式指定环境时将默认使用 --dev。')
|
|
680
|
+
process.exit(1)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
getEnvironmentFlagExample(token) {
|
|
684
|
+
switch (String(token || '').toLowerCase()) {
|
|
685
|
+
case 'dev':
|
|
686
|
+
case 'development':
|
|
687
|
+
return '--dev'
|
|
688
|
+
case 'prod':
|
|
689
|
+
case 'production':
|
|
690
|
+
return '--prod'
|
|
691
|
+
case 'staging':
|
|
692
|
+
return '--staging'
|
|
693
|
+
case 'test':
|
|
694
|
+
return '--test'
|
|
695
|
+
case 'e2e':
|
|
696
|
+
return '--e2e'
|
|
697
|
+
default:
|
|
698
|
+
return null
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
buildEnvironmentSuggestion(command, normalizedFlag, positionalArgs, token) {
|
|
703
|
+
const parts = [this.invocation, command]
|
|
704
|
+
const rest = Array.isArray(positionalArgs) ? [...positionalArgs] : []
|
|
705
|
+
if (rest.length > 0) {
|
|
706
|
+
const matchIndex = rest.findIndex(arg => String(arg).toLowerCase() === String(token).toLowerCase())
|
|
707
|
+
if (matchIndex !== -1) rest.splice(matchIndex, 1)
|
|
708
|
+
}
|
|
709
|
+
if (!rest.includes(normalizedFlag)) {
|
|
710
|
+
rest.push(normalizedFlag)
|
|
711
|
+
}
|
|
712
|
+
return parts.concat(rest).join(' ')
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
validateTestPositionals(positionalArgs) {
|
|
716
|
+
const [type = 'e2e', target = 'all', testPath] = positionalArgs
|
|
717
|
+
|
|
718
|
+
if (type !== 'e2e') return
|
|
719
|
+
|
|
720
|
+
if (target === 'all') {
|
|
721
|
+
logger.error('dx test e2e all 不受支持,请指定 target 和测试文件或目录路径')
|
|
722
|
+
process.exit(1)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const testConfig = this.commands?.test?.[type]?.[target]
|
|
726
|
+
if (!testConfig) return
|
|
727
|
+
if (!testConfig.requiresPath) return
|
|
728
|
+
|
|
729
|
+
if (!testPath) {
|
|
730
|
+
logger.error(`dx test e2e ${target} 必须提供测试文件或目录路径`)
|
|
731
|
+
logger.info(`示例: ${this.invocation} test e2e ${target} apps/${target}/e2e/health`)
|
|
732
|
+
process.exit(1)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (testPath === 'all') {
|
|
736
|
+
logger.error(`dx test e2e ${target} 不支持 all,必须提供测试文件或目录路径`)
|
|
737
|
+
process.exit(1)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
validateStartPositionals(positionalArgs) {
|
|
742
|
+
const service = positionalArgs[0] || 'development'
|
|
743
|
+
const environment = this.determineEnvironment()
|
|
744
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
745
|
+
|
|
746
|
+
if (service === 'development' && envKey !== 'development') {
|
|
747
|
+
logger.error('dx start 在未指定服务时仅允许使用开发环境')
|
|
748
|
+
logger.info(`示例: ${this.invocation} start --dev`)
|
|
749
|
+
logger.info(`示例: ${this.invocation} start backend --prod`)
|
|
750
|
+
process.exit(1)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const startConfig = this.commands?.start?.[service]
|
|
754
|
+
if (!startConfig) return
|
|
755
|
+
|
|
756
|
+
// 对 start 下的单层 command 配置,默认视为开发态目标,仅允许 --dev。
|
|
757
|
+
if (startConfig.command && envKey !== 'development') {
|
|
758
|
+
logger.error(`启动目标 ${service} 仅支持开发环境`)
|
|
759
|
+
logger.info(`示例: ${this.invocation} start ${service} --dev`)
|
|
760
|
+
process.exit(1)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
validateBuildPositionals(positionalArgs) {
|
|
765
|
+
const target = positionalArgs[0] || 'all'
|
|
766
|
+
const environment = this.determineEnvironment()
|
|
767
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
768
|
+
const explicitEnv = Boolean(
|
|
769
|
+
this.flags.dev || this.flags.prod || this.flags.staging || this.flags.test || this.flags.e2e,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
if (!explicitEnv) return
|
|
773
|
+
|
|
774
|
+
const buildConfig = this.commands?.build?.[target]
|
|
775
|
+
if (!buildConfig || buildConfig.command) return
|
|
776
|
+
|
|
777
|
+
const supportsCurrentEnv = Boolean(buildConfig[envKey])
|
|
778
|
+
if (supportsCurrentEnv) return
|
|
779
|
+
|
|
780
|
+
const envFlag = this.getEnvironmentFlagExample(envKey) || `--${envKey}`
|
|
781
|
+
logger.error(`构建目标 ${target} 不支持 ${envFlag} 环境`)
|
|
782
|
+
logger.info('显式传入环境标志时,必须是该 target 实际支持的环境。')
|
|
783
|
+
const available = ['development', 'staging', 'production', 'test', 'e2e']
|
|
784
|
+
.filter(key => key in buildConfig)
|
|
785
|
+
.map(key => this.getEnvironmentFlagExample(key) || `--${key}`)
|
|
786
|
+
if (available.length > 0) {
|
|
787
|
+
logger.info(`支持的环境: ${available.join(', ')}`)
|
|
788
|
+
logger.info(`示例: ${this.invocation} build ${target} ${available[0]}`)
|
|
789
|
+
if (available.length > 1) {
|
|
790
|
+
logger.info(`示例: ${this.invocation} build ${target} ${available[1]}`)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
process.exit(1)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
reportExtraPositionals(command, extras) {
|
|
797
|
+
const list = extras.join(', ')
|
|
798
|
+
if (command === '全局') {
|
|
799
|
+
logger.error(`检测到未识别的参数: ${list}`)
|
|
800
|
+
} else {
|
|
801
|
+
logger.error(`命令 ${command} 存在未识别的额外参数: ${list}`)
|
|
802
|
+
}
|
|
803
|
+
const hint = command && command !== '全局' ? `${this.invocation} help ${command}` : `${this.invocation} --help`
|
|
804
|
+
logger.info(`提示: 执行 ${hint} 或 ${this.invocation} --help 查看命令用法`)
|
|
805
|
+
if (command && command !== '全局') {
|
|
806
|
+
logger.info(`示例: ${this.invocation} ${command} --help`)
|
|
807
|
+
}
|
|
808
|
+
process.exit(1)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
reportUnknownFlag(command, flag, allowedFlags) {
|
|
812
|
+
logger.error(`检测到未识别的选项: ${flag}`)
|
|
813
|
+
const supported = Array.from(allowedFlags.keys())
|
|
814
|
+
if (supported.length > 0) {
|
|
815
|
+
logger.info(`支持的选项: ${supported.join(', ')}`)
|
|
816
|
+
} else if (command) {
|
|
817
|
+
logger.info(`命令 ${command} 不接受额外选项`)
|
|
818
|
+
}
|
|
819
|
+
const hint = command ? `${this.invocation} help ${command}` : `${this.invocation} --help`
|
|
820
|
+
logger.info(`提示: 执行 ${hint} 或 ${this.invocation} --help 查看命令用法`)
|
|
821
|
+
if (command) {
|
|
822
|
+
logger.info(`示例: ${this.invocation} ${command} --help`)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 校验是否在仓库根目录执行
|
|
827
|
+
ensureRepoRoot() {
|
|
828
|
+
const cwd = process.cwd()
|
|
829
|
+
|
|
830
|
+
const markers = ['pnpm-workspace.yaml', 'package.json', 'apps']
|
|
831
|
+
const missing = markers.filter(p => !existsSync(join(cwd, p)))
|
|
832
|
+
if (missing.length) {
|
|
833
|
+
logger.error(`请从仓库根目录运行此命令。缺少标识文件/目录: ${missing.join(', ')}`)
|
|
834
|
+
process.exit(1)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const commandsPath = join(this.configDir, 'commands.json')
|
|
838
|
+
if (!existsSync(commandsPath)) {
|
|
839
|
+
logger.error(`未找到命令配置文件: ${commandsPath}`)
|
|
840
|
+
process.exit(1)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async getWorktreeManager() {
|
|
845
|
+
if (!this.worktreeManager) {
|
|
846
|
+
const { default: worktreeManager } = await import('../worktree.js')
|
|
847
|
+
this.worktreeManager = worktreeManager
|
|
848
|
+
}
|
|
849
|
+
return this.worktreeManager
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// 并发命令处理
|
|
853
|
+
async handleConcurrentCommands(commandPaths, baseCommand, environment) {
|
|
854
|
+
const commands = []
|
|
855
|
+
|
|
856
|
+
for (const path of commandPaths) {
|
|
857
|
+
const config = this.resolveCommandPath(
|
|
858
|
+
path,
|
|
859
|
+
baseCommand,
|
|
860
|
+
this.normalizeEnvKey(environment)
|
|
861
|
+
)
|
|
862
|
+
if (!config) {
|
|
863
|
+
logger.warn(`未解析到命令配置: ${path} (${environment || '-'})`)
|
|
864
|
+
continue
|
|
865
|
+
}
|
|
866
|
+
commands.push({
|
|
867
|
+
command: config.command,
|
|
868
|
+
options: {
|
|
869
|
+
app: config.app,
|
|
870
|
+
ports: config.ports,
|
|
871
|
+
flags: this.flags,
|
|
872
|
+
},
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (commands.length > 0) {
|
|
877
|
+
await execManager.executeConcurrent(commands)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// 顺序命令处理
|
|
882
|
+
async handleSequentialCommands(commandPaths, environment) {
|
|
883
|
+
for (const path of commandPaths) {
|
|
884
|
+
const config = this.resolveCommandPath(path, null, this.normalizeEnvKey(environment))
|
|
885
|
+
if (!config) {
|
|
886
|
+
logger.warn(`未解析到命令配置: ${path} (${environment || '-'})`)
|
|
887
|
+
continue
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// 支持在顺序执行中嵌套并发/顺序配置
|
|
891
|
+
if (config.concurrent && Array.isArray(config.commands)) {
|
|
892
|
+
await this.handleConcurrentCommands(config.commands, null, environment)
|
|
893
|
+
} else if (config.sequential && Array.isArray(config.commands)) {
|
|
894
|
+
await this.handleSequentialCommands(config.commands, environment)
|
|
895
|
+
} else {
|
|
896
|
+
await this.executeCommand(config)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// 解析命令路径
|
|
902
|
+
resolveCommandPath(path, baseCommand, environment) {
|
|
903
|
+
const parts = path.split('.')
|
|
904
|
+
let config = this.commands
|
|
905
|
+
|
|
906
|
+
for (const part of parts) {
|
|
907
|
+
config = config[part]
|
|
908
|
+
if (!config) break
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// 如果有环境参数,尝试获取对应环境的配置
|
|
912
|
+
if (environment && config) {
|
|
913
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
914
|
+
if (config[envKey]) config = config[envKey]
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return config
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
collectStartPorts(service, startConfig, envKey) {
|
|
921
|
+
const portSet = new Set()
|
|
922
|
+
|
|
923
|
+
if (startConfig && Array.isArray(startConfig.ports)) {
|
|
924
|
+
startConfig.ports.forEach(port => {
|
|
925
|
+
this.addPortToSet(portSet, port)
|
|
926
|
+
})
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return Array.from(portSet)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
addPortToSet(target, port) {
|
|
933
|
+
const numeric = Number(port)
|
|
934
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
935
|
+
target.add(numeric)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// 执行单个命令
|
|
940
|
+
async executeCommand(config, overrideFlags) {
|
|
941
|
+
if (!config) {
|
|
942
|
+
logger.error('无效的命令配置')
|
|
943
|
+
return
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const withTempEnv = async fn => {
|
|
947
|
+
const envPatch = config?.env && typeof config.env === 'object' ? config.env : null
|
|
948
|
+
if (!envPatch) return await fn()
|
|
949
|
+
|
|
950
|
+
const previous = {}
|
|
951
|
+
for (const [key, value] of Object.entries(envPatch)) {
|
|
952
|
+
previous[key] = Object.prototype.hasOwnProperty.call(process.env, key)
|
|
953
|
+
? process.env[key]
|
|
954
|
+
: undefined
|
|
955
|
+
process.env[key] = String(value)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
return await fn()
|
|
960
|
+
} finally {
|
|
961
|
+
for (const [key, oldValue] of Object.entries(previous)) {
|
|
962
|
+
if (oldValue === undefined) delete process.env[key]
|
|
963
|
+
else process.env[key] = oldValue
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// internal runners (for projects that only ship scripts/config)
|
|
969
|
+
if (config.internal) {
|
|
970
|
+
if (config.internal === 'sdk-build') {
|
|
971
|
+
await withTempEnv(async () => {
|
|
972
|
+
const { runSdkBuild } = await import('../sdk-build.js')
|
|
973
|
+
await runSdkBuild(Array.isArray(config.args) ? config.args : [])
|
|
974
|
+
})
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
if (config.internal === 'backend-package') {
|
|
978
|
+
await withTempEnv(async () => {
|
|
979
|
+
const { runBackendPackage } = await import('../backend-package.js')
|
|
980
|
+
await runBackendPackage(Array.isArray(config.args) ? config.args : [])
|
|
981
|
+
})
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (config.internal === 'start-dev') {
|
|
986
|
+
await withTempEnv(async () => {
|
|
987
|
+
const { runStartDev } = await import('../start-dev.js')
|
|
988
|
+
await runStartDev(Array.isArray(config.args) ? config.args : [])
|
|
989
|
+
})
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (config.internal === 'pm2-stack') {
|
|
994
|
+
await withTempEnv(async () => {
|
|
995
|
+
const { runPm2Stack } = await import('./commands/stack.js')
|
|
996
|
+
const stackConfig = config.stack && typeof config.stack === 'object' ? config.stack : {}
|
|
997
|
+
await runPm2Stack(stackConfig)
|
|
998
|
+
})
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
throw new Error(`未知 internal runner: ${config.internal}`)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (!config.command) {
|
|
1006
|
+
logger.error('无效的命令配置: 缺少 command/internal')
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const effectiveFlags = overrideFlags || this.flags
|
|
1011
|
+
let rawCommand = String(config.command).trim()
|
|
1012
|
+
if (effectiveFlags.verbose) {
|
|
1013
|
+
rawCommand = appendNxVerboseFlag(rawCommand)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const options = {
|
|
1017
|
+
app: config.app,
|
|
1018
|
+
flags: effectiveFlags,
|
|
1019
|
+
cwd: config.cwd,
|
|
1020
|
+
ports: config.ports || [],
|
|
1021
|
+
// 允许上游在 config.env 中注入环境变量(例如 NX_CACHE=false)
|
|
1022
|
+
env: config.env || {},
|
|
1023
|
+
skipEnvValidation: Boolean(config.skipEnvValidation),
|
|
1024
|
+
forcePortCleanup: Boolean(config.forcePortCleanup),
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
await execManager.executeCommand(rawCommand, options)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// 确定环境
|
|
1031
|
+
determineEnvironment() {
|
|
1032
|
+
return envManager.detectEnvironment(this.flags)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// 规范化环境键到命令配置使用的命名(development/staging/production/test/e2e)
|
|
1036
|
+
normalizeEnvKey(env) {
|
|
1037
|
+
switch (String(env || '').toLowerCase()) {
|
|
1038
|
+
case 'development':
|
|
1039
|
+
return 'development'
|
|
1040
|
+
case 'production':
|
|
1041
|
+
return 'production'
|
|
1042
|
+
case 'staging':
|
|
1043
|
+
return 'staging'
|
|
1044
|
+
case 'test':
|
|
1045
|
+
return 'test'
|
|
1046
|
+
case 'e2e':
|
|
1047
|
+
return 'e2e'
|
|
1048
|
+
default:
|
|
1049
|
+
return env
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
createExecutionFlags(environment) {
|
|
1054
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
1055
|
+
const execFlags = { ...this.flags }
|
|
1056
|
+
|
|
1057
|
+
;['dev', 'prod', 'staging', 'test', 'e2e'].forEach(key => {
|
|
1058
|
+
delete execFlags[key]
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
if (envKey === 'production') execFlags.prod = true
|
|
1062
|
+
else if (envKey === 'development') execFlags.dev = true
|
|
1063
|
+
else if (envKey === 'test') execFlags.test = true
|
|
1064
|
+
else if (envKey === 'e2e') execFlags.e2e = true
|
|
1065
|
+
else if (envKey === 'staging') execFlags.staging = true
|
|
1066
|
+
|
|
1067
|
+
return execFlags
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export { DxCli }
|