@ranger1/dx 0.1.95 → 0.1.97

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/lib/cli/dx-cli.js CHANGED
@@ -13,10 +13,10 @@ import {
13
13
  import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
14
14
  import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
15
15
  import { showHelp, showCommandHelp } from './help.js'
16
+ import { buildStrictHelpValidationContext, validateHelpConfig } from './help-schema.js'
16
17
  import { getPackageVersion } from '../version.js'
17
18
  import {
18
19
  handleHelp,
19
- handleDev,
20
20
  handleBuild,
21
21
  handleTest,
22
22
  handleLint,
@@ -51,7 +51,6 @@ class DxCli {
51
51
  this.envCache = null
52
52
  this.commandHandlers = {
53
53
  help: args => handleHelp(this, args),
54
- dev: args => handleDev(this, args),
55
54
  start: args => handleStart(this, args),
56
55
  build: args => handleBuild(this, args),
57
56
  test: args => handleTest(this, args),
@@ -70,6 +69,7 @@ class DxCli {
70
69
  }
71
70
 
72
71
  this.flagDefinitions = FLAG_DEFINITIONS
72
+ this.validateLoadedHelpConfig()
73
73
  }
74
74
 
75
75
  // 加载命令配置
@@ -84,6 +84,10 @@ class DxCli {
84
84
  }
85
85
  }
86
86
 
87
+ validateLoadedHelpConfig() {
88
+ validateHelpConfig(this.commands, buildStrictHelpValidationContext(this))
89
+ }
90
+
87
91
  // 检测并安装依赖
88
92
  async ensureDependencies() {
89
93
  const nodeModulesPath = join(process.cwd(), 'node_modules')
@@ -172,22 +176,9 @@ class DxCli {
172
176
  throw new Error('未找到 db.generate 命令配置,请检查 dx/config/commands.json')
173
177
  }
174
178
 
175
- const envKey = this.normalizeEnvKey(environment)
176
- const execFlags = { ...this.flags }
177
- ;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
178
- key => {
179
- delete execFlags[key]
180
- },
181
- )
182
- if (envKey === 'prod') execFlags.prod = true
183
- else if (envKey === 'dev') execFlags.dev = true
184
- else if (envKey === 'test') execFlags.test = true
185
- else if (envKey === 'e2e') execFlags.e2e = true
186
- else if (envKey === 'staging') execFlags.staging = true
187
-
188
179
  await execManager.executeCommand(generateConfig.command, {
189
180
  app: generateConfig.app || 'backend',
190
- flags: execFlags,
181
+ flags: this.createExecutionFlags(environment),
191
182
  // Prisma generate 不应卡在环境变量校验上
192
183
  skipEnvValidation: true,
193
184
  })
@@ -286,9 +277,9 @@ class DxCli {
286
277
  // 显示帮助
287
278
  if (this.flags.help || !this.command) {
288
279
  if (this.flags.help && this.command && this.command !== 'help') {
289
- showCommandHelp(this.command)
280
+ showCommandHelp(this.command, this)
290
281
  } else {
291
- showHelp()
282
+ showHelp(this)
292
283
  }
293
284
  return
294
285
  }
@@ -298,7 +289,7 @@ class DxCli {
298
289
  // Fail fast for unknown commands before dependency/env startup checks.
299
290
  if (this.command && !this.commandHandlers[this.command]) {
300
291
  logger.error(`未知命令: ${this.command}`)
301
- showHelp()
292
+ showHelp(this)
302
293
  process.exit(1)
303
294
  }
304
295
 
@@ -346,7 +337,7 @@ class DxCli {
346
337
  const [, ...subArgs] = cleanArgs
347
338
 
348
339
  if (!command) {
349
- showHelp()
340
+ showHelp(this)
350
341
  return
351
342
  }
352
343
 
@@ -504,14 +495,14 @@ class DxCli {
504
495
  case 'worktree': {
505
496
  if (positionalArgs.length === 0) return
506
497
  const action = positionalArgs[0]
507
- if (['del', 'delete', 'rm'].includes(action)) {
498
+ if (action === 'del') {
508
499
  return
509
500
  }
510
- if (['make'].includes(action)) {
501
+ if (action === 'make') {
511
502
  ensureMax(3)
512
503
  break
513
504
  }
514
- if (['list', 'ls', 'clean', 'prune'].includes(action)) {
505
+ if (action === 'list' || action === 'clean') {
515
506
  ensureMax(1)
516
507
  break
517
508
  }
@@ -550,11 +541,8 @@ class DxCli {
550
541
  const value = String(token).toLowerCase()
551
542
  return (
552
543
  value === 'dev' ||
553
- value === 'development' ||
554
544
  value === 'prod' ||
555
- value === 'production' ||
556
545
  value === 'staging' ||
557
- value === 'stage' ||
558
546
  value === 'test' ||
559
547
  value === 'e2e'
560
548
  )
@@ -570,18 +558,19 @@ class DxCli {
570
558
  if (suggestion) {
571
559
  logger.info(`建议命令: ${suggestion}`)
572
560
  } else if (normalizedFlag) {
573
- logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
561
+ logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
574
562
  }
575
563
  logger.info('未显式指定环境时将默认使用 --dev。')
576
564
  process.exit(1)
577
565
  }
578
566
 
579
567
  getEnvironmentFlagExample(token) {
580
- const key = this.normalizeEnvKey(token)
581
- switch (key) {
568
+ switch (String(token || '').toLowerCase()) {
582
569
  case 'dev':
570
+ case 'development':
583
571
  return '--dev'
584
572
  case 'prod':
573
+ case 'production':
585
574
  return '--prod'
586
575
  case 'staging':
587
576
  return '--staging'
@@ -634,13 +623,13 @@ class DxCli {
634
623
  }
635
624
 
636
625
  validateStartPositionals(positionalArgs) {
637
- const service = positionalArgs[0] || 'dev'
626
+ const service = positionalArgs[0] || 'development'
638
627
  const environment = this.determineEnvironment()
639
628
  const envKey = this.normalizeEnvKey(environment)
640
629
 
641
- if (service === 'dev' && envKey !== 'dev') {
630
+ if (service === 'development' && envKey !== 'development') {
642
631
  logger.error('dx start 在未指定服务时仅允许使用开发环境')
643
- logger.info(`示例: ${this.invocation} start all --dev`)
632
+ logger.info(`示例: ${this.invocation} start --dev`)
644
633
  logger.info(`示例: ${this.invocation} start backend --prod`)
645
634
  process.exit(1)
646
635
  }
@@ -649,7 +638,7 @@ class DxCli {
649
638
  if (!startConfig) return
650
639
 
651
640
  // 对 start 下的单层 command 配置,默认视为开发态目标,仅允许 --dev。
652
- if (startConfig.command && envKey !== 'dev') {
641
+ if (startConfig.command && envKey !== 'development') {
653
642
  logger.error(`启动目标 ${service} 仅支持开发环境`)
654
643
  logger.info(`示例: ${this.invocation} start ${service} --dev`)
655
644
  process.exit(1)
@@ -669,13 +658,13 @@ class DxCli {
669
658
  const buildConfig = this.commands?.build?.[target]
670
659
  if (!buildConfig || buildConfig.command) return
671
660
 
672
- const supportsCurrentEnv = Boolean(buildConfig[envKey] || (envKey === 'staging' && buildConfig.prod))
661
+ const supportsCurrentEnv = Boolean(buildConfig[envKey])
673
662
  if (supportsCurrentEnv) return
674
663
 
675
664
  const envFlag = this.getEnvironmentFlagExample(envKey) || `--${envKey}`
676
665
  logger.error(`构建目标 ${target} 不支持 ${envFlag} 环境`)
677
666
  logger.info('显式传入环境标志时,必须是该 target 实际支持的环境。')
678
- const available = ['dev', 'staging', 'prod', 'test', 'e2e']
667
+ const available = ['development', 'staging', 'production', 'test', 'e2e']
679
668
  .filter(key => key in buildConfig)
680
669
  .map(key => this.getEnvironmentFlagExample(key) || `--${key}`)
681
670
  if (available.length > 0) {
@@ -688,19 +677,6 @@ class DxCli {
688
677
  process.exit(1)
689
678
  }
690
679
 
691
- reportDevCommandRemoved(args) {
692
- const target = args?.[0]
693
- logger.error('`dx dev` 命令已移除,统一使用 `dx start`。')
694
- if (target) {
695
- logger.info(`请执行: ${this.invocation} start ${target} --dev`)
696
- } else {
697
- logger.info(`示例: ${this.invocation} start backend --dev`)
698
- logger.info(` ${this.invocation} start front --dev`)
699
- logger.info(` ${this.invocation} start admin --dev`)
700
- }
701
- process.exit(1)
702
- }
703
-
704
680
  reportExtraPositionals(command, extras) {
705
681
  const list = extras.join(', ')
706
682
  if (command === '全局') {
@@ -772,7 +748,7 @@ class DxCli {
772
748
  continue
773
749
  }
774
750
  commands.push({
775
- command: this.applySdkOfflineFlag(config.command),
751
+ command: config.command,
776
752
  options: {
777
753
  app: config.app,
778
754
  ports: config.ports,
@@ -820,22 +796,11 @@ class DxCli {
820
796
  if (environment && config) {
821
797
  const envKey = this.normalizeEnvKey(environment)
822
798
  if (config[envKey]) config = config[envKey]
823
- else if (envKey === 'staging' && config.prod) config = config.prod
824
799
  }
825
800
 
826
801
  return config
827
802
  }
828
803
 
829
- // SDK 构建命令当前不再暴露 --online/--offline 模式,保留该方法仅为兼容旧调用
830
- applySdkModeFlags(command) {
831
- return command
832
- }
833
-
834
- // 向后兼容的别名
835
- applySdkOfflineFlag(command) {
836
- return command
837
- }
838
-
839
804
  collectStartPorts(service, startConfig, envKey) {
840
805
  const portSet = new Set()
841
806
 
@@ -845,15 +810,6 @@ class DxCli {
845
810
  })
846
811
  }
847
812
 
848
- if (envKey === 'dev') {
849
- const legacyConfig = this.commands.dev?.[service]
850
- if (legacyConfig && Array.isArray(legacyConfig.ports)) {
851
- legacyConfig.ports.forEach(port => {
852
- this.addPortToSet(portSet, port)
853
- })
854
- }
855
- }
856
-
857
813
  return Array.from(portSet)
858
814
  }
859
815
 
@@ -936,28 +892,6 @@ class DxCli {
936
892
  }
937
893
 
938
894
  const rawCommand = String(config.command).trim()
939
- // backward compat: old commands.json referenced scripts/lib/*.js in the project
940
- if (rawCommand.startsWith('node scripts/lib/sdk-build.js')) {
941
- const argsText = rawCommand.replace(/^node\s+scripts\/lib\/sdk-build\.js\s*/g, '')
942
- const args = argsText ? argsText.split(/\s+/).filter(Boolean) : []
943
- await withTempEnv(async () => {
944
- const { runSdkBuild } = await import('../sdk-build.js')
945
- await runSdkBuild(args)
946
- })
947
- return
948
- }
949
-
950
- if (rawCommand.startsWith('node scripts/lib/backend-package.js')) {
951
- const argsText = rawCommand.replace(/^node\s+scripts\/lib\/backend-package\.js\s*/g, '')
952
- const args = argsText ? argsText.split(/\s+/).filter(Boolean) : []
953
- await withTempEnv(async () => {
954
- const { runBackendPackage } = await import('../backend-package.js')
955
- await runBackendPackage(args)
956
- })
957
- return
958
- }
959
-
960
- const command = this.applySdkOfflineFlag(rawCommand)
961
895
 
962
896
  const options = {
963
897
  app: config.app,
@@ -969,7 +903,7 @@ class DxCli {
969
903
  forcePortCleanup: Boolean(config.forcePortCleanup),
970
904
  }
971
905
 
972
- await execManager.executeCommand(command, options)
906
+ await execManager.executeCommand(rawCommand, options)
973
907
  }
974
908
 
975
909
  // 确定环境
@@ -977,17 +911,14 @@ class DxCli {
977
911
  return envManager.detectEnvironment(this.flags)
978
912
  }
979
913
 
980
- // 规范化环境键到命令配置使用的命名(dev/prod/test/e2e)
914
+ // 规范化环境键到命令配置使用的命名(development/staging/production/test/e2e)
981
915
  normalizeEnvKey(env) {
982
916
  switch (String(env || '').toLowerCase()) {
983
917
  case 'development':
984
- case 'dev':
985
- return 'dev'
918
+ return 'development'
986
919
  case 'production':
987
- case 'prod':
988
- return 'prod'
920
+ return 'production'
989
921
  case 'staging':
990
- case 'stage':
991
922
  return 'staging'
992
923
  case 'test':
993
924
  return 'test'
@@ -998,6 +929,23 @@ class DxCli {
998
929
  }
999
930
  }
1000
931
 
932
+ createExecutionFlags(environment) {
933
+ const envKey = this.normalizeEnvKey(environment)
934
+ const execFlags = { ...this.flags }
935
+
936
+ ;['dev', 'prod', 'staging', 'test', 'e2e'].forEach(key => {
937
+ delete execFlags[key]
938
+ })
939
+
940
+ if (envKey === 'production') execFlags.prod = true
941
+ else if (envKey === 'development') execFlags.dev = true
942
+ else if (envKey === 'test') execFlags.test = true
943
+ else if (envKey === 'e2e') execFlags.e2e = true
944
+ else if (envKey === 'staging') execFlags.staging = true
945
+
946
+ return execFlags
947
+ }
948
+
1001
949
  }
1002
950
 
1003
951
  export { DxCli }
package/lib/cli/flags.js CHANGED
@@ -1,11 +1,8 @@
1
1
  export const FLAG_DEFINITIONS = {
2
2
  _global: [
3
3
  { flag: '--dev' },
4
- { flag: '--development' },
5
4
  { flag: '--prod' },
6
- { flag: '--production' },
7
5
  { flag: '--staging' },
8
- { flag: '--stage' },
9
6
  { flag: '--test' },
10
7
  { flag: '--e2e' },
11
8
  { flag: '--no-env-check' },
@@ -24,7 +21,11 @@ export const FLAG_DEFINITIONS = {
24
21
  { flag: '--name', expectsValue: true },
25
22
  { flag: '-n', expectsValue: true },
26
23
  ],
27
- test: [{ flag: '-t', expectsValue: true }],
24
+ test: [
25
+ { flag: '-t', expectsValue: true },
26
+ { flag: '--name', expectsValue: true },
27
+ { flag: '--test-name-pattern', expectsValue: true },
28
+ ],
28
29
  package: [
29
30
  { flag: '--skip-build' },
30
31
  { flag: '--keep-workdir' },
@@ -55,15 +56,12 @@ export function parseFlags(args = []) {
55
56
  if (!flag.startsWith('-')) continue
56
57
  switch (flag) {
57
58
  case '--dev':
58
- case '--development':
59
59
  flags.dev = true
60
60
  break
61
61
  case '--prod':
62
- case '--production':
63
62
  flags.prod = true
64
63
  break
65
64
  case '--staging':
66
- case '--stage':
67
65
  flags.staging = true
68
66
  break
69
67
  case '--test':
@@ -0,0 +1,217 @@
1
+ const ENVIRONMENT_KEYS = new Set(['development', 'production', 'staging', 'test', 'e2e'])
2
+ const COMMAND_LIST_HIDDEN = new Set(['help'])
3
+ const META_KEYS = new Set(['help', 'description', 'args', 'interactive', 'dangerous'])
4
+ const INTERNAL_CONFIG_KEYS = new Set([
5
+ 'services',
6
+ 'urls',
7
+ 'preflight',
8
+ 'ecosystemConfig',
9
+ 'pm2Bin',
10
+ 'backendDeploy',
11
+ 'artifactDeploy',
12
+ 'telegramWebhook',
13
+ ])
14
+
15
+ export function buildHelpRuntimeContext(cli = {}) {
16
+ return {
17
+ registeredCommands: getRegisteredCommands(cli),
18
+ knownFlags: getKnownFlags(cli),
19
+ }
20
+ }
21
+
22
+ export function getRegisteredCommands(cli = {}) {
23
+ const handlers = cli?.commandHandlers
24
+ if (!handlers || typeof handlers !== 'object') return []
25
+
26
+ return Object.keys(handlers).filter(name => !COMMAND_LIST_HIDDEN.has(name))
27
+ }
28
+
29
+ export function getKnownFlags(cli = {}) {
30
+ const definitions = cli?.flagDefinitions
31
+ const knownFlags = new Map()
32
+
33
+ if (!definitions || typeof definitions !== 'object') return knownFlags
34
+
35
+ for (const entries of Object.values(definitions)) {
36
+ if (!Array.isArray(entries)) continue
37
+ for (const entry of entries) {
38
+ if (!entry?.flag) continue
39
+ knownFlags.set(entry.flag, {
40
+ expectsValue: Boolean(entry.expectsValue),
41
+ })
42
+ }
43
+ }
44
+
45
+ return knownFlags
46
+ }
47
+
48
+ export function classifyCommandNode(node = {}) {
49
+ if (node?.help?.nodeType) return node.help.nodeType
50
+ if (looksLikeInternalConfigBag(node)) return 'internal-config-bag'
51
+ if (node?.command || node?.internal) return 'target-leaf'
52
+ if (node?.concurrent || node?.sequential) return 'orchestration-node'
53
+ if (looksLikeEnvContainer(node)) return 'env-container'
54
+ if (looksLikeCategoryNode(node)) return 'category-node'
55
+ return 'unknown-node'
56
+ }
57
+
58
+ export function isVisibleHelpNode(name, node, nodeType = classifyCommandNode(node)) {
59
+ void name
60
+
61
+ if (node?.help?.expose === false) return false
62
+ if (nodeType === 'internal-config-bag') return false
63
+ if (nodeType === 'category-node') return false
64
+ if (nodeType === 'orchestration-node') return node?.help?.expose === true
65
+ return true
66
+ }
67
+
68
+ export function getGlobalHelpModel(commands = {}, context = {}) {
69
+ const registeredCommands = Array.isArray(context?.registeredCommands)
70
+ ? context.registeredCommands
71
+ : []
72
+
73
+ return {
74
+ summary: commands?.help?.summary ?? '',
75
+ commands: registeredCommands.map(name => getCommandHelpModel(commands, name, context)),
76
+ globalOptions: normalizeArray(commands?.help?.globalOptions),
77
+ examples: normalizeArray(commands?.help?.examples),
78
+ }
79
+ }
80
+
81
+ export function getCommandHelpModel(commands = {}, commandName, context = {}) {
82
+ const commandConfig = commands?.[commandName] ?? {}
83
+ const commandHelp = getCommandHelpConfig(commands, commandName)
84
+
85
+ return {
86
+ name: commandName,
87
+ summary: resolveSummary(commandConfig, commandHelp),
88
+ usage: resolveUsage(commandName, commandHelp, commandConfig),
89
+ args: normalizeArray(commandHelp?.args),
90
+ notes: normalizeArray(commandHelp?.notes),
91
+ examples: normalizeArray(commandHelp?.examples),
92
+ options: normalizeArray(commandHelp?.options),
93
+ targets: resolveVisibleTargets(commands, commandName, commandConfig, context),
94
+ }
95
+ }
96
+
97
+ export function resolveSummary(node = {}, help = null) {
98
+ return help?.summary || node?.help?.summary || node?.description || ''
99
+ }
100
+
101
+ function resolveUsage(commandName, commandHelp = {}, commandConfig = {}) {
102
+ return commandHelp?.usage || generateUsage(commandName, commandConfig)
103
+ }
104
+
105
+ function generateUsage(commandName, commandConfig = {}) {
106
+ switch (commandName) {
107
+ case 'start':
108
+ return 'dx start <service> [环境标志]'
109
+ case 'build':
110
+ case 'package':
111
+ return `dx ${commandName} <target> [环境标志]`
112
+ case 'db':
113
+ return 'dx db <action> [name] [环境标志]'
114
+ case 'test':
115
+ return 'dx test [type] <target> [path]'
116
+ case 'deploy':
117
+ case 'clean':
118
+ case 'cache':
119
+ return `dx ${commandName} <target>`
120
+ case 'help':
121
+ return 'dx help [command]'
122
+ case 'worktree':
123
+ return 'dx worktree [action] [args...]'
124
+ case 'lint':
125
+ case 'status':
126
+ return `dx ${commandName}`
127
+ default:
128
+ return hasVisibleTargets(commandConfig) ? `dx ${commandName} <target>` : `dx ${commandName}`
129
+ }
130
+ }
131
+
132
+ function hasVisibleTargets(commandConfig) {
133
+ if (!isPlainObject(commandConfig)) return false
134
+
135
+ return Object.entries(commandConfig).some(([name, node]) => {
136
+ if (name === 'help') return false
137
+ return isVisibleHelpNode(name, node)
138
+ })
139
+ }
140
+
141
+ function resolveVisibleTargets(commands, commandName, commandConfig, context) {
142
+ void context
143
+
144
+ if (!isPlainObject(commandConfig)) return []
145
+
146
+ return Object.entries(commandConfig)
147
+ .filter(([name]) => name !== 'help')
148
+ .map(([name, node]) => {
149
+ const nodeType = classifyCommandNode(node)
150
+ if (!isVisibleHelpNode(name, node, nodeType)) return null
151
+
152
+ const targetHelp = getTargetHelpConfig(commands, commandName, name)
153
+
154
+ return {
155
+ name,
156
+ nodeType,
157
+ summary: resolveSummary(node, targetHelp),
158
+ notes: normalizeArray(targetHelp?.notes),
159
+ options: normalizeArray(targetHelp?.options),
160
+ examples: normalizeArray(targetHelp?.examples),
161
+ }
162
+ })
163
+ .filter(Boolean)
164
+ }
165
+
166
+ function getCommandHelpConfig(commands, commandName) {
167
+ const config = commands?.help?.commands?.[commandName]
168
+ return isPlainObject(config) ? config : {}
169
+ }
170
+
171
+ function getTargetHelpConfig(commands, commandName, targetName) {
172
+ const config = commands?.help?.targets?.[commandName]?.[targetName]
173
+ return isPlainObject(config) ? config : {}
174
+ }
175
+
176
+ function looksLikeInternalConfigBag(node) {
177
+ if (!isPlainObject(node)) return false
178
+ if (node.command || node.internal || node.concurrent || node.sequential) return false
179
+ if (looksLikeEnvContainer(node)) return false
180
+
181
+ const visibleKeys = getVisibleKeys(node)
182
+ if (visibleKeys.length === 0) return false
183
+
184
+ return visibleKeys.some(key => INTERNAL_CONFIG_KEYS.has(key))
185
+ }
186
+
187
+ function looksLikeEnvContainer(node) {
188
+ if (!isPlainObject(node)) return false
189
+ const visibleKeys = getVisibleKeys(node)
190
+ if (visibleKeys.length === 0) return false
191
+
192
+ const envKeys = visibleKeys.filter(key => ENVIRONMENT_KEYS.has(key))
193
+ return envKeys.length > 0 && envKeys.length === visibleKeys.length
194
+ }
195
+
196
+ function looksLikeCategoryNode(node) {
197
+ if (!isPlainObject(node)) return false
198
+ if (node.command || node.internal || node.concurrent || node.sequential) return false
199
+ if (looksLikeEnvContainer(node) || looksLikeInternalConfigBag(node)) return false
200
+
201
+ const visibleKeys = getVisibleKeys(node)
202
+ if (visibleKeys.length === 0) return false
203
+
204
+ return visibleKeys.every(key => isPlainObject(node[key]))
205
+ }
206
+
207
+ function getVisibleKeys(node) {
208
+ return Object.keys(node).filter(key => !META_KEYS.has(key))
209
+ }
210
+
211
+ function normalizeArray(value) {
212
+ return Array.isArray(value) ? value : []
213
+ }
214
+
215
+ function isPlainObject(value) {
216
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
217
+ }