@weotro/dx 0.1.19 → 0.1.21

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/README.md CHANGED
@@ -103,6 +103,57 @@ DX_CONFIG_DIR=/path/to/your-repo/dx/config dx status
103
103
 
104
104
  ### 1) dx/config/commands.json
105
105
 
106
+ #### 自定义命令参数透传
107
+
108
+ 在选中的可执行叶节点声明 `allowPassthrough: true`,然后用第一个独立的 `--`
109
+ 分开 DX 选项和子命令参数:
110
+
111
+ ```sh
112
+ dx release plan --prod -- --brands zh --targets backend --ref v0.2.205
113
+ ```
114
+
115
+ 最小 `commands.json`(脚本由目标项目提供):
116
+
117
+ ```json
118
+ {
119
+ "release": {
120
+ "plan": {
121
+ "production": {
122
+ "command": "node scripts/release/cli.mjs plan --environment production",
123
+ "allowPassthrough": true
124
+ }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ 字段契约:
131
+
132
+ | 字段 | 类型 / 默认值 | 作用 |
133
+ | --- | --- | --- |
134
+ | `allowPassthrough` | boolean / `false` | 只有严格的 `true` 开启透传;必须放在环境选择后的 `command` 叶节点,父节点不继承 |
135
+ | `command` | string | 现有 Bash 命令;用户参数逐项转义后追加在末尾,固定参数在前 |
136
+
137
+ 适用于通用配置处理器执行的自定义命令,包括 `release plan/run` 这类内置命令族的扩展 action,
138
+ 以及任意非内置顶层命令。`concurrent`、`sequential`、`internal` 配置暂不支持透传,
139
+ 即使声明 `allowPassthrough: true` 也会拒绝非空参数;不会向多个子任务广播。
140
+ 内置 `test/db/env` 等仍使用各自已有的参数契约,此字段不会覆盖内置处理器。
141
+
142
+ 迁移时,为确实需要接收参数的叶节点增加该字段,并将业务参数移到 `--` 后。
143
+ 未开启或不支持透传的配置收到非空子参数时会非零退出,不再静默丢弃后执行默认命令。
144
+ 没有子参数或仅有末尾 `--` 时保留旧行为;省略分隔符的未知选项继续报错。
145
+
146
+ 第一个 `--` 后的顺序、重复项、空字符串、特殊字符和第二个 `--` 均保留。
147
+ 其中的 `--prod`、`-Y`、`--help`、`--version`、`--config-dir` 不影响 DX 自身。
148
+ 危险操作仍须通过原确认流程;子命令退出失败时 DX 非零退出。
149
+ DX 不校验业务参数含义,拼错的 key、品牌、目标、ref 由子脚本严格校验。
150
+
151
+ 建议 `command` 只调用一个脚本;包含管道、多个语句或末尾注释的 shell 文本应先移入脚本,
152
+ 确保追加参数由明确的入口接收。不要在命令中自行拼接用户输入。
153
+
154
+ 回归验证:`pnpm test --runInBand test/configured-passthrough.test.js`,测试使用真实
155
+ `bin/dx.js` 和临时 probe,校验实际子进程 argv、拒绝时未启动子进程及失败退出。
156
+
106
157
  这是核心文件,定义了 dx 各命令要执行的 shell 命令,也承载帮助信息配置。
107
158
 
108
159
  它支持:
package/bin/dx.js CHANGED
@@ -8,7 +8,9 @@ function parseConfigDir(argv) {
8
8
  const envValue = process.env.DX_CONFIG_DIR
9
9
  if (envValue) return String(envValue)
10
10
 
11
- const args = Array.isArray(argv) ? argv : []
11
+ const raw = Array.isArray(argv) ? argv : []
12
+ const separator = raw.indexOf('--')
13
+ const args = separator === -1 ? raw : raw.slice(0, separator)
12
14
  const idx = args.indexOf('--config-dir')
13
15
  if (idx !== -1 && idx + 1 < args.length) return String(args[idx + 1])
14
16
  for (const token of args) {
@@ -22,7 +24,14 @@ function stripConfigDirArgs(argv) {
22
24
  const out = []
23
25
  for (let i = 0; i < args.length; i++) {
24
26
  const token = args[i]
27
+ if (token === '--') {
28
+ out.push(...args.slice(i))
29
+ break
30
+ }
25
31
  if (token === '--config-dir') {
32
+ if (args[i + 1] === undefined || args[i + 1] === '--') {
33
+ throw new Error('--config-dir 需要在 -- 分隔符前提供配置目录')
34
+ }
26
35
  i++
27
36
  continue
28
37
  }
package/lib/cli/dx-cli.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  resolveTargetRequiredVars,
12
12
  } from '../env-policy.js'
13
13
  import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
14
- import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
14
+ import { getCleanArgs, getCleanArgsWithConsumedValues, getPassthroughArgs } from './args.js'
15
15
  import { showHelp, showCommandHelp } from './help.js'
16
16
  import { buildStrictHelpValidationContext, validateHelpConfig } from './help-schema.js'
17
17
  import { appendNxVerboseFlag } from './nx-command.js'
@@ -189,12 +189,14 @@ class DxCli {
189
189
  }
190
190
 
191
191
  registerConfiguredCommandHandlers() {
192
+ this.configuredCommandNames = new Set()
192
193
  for (const [commandName, config] of Object.entries(this.commands || {})) {
193
194
  if (commandName === 'help') continue
194
195
  if (this.commandHandlers[commandName]) continue
195
196
  if (!this.isExecutableConfigTree(config)) continue
196
197
 
197
198
  this.commandHandlers[commandName] = args => this.handleConfiguredCommand(commandName, args)
199
+ this.configuredCommandNames.add(commandName)
198
200
  }
199
201
  }
200
202
 
@@ -220,6 +222,9 @@ class DxCli {
220
222
  return
221
223
  }
222
224
 
225
+ const passthroughArgs = getPassthroughArgs(this.args)
226
+ this.validateConfiguredPassthrough(config, passthroughArgs)
227
+
223
228
  if (config.dangerous) {
224
229
  const operation = config.description || [commandName, ...args].join(' ')
225
230
  const confirmed = await confirmManager.confirmDangerous(operation, environment, this.flags.Y)
@@ -234,7 +239,17 @@ class DxCli {
234
239
  } else if (config.sequential && Array.isArray(config.commands)) {
235
240
  await this.handleSequentialCommands(config.commands, environment)
236
241
  } else {
237
- await this.executeCommand(config)
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 <命令路径> [环境标志] -- <子命令参数>。')
238
253
  }
239
254
  }
240
255
 
@@ -389,6 +404,14 @@ class DxCli {
389
404
 
390
405
  this.validateInputs()
391
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
+
392
415
  // Fail fast for unknown commands before dependency/env startup checks.
393
416
  if (this.command && !this.commandHandlers[this.command]) {
394
417
  logger.error(`未知命令: ${this.command}`)
@@ -810,6 +833,7 @@ class DxCli {
810
833
 
811
834
  reportUnknownFlag(command, flag, allowedFlags) {
812
835
  logger.error(`检测到未识别的选项: ${flag}`)
836
+ logger.info('自定义命令参数请放在 -- 后,并在 command 叶节点设置 allowPassthrough: true。')
813
837
  const supported = Array.from(allowedFlags.keys())
814
838
  if (supported.length > 0) {
815
839
  logger.info(`支持的选项: ${supported.join(', ')}`)
@@ -937,7 +961,8 @@ class DxCli {
937
961
  }
938
962
 
939
963
  // 执行单个命令
940
- async executeCommand(config, overrideFlags) {
964
+ async executeCommand(config, overrideFlags, passthroughArgs = []) {
965
+ this.validateConfiguredPassthrough(config, passthroughArgs)
941
966
  if (!config) {
942
967
  logger.error('无效的命令配置')
943
968
  return
@@ -1022,6 +1047,7 @@ class DxCli {
1022
1047
  env: config.env || {},
1023
1048
  skipEnvValidation: Boolean(config.skipEnvValidation),
1024
1049
  forcePortCleanup: Boolean(config.forcePortCleanup),
1050
+ passthroughArgs,
1025
1051
  }
1026
1052
 
1027
1053
  await execManager.executeCommand(rawCommand, options)
@@ -84,19 +84,31 @@ export function getGlobalHelpModel(commands = {}, context = {}) {
84
84
  export function getCommandHelpModel(commands = {}, commandName, context = {}) {
85
85
  const commandConfig = commands?.[commandName] ?? {}
86
86
  const commandHelp = getCommandHelpConfig(commands, commandName)
87
+ const notes = [...normalizeArray(commandHelp?.notes)]
88
+ if (hasPassthroughCommand(commandConfig)) {
89
+ notes.push(`自定义 command 叶节点设置 allowPassthrough: true 后可透传参数:dx ${commandName} <命令路径> [环境标志] -- <子命令参数>;未开启或组合/internal 命令收到额外参数会报错。`)
90
+ }
87
91
 
88
92
  return {
89
93
  name: commandName,
90
94
  summary: resolveSummary(commandConfig, commandHelp) || BUILTIN_SUMMARIES[commandName] || '',
91
95
  usage: resolveUsage(commandName, commandHelp, commandConfig),
92
96
  args: normalizeArray(commandHelp?.args),
93
- notes: normalizeArray(commandHelp?.notes),
97
+ notes,
94
98
  examples: normalizeArray(commandHelp?.examples),
95
99
  options: normalizeArray(commandHelp?.options),
96
100
  targets: resolveVisibleTargets(commands, commandName, commandConfig, context),
97
101
  }
98
102
  }
99
103
 
104
+ function hasPassthroughCommand(node) {
105
+ if (!isPlainObject(node)) return false
106
+ if (node.command || node.internal || node.concurrent || node.sequential) {
107
+ return Boolean(node.command && node.allowPassthrough === true && !node.internal && !node.concurrent && !node.sequential)
108
+ }
109
+ return Object.entries(node).some(([key, value]) => key !== 'help' && hasPassthroughCommand(value))
110
+ }
111
+
100
112
  export function resolveSummary(node = {}, help = null) {
101
113
  return help?.summary || node?.help?.summary || node?.description || ''
102
114
  }
package/lib/exec.js CHANGED
@@ -66,6 +66,7 @@ export class ExecManager {
66
66
  ports = [],
67
67
  skipEnvValidation = false,
68
68
  forcePortCleanup = false,
69
+ passthroughArgs = [],
69
70
  } = options
70
71
 
71
72
  // 在执行前同步环境,确保 Nx/构建工具拿到规范的 NODE_ENV
@@ -186,7 +187,12 @@ export class ExecManager {
186
187
  }
187
188
  }
188
189
 
189
- logger.command(fullCommand)
190
+ // Quote for the bash executor, after command classification so child data
191
+ // cannot change environment selection or trigger port-cleanup heuristics.
192
+ const executionCommand = passthroughArgs.length > 0
193
+ ? `${fullCommand} ${passthroughArgs.map(arg => `'${arg.replace(/'/g, "'\\''")}'`).join(' ')}`
194
+ : fullCommand
195
+ logger.command(executionCommand)
190
196
 
191
197
  // 执行命令(可能重试)
192
198
  let lastError = null
@@ -253,7 +259,7 @@ export class ExecManager {
253
259
  }
254
260
  }
255
261
 
256
- const result = await this.spawnCommand(fullCommand, {
262
+ const result = await this.spawnCommand(executionCommand, {
257
263
  cwd: cwd || process.cwd(),
258
264
  stdio,
259
265
  env: sanitizeChildEnv({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weotro/dx",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {