@ranger1/dx 0.1.96 → 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/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 +46 -98
- package/lib/cli/flags.js +0 -6
- package/lib/cli/help-model.js +217 -0
- package/lib/cli/help-renderer.js +135 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
function normalizeArray(value) {
|
|
2
|
+
return Array.isArray(value) ? value : []
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function pushSection(lines, title, entries = []) {
|
|
6
|
+
const normalizedEntries = entries.filter(Boolean)
|
|
7
|
+
if (normalizedEntries.length === 0) return
|
|
8
|
+
|
|
9
|
+
lines.push('', title, ...normalizedEntries)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatExample(example = {}, indent = ' ') {
|
|
13
|
+
const command = typeof example.command === 'string' ? example.command : ''
|
|
14
|
+
const description = typeof example.description === 'string' ? example.description : ''
|
|
15
|
+
|
|
16
|
+
if (!command) return ''
|
|
17
|
+
if (!description) return `${indent}${command}`
|
|
18
|
+
return `${indent}${command} # ${description}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatOption(option = {}, indent = ' ') {
|
|
22
|
+
const flags = normalizeArray(option.flags).filter(Boolean).join(', ')
|
|
23
|
+
const description = typeof option.description === 'string' ? option.description : ''
|
|
24
|
+
|
|
25
|
+
if (!flags && !description) return ''
|
|
26
|
+
if (!description) return `${indent}${flags}`
|
|
27
|
+
return `${indent}${flags} ${description}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getVisibleTargets(targets = []) {
|
|
31
|
+
return normalizeArray(targets).filter(target => target?.nodeType !== 'orchestration-node')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatTarget(target = {}, width = 0) {
|
|
35
|
+
const name = typeof target.name === 'string' ? target.name : ''
|
|
36
|
+
const summary = typeof target.summary === 'string' ? target.summary : ''
|
|
37
|
+
const lines = []
|
|
38
|
+
|
|
39
|
+
if (!name) return lines
|
|
40
|
+
|
|
41
|
+
lines.push(summary ? ` ${name.padEnd(width)} ${summary}` : ` ${name}`)
|
|
42
|
+
|
|
43
|
+
normalizeArray(target.notes).forEach(note => {
|
|
44
|
+
if (!note) return
|
|
45
|
+
lines.push(` 提示: ${note}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
normalizeArray(target.options).forEach(option => {
|
|
49
|
+
const rendered = formatOption(option, ' 选项: ')
|
|
50
|
+
if (rendered) lines.push(rendered)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
normalizeArray(target.examples).forEach(example => {
|
|
54
|
+
const rendered = formatExample(example, ' 示例: ')
|
|
55
|
+
if (rendered) lines.push(rendered)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return lines
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function renderGlobalHelp(model = {}) {
|
|
62
|
+
const lines = []
|
|
63
|
+
const invocation = model.invocation || 'dx'
|
|
64
|
+
const title = [model.title, model.summary].filter(Boolean).join(' - ') || model.title || model.summary
|
|
65
|
+
const commands = normalizeArray(model.commands)
|
|
66
|
+
const commandWidth = Math.max(0, ...commands.map(command => String(command?.name || '').length))
|
|
67
|
+
|
|
68
|
+
if (title) {
|
|
69
|
+
lines.push('', title)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pushSection(lines, '用法:', [` ${invocation} <命令> [选项] [参数...]`])
|
|
73
|
+
pushSection(
|
|
74
|
+
lines,
|
|
75
|
+
'命令:',
|
|
76
|
+
commands.map(command => {
|
|
77
|
+
const name = String(command?.name || '')
|
|
78
|
+
const summary = String(command?.summary || '')
|
|
79
|
+
if (!name) return ''
|
|
80
|
+
return summary ? ` ${name.padEnd(commandWidth)} ${summary}` : ` ${name}`
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
pushSection(lines, '选项:', normalizeArray(model.globalOptions).map(option => formatOption(option)))
|
|
84
|
+
pushSection(lines, '示例:', normalizeArray(model.examples).map(example => formatExample(example)))
|
|
85
|
+
|
|
86
|
+
return lines.join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderCommandHelp(model = {}) {
|
|
90
|
+
const name = typeof model.name === 'string' ? model.name : ''
|
|
91
|
+
const usage = typeof model.usage === 'string' ? model.usage : ''
|
|
92
|
+
const args = normalizeArray(model.args)
|
|
93
|
+
const notes = normalizeArray(model.notes)
|
|
94
|
+
const options = normalizeArray(model.options)
|
|
95
|
+
const examples = normalizeArray(model.examples)
|
|
96
|
+
const targets = getVisibleTargets(model.targets)
|
|
97
|
+
const targetWidth = Math.max(0, ...targets.map(target => String(target?.name || '').length))
|
|
98
|
+
const lines = []
|
|
99
|
+
|
|
100
|
+
if (name) {
|
|
101
|
+
lines.push('', `${name} 命令用法:`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (usage) {
|
|
105
|
+
lines.push(` ${usage}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (model.summary) {
|
|
109
|
+
pushSection(lines, '摘要:', [` ${model.summary}`])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pushSection(
|
|
113
|
+
lines,
|
|
114
|
+
'参数说明:',
|
|
115
|
+
args.map(arg => {
|
|
116
|
+
const argName = typeof arg.name === 'string' ? arg.name : ''
|
|
117
|
+
const description = typeof arg.description === 'string' ? arg.description : ''
|
|
118
|
+
|
|
119
|
+
if (!argName && !description) return ''
|
|
120
|
+
if (!description) return ` ${argName}`
|
|
121
|
+
return ` ${argName}: ${description}`
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
pushSection(
|
|
126
|
+
lines,
|
|
127
|
+
'可用 target:',
|
|
128
|
+
targets.flatMap(target => formatTarget(target, targetWidth)),
|
|
129
|
+
)
|
|
130
|
+
pushSection(lines, '选项:', options.map(option => formatOption(option)))
|
|
131
|
+
pushSection(lines, '提示:', notes.map(note => (note ? ` ${note}` : '')))
|
|
132
|
+
pushSection(lines, '示例:', examples.map(example => formatExample(example)))
|
|
133
|
+
|
|
134
|
+
return lines.join('\n')
|
|
135
|
+
}
|