@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.
@@ -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
+ }