@ranger1/dx 0.1.96 → 0.1.98
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 +78 -5
- package/lib/cli/commands/core.js +12 -20
- package/lib/cli/commands/db.js +22 -32
- package/lib/cli/commands/export.js +7 -2
- package/lib/cli/commands/start.js +14 -33
- package/lib/cli/commands/worktree.js +0 -4
- package/lib/cli/dx-cli.js +56 -99
- package/lib/cli/flags.js +0 -6
- package/lib/cli/help-model.js +217 -0
- package/lib/cli/help-renderer.js +137 -0
- package/lib/cli/help-schema.js +549 -0
- package/lib/cli/help.js +114 -292
- package/lib/env.js +4 -5
- package/lib/worktree.js +37 -17
- package/package.json +1 -1
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')
|
|
@@ -160,8 +164,17 @@ class DxCli {
|
|
|
160
164
|
|
|
161
165
|
// 检测并生成 Prisma Client
|
|
162
166
|
async ensurePrismaClient() {
|
|
167
|
+
// 仅对真正安装了 @prisma/client 的项目执行检查。
|
|
168
|
+
// 依据「node_modules/@prisma/client/package.json 是否存在」判断项目是否使用 Prisma,
|
|
169
|
+
// 避免在纯文档站、纯前端等未引入 Prisma 的项目上误触发生成流程,
|
|
170
|
+
// 进而报出形如「未找到 db.generate 命令配置」的误导性错误。
|
|
171
|
+
const prismaClientDir = join(process.cwd(), 'node_modules', '@prisma', 'client')
|
|
172
|
+
if (!existsSync(join(prismaClientDir, 'package.json'))) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
163
176
|
// pnpm 结构下检测 @prisma/client 生成的 default.js 文件
|
|
164
|
-
const prismaClientPath = join(
|
|
177
|
+
const prismaClientPath = join(prismaClientDir, 'default.js')
|
|
165
178
|
|
|
166
179
|
if (!existsSync(prismaClientPath)) {
|
|
167
180
|
const environment = this.determineEnvironment()
|
|
@@ -172,22 +185,9 @@ class DxCli {
|
|
|
172
185
|
throw new Error('未找到 db.generate 命令配置,请检查 dx/config/commands.json')
|
|
173
186
|
}
|
|
174
187
|
|
|
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
188
|
await execManager.executeCommand(generateConfig.command, {
|
|
189
189
|
app: generateConfig.app || 'backend',
|
|
190
|
-
flags:
|
|
190
|
+
flags: this.createExecutionFlags(environment),
|
|
191
191
|
// Prisma generate 不应卡在环境变量校验上
|
|
192
192
|
skipEnvValidation: true,
|
|
193
193
|
})
|
|
@@ -286,9 +286,9 @@ class DxCli {
|
|
|
286
286
|
// 显示帮助
|
|
287
287
|
if (this.flags.help || !this.command) {
|
|
288
288
|
if (this.flags.help && this.command && this.command !== 'help') {
|
|
289
|
-
showCommandHelp(this.command)
|
|
289
|
+
showCommandHelp(this.command, this)
|
|
290
290
|
} else {
|
|
291
|
-
showHelp()
|
|
291
|
+
showHelp(this)
|
|
292
292
|
}
|
|
293
293
|
return
|
|
294
294
|
}
|
|
@@ -298,7 +298,7 @@ class DxCli {
|
|
|
298
298
|
// Fail fast for unknown commands before dependency/env startup checks.
|
|
299
299
|
if (this.command && !this.commandHandlers[this.command]) {
|
|
300
300
|
logger.error(`未知命令: ${this.command}`)
|
|
301
|
-
showHelp()
|
|
301
|
+
showHelp(this)
|
|
302
302
|
process.exit(1)
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -346,7 +346,7 @@ class DxCli {
|
|
|
346
346
|
const [, ...subArgs] = cleanArgs
|
|
347
347
|
|
|
348
348
|
if (!command) {
|
|
349
|
-
showHelp()
|
|
349
|
+
showHelp(this)
|
|
350
350
|
return
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -504,14 +504,14 @@ class DxCli {
|
|
|
504
504
|
case 'worktree': {
|
|
505
505
|
if (positionalArgs.length === 0) return
|
|
506
506
|
const action = positionalArgs[0]
|
|
507
|
-
if (
|
|
507
|
+
if (action === 'del') {
|
|
508
508
|
return
|
|
509
509
|
}
|
|
510
|
-
if (
|
|
510
|
+
if (action === 'make') {
|
|
511
511
|
ensureMax(3)
|
|
512
512
|
break
|
|
513
513
|
}
|
|
514
|
-
if (
|
|
514
|
+
if (action === 'list' || action === 'clean') {
|
|
515
515
|
ensureMax(1)
|
|
516
516
|
break
|
|
517
517
|
}
|
|
@@ -550,11 +550,8 @@ class DxCli {
|
|
|
550
550
|
const value = String(token).toLowerCase()
|
|
551
551
|
return (
|
|
552
552
|
value === 'dev' ||
|
|
553
|
-
value === 'development' ||
|
|
554
553
|
value === 'prod' ||
|
|
555
|
-
value === 'production' ||
|
|
556
554
|
value === 'staging' ||
|
|
557
|
-
value === 'stage' ||
|
|
558
555
|
value === 'test' ||
|
|
559
556
|
value === 'e2e'
|
|
560
557
|
)
|
|
@@ -570,18 +567,19 @@ class DxCli {
|
|
|
570
567
|
if (suggestion) {
|
|
571
568
|
logger.info(`建议命令: ${suggestion}`)
|
|
572
569
|
} else if (normalizedFlag) {
|
|
573
|
-
|
|
570
|
+
logger.info(`示例: ${this.invocation} ${command} ... ${normalizedFlag}`)
|
|
574
571
|
}
|
|
575
572
|
logger.info('未显式指定环境时将默认使用 --dev。')
|
|
576
573
|
process.exit(1)
|
|
577
574
|
}
|
|
578
575
|
|
|
579
576
|
getEnvironmentFlagExample(token) {
|
|
580
|
-
|
|
581
|
-
switch (key) {
|
|
577
|
+
switch (String(token || '').toLowerCase()) {
|
|
582
578
|
case 'dev':
|
|
579
|
+
case 'development':
|
|
583
580
|
return '--dev'
|
|
584
581
|
case 'prod':
|
|
582
|
+
case 'production':
|
|
585
583
|
return '--prod'
|
|
586
584
|
case 'staging':
|
|
587
585
|
return '--staging'
|
|
@@ -634,13 +632,13 @@ class DxCli {
|
|
|
634
632
|
}
|
|
635
633
|
|
|
636
634
|
validateStartPositionals(positionalArgs) {
|
|
637
|
-
const service = positionalArgs[0] || '
|
|
635
|
+
const service = positionalArgs[0] || 'development'
|
|
638
636
|
const environment = this.determineEnvironment()
|
|
639
637
|
const envKey = this.normalizeEnvKey(environment)
|
|
640
638
|
|
|
641
|
-
if (service === '
|
|
639
|
+
if (service === 'development' && envKey !== 'development') {
|
|
642
640
|
logger.error('dx start 在未指定服务时仅允许使用开发环境')
|
|
643
|
-
logger.info(`示例: ${this.invocation} start
|
|
641
|
+
logger.info(`示例: ${this.invocation} start --dev`)
|
|
644
642
|
logger.info(`示例: ${this.invocation} start backend --prod`)
|
|
645
643
|
process.exit(1)
|
|
646
644
|
}
|
|
@@ -649,7 +647,7 @@ class DxCli {
|
|
|
649
647
|
if (!startConfig) return
|
|
650
648
|
|
|
651
649
|
// 对 start 下的单层 command 配置,默认视为开发态目标,仅允许 --dev。
|
|
652
|
-
if (startConfig.command && envKey !== '
|
|
650
|
+
if (startConfig.command && envKey !== 'development') {
|
|
653
651
|
logger.error(`启动目标 ${service} 仅支持开发环境`)
|
|
654
652
|
logger.info(`示例: ${this.invocation} start ${service} --dev`)
|
|
655
653
|
process.exit(1)
|
|
@@ -669,13 +667,13 @@ class DxCli {
|
|
|
669
667
|
const buildConfig = this.commands?.build?.[target]
|
|
670
668
|
if (!buildConfig || buildConfig.command) return
|
|
671
669
|
|
|
672
|
-
const supportsCurrentEnv = Boolean(buildConfig[envKey]
|
|
670
|
+
const supportsCurrentEnv = Boolean(buildConfig[envKey])
|
|
673
671
|
if (supportsCurrentEnv) return
|
|
674
672
|
|
|
675
673
|
const envFlag = this.getEnvironmentFlagExample(envKey) || `--${envKey}`
|
|
676
674
|
logger.error(`构建目标 ${target} 不支持 ${envFlag} 环境`)
|
|
677
675
|
logger.info('显式传入环境标志时,必须是该 target 实际支持的环境。')
|
|
678
|
-
const available = ['
|
|
676
|
+
const available = ['development', 'staging', 'production', 'test', 'e2e']
|
|
679
677
|
.filter(key => key in buildConfig)
|
|
680
678
|
.map(key => this.getEnvironmentFlagExample(key) || `--${key}`)
|
|
681
679
|
if (available.length > 0) {
|
|
@@ -688,19 +686,6 @@ class DxCli {
|
|
|
688
686
|
process.exit(1)
|
|
689
687
|
}
|
|
690
688
|
|
|
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
689
|
reportExtraPositionals(command, extras) {
|
|
705
690
|
const list = extras.join(', ')
|
|
706
691
|
if (command === '全局') {
|
|
@@ -772,7 +757,7 @@ class DxCli {
|
|
|
772
757
|
continue
|
|
773
758
|
}
|
|
774
759
|
commands.push({
|
|
775
|
-
command:
|
|
760
|
+
command: config.command,
|
|
776
761
|
options: {
|
|
777
762
|
app: config.app,
|
|
778
763
|
ports: config.ports,
|
|
@@ -820,22 +805,11 @@ class DxCli {
|
|
|
820
805
|
if (environment && config) {
|
|
821
806
|
const envKey = this.normalizeEnvKey(environment)
|
|
822
807
|
if (config[envKey]) config = config[envKey]
|
|
823
|
-
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
824
808
|
}
|
|
825
809
|
|
|
826
810
|
return config
|
|
827
811
|
}
|
|
828
812
|
|
|
829
|
-
// SDK 构建命令当前不再暴露 --online/--offline 模式,保留该方法仅为兼容旧调用
|
|
830
|
-
applySdkModeFlags(command) {
|
|
831
|
-
return command
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// 向后兼容的别名
|
|
835
|
-
applySdkOfflineFlag(command) {
|
|
836
|
-
return command
|
|
837
|
-
}
|
|
838
|
-
|
|
839
813
|
collectStartPorts(service, startConfig, envKey) {
|
|
840
814
|
const portSet = new Set()
|
|
841
815
|
|
|
@@ -845,15 +819,6 @@ class DxCli {
|
|
|
845
819
|
})
|
|
846
820
|
}
|
|
847
821
|
|
|
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
822
|
return Array.from(portSet)
|
|
858
823
|
}
|
|
859
824
|
|
|
@@ -936,28 +901,6 @@ class DxCli {
|
|
|
936
901
|
}
|
|
937
902
|
|
|
938
903
|
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
904
|
|
|
962
905
|
const options = {
|
|
963
906
|
app: config.app,
|
|
@@ -969,7 +912,7 @@ class DxCli {
|
|
|
969
912
|
forcePortCleanup: Boolean(config.forcePortCleanup),
|
|
970
913
|
}
|
|
971
914
|
|
|
972
|
-
await execManager.executeCommand(
|
|
915
|
+
await execManager.executeCommand(rawCommand, options)
|
|
973
916
|
}
|
|
974
917
|
|
|
975
918
|
// 确定环境
|
|
@@ -977,17 +920,14 @@ class DxCli {
|
|
|
977
920
|
return envManager.detectEnvironment(this.flags)
|
|
978
921
|
}
|
|
979
922
|
|
|
980
|
-
// 规范化环境键到命令配置使用的命名(
|
|
923
|
+
// 规范化环境键到命令配置使用的命名(development/staging/production/test/e2e)
|
|
981
924
|
normalizeEnvKey(env) {
|
|
982
925
|
switch (String(env || '').toLowerCase()) {
|
|
983
926
|
case 'development':
|
|
984
|
-
|
|
985
|
-
return 'dev'
|
|
927
|
+
return 'development'
|
|
986
928
|
case 'production':
|
|
987
|
-
|
|
988
|
-
return 'prod'
|
|
929
|
+
return 'production'
|
|
989
930
|
case 'staging':
|
|
990
|
-
case 'stage':
|
|
991
931
|
return 'staging'
|
|
992
932
|
case 'test':
|
|
993
933
|
return 'test'
|
|
@@ -998,6 +938,23 @@ class DxCli {
|
|
|
998
938
|
}
|
|
999
939
|
}
|
|
1000
940
|
|
|
941
|
+
createExecutionFlags(environment) {
|
|
942
|
+
const envKey = this.normalizeEnvKey(environment)
|
|
943
|
+
const execFlags = { ...this.flags }
|
|
944
|
+
|
|
945
|
+
;['dev', 'prod', 'staging', 'test', 'e2e'].forEach(key => {
|
|
946
|
+
delete execFlags[key]
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
if (envKey === 'production') execFlags.prod = true
|
|
950
|
+
else if (envKey === 'development') execFlags.dev = true
|
|
951
|
+
else if (envKey === 'test') execFlags.test = true
|
|
952
|
+
else if (envKey === 'e2e') execFlags.e2e = true
|
|
953
|
+
else if (envKey === 'staging') execFlags.staging = true
|
|
954
|
+
|
|
955
|
+
return execFlags
|
|
956
|
+
}
|
|
957
|
+
|
|
1001
958
|
}
|
|
1002
959
|
|
|
1003
960
|
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' },
|
|
@@ -59,15 +56,12 @@ export function parseFlags(args = []) {
|
|
|
59
56
|
if (!flag.startsWith('-')) continue
|
|
60
57
|
switch (flag) {
|
|
61
58
|
case '--dev':
|
|
62
|
-
case '--development':
|
|
63
59
|
flags.dev = true
|
|
64
60
|
break
|
|
65
61
|
case '--prod':
|
|
66
|
-
case '--production':
|
|
67
62
|
flags.prod = true
|
|
68
63
|
break
|
|
69
64
|
case '--staging':
|
|
70
|
-
case '--stage':
|
|
71
65
|
flags.staging = true
|
|
72
66
|
break
|
|
73
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
|
+
}
|