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