|
1 | | -import { Command } from 'commander'; |
| 1 | +import { Command, type Argument as CommanderArgument, type Option as CommanderOption } from 'commander'; |
2 | 2 | import yaml from 'js-yaml'; |
3 | 3 | import type { Arg, CliCommand } from './registry.js'; |
4 | 4 | import { fullName } from './registry.js'; |
5 | 5 | import { formatCommandExample } from './serialization.js'; |
6 | 6 |
|
7 | 7 | export type StructuredHelpFormat = 'yaml' | 'json'; |
8 | 8 |
|
| 9 | +export interface ArgSpec { |
| 10 | + name: string; |
| 11 | + required?: true; |
| 12 | + variadic?: true; |
| 13 | + help?: string; |
| 14 | + default?: unknown; |
| 15 | + choices?: string[]; |
| 16 | +} |
| 17 | + |
| 18 | +export interface OptionSpec { |
| 19 | + name: string; |
| 20 | + flags: string; |
| 21 | + help?: string; |
| 22 | + takes_value?: 'required' | 'optional'; |
| 23 | + required?: true; |
| 24 | + default?: unknown; |
| 25 | + choices?: string[]; |
| 26 | + negate?: true; |
| 27 | +} |
| 28 | + |
9 | 29 | const COMMON_OPTIONS = [ |
10 | 30 | { |
11 | 31 | flags: '-f, --format <fmt>', |
@@ -158,6 +178,150 @@ function compactCommonOption(option: typeof COMMON_OPTIONS[number]): Record<stri |
158 | 178 | }; |
159 | 179 | } |
160 | 180 |
|
| 181 | +function compactCommanderArgument(arg: CommanderArgument): ArgSpec { |
| 182 | + return { |
| 183 | + name: arg.name(), |
| 184 | + ...(arg.required ? { required: true } : {}), |
| 185 | + ...(arg.variadic ? { variadic: true } : {}), |
| 186 | + ...(arg.description ? { help: arg.description } : {}), |
| 187 | + ...(arg.defaultValue !== undefined ? { default: arg.defaultValue } : {}), |
| 188 | + ...(arg.argChoices?.length ? { choices: [...arg.argChoices] } : {}), |
| 189 | + }; |
| 190 | +} |
| 191 | + |
| 192 | +function compactCommanderOption(option: CommanderOption): OptionSpec | null { |
| 193 | + if (option.hidden) return null; |
| 194 | + return { |
| 195 | + name: option.attributeName(), |
| 196 | + flags: option.flags, |
| 197 | + ...(option.description ? { help: option.description } : {}), |
| 198 | + ...(option.required ? { takes_value: 'required' as const } : {}), |
| 199 | + ...(option.optional ? { takes_value: 'optional' as const } : {}), |
| 200 | + ...(option.mandatory ? { required: true } : {}), |
| 201 | + ...(option.defaultValue !== undefined ? { default: option.defaultValue } : {}), |
| 202 | + ...(option.argChoices?.length ? { choices: [...option.argChoices] } : {}), |
| 203 | + ...(option.negate ? { negate: true } : {}), |
| 204 | + }; |
| 205 | +} |
| 206 | + |
| 207 | +function compactCommanderOptions(options: readonly CommanderOption[]): OptionSpec[] { |
| 208 | + return options |
| 209 | + .map(compactCommanderOption) |
| 210 | + .filter((option): option is OptionSpec => option !== null); |
| 211 | +} |
| 212 | + |
| 213 | +function commanderPath(command: Command): string[] { |
| 214 | + const parts: string[] = []; |
| 215 | + let current: Command | null = command; |
| 216 | + while (current) { |
| 217 | + const name = current.name(); |
| 218 | + if (name) parts.push(name); |
| 219 | + current = current.parent; |
| 220 | + } |
| 221 | + return parts.reverse(); |
| 222 | +} |
| 223 | + |
| 224 | +function commandPathFromRoot(namespaceRoot: Command, command: Command): string[] { |
| 225 | + const rootPath = commanderPath(namespaceRoot); |
| 226 | + const commandPath = commanderPath(command); |
| 227 | + return commandPath.slice(rootPath.length); |
| 228 | +} |
| 229 | + |
| 230 | +function collectLeafCommands(command: Command): Command[] { |
| 231 | + if (command.commands.length === 0) return [command]; |
| 232 | + return command.commands.flatMap(child => collectLeafCommands(child)); |
| 233 | +} |
| 234 | + |
| 235 | +function formatCommanderPositionals(args: readonly CommanderArgument[]): string { |
| 236 | + return args |
| 237 | + .map(arg => { |
| 238 | + const name = `${arg.name()}${arg.variadic ? '...' : ''}`; |
| 239 | + return arg.required ? `<${name}>` : `[${name}]`; |
| 240 | + }) |
| 241 | + .join(' '); |
| 242 | +} |
| 243 | + |
| 244 | +function formatCommanderUsage( |
| 245 | + command: Command, |
| 246 | + opts: { namespaceRoot?: Command; globalCommand?: Command } = {}, |
| 247 | +): string { |
| 248 | + const path = commanderPath(command).join(' '); |
| 249 | + const positionalText = formatCommanderPositionals(command.registeredArguments); |
| 250 | + const hasOptions = compactCommanderOptions(command.options).length > 0 |
| 251 | + || (opts.namespaceRoot ? compactCommanderOptions(opts.namespaceRoot.options).length > 0 : false) |
| 252 | + || (opts.globalCommand ? compactCommanderOptions(opts.globalCommand.options).length > 0 : false); |
| 253 | + const optionText = hasOptions ? ' [options]' : ''; |
| 254 | + return `${path}${positionalText ? ` ${positionalText}` : ''}${optionText}`; |
| 255 | +} |
| 256 | + |
| 257 | +function compactCommanderCommand( |
| 258 | + namespaceRoot: Command, |
| 259 | + command: Command, |
| 260 | + opts: { globalCommand?: Command } = {}, |
| 261 | +): Record<string, unknown> { |
| 262 | + const relativePath = commandPathFromRoot(namespaceRoot, command); |
| 263 | + return { |
| 264 | + name: relativePath.join(' '), |
| 265 | + command: commanderPath(command).join(' '), |
| 266 | + usage: formatCommanderUsage(command, { namespaceRoot, globalCommand: opts.globalCommand }), |
| 267 | + description: command.description(), |
| 268 | + ...(command.aliases().length ? { aliases: command.aliases() } : {}), |
| 269 | + positionals: command.registeredArguments.map(compactCommanderArgument), |
| 270 | + command_options: compactCommanderOptions(command.options), |
| 271 | + }; |
| 272 | +} |
| 273 | + |
| 274 | +export function commanderNamespaceHelpData( |
| 275 | + namespaceRoot: Command, |
| 276 | + opts: { globalCommand?: Command; description?: string } = {}, |
| 277 | +): Record<string, unknown> { |
| 278 | + const leaves = collectLeafCommands(namespaceRoot) |
| 279 | + .filter(command => command !== namespaceRoot) |
| 280 | + .sort((a, b) => commandPathFromRoot(namespaceRoot, a).join(' ').localeCompare(commandPathFromRoot(namespaceRoot, b).join(' '))); |
| 281 | + return { |
| 282 | + namespace: namespaceRoot.name(), |
| 283 | + command: commanderPath(namespaceRoot).join(' '), |
| 284 | + usage: `${commanderPath(namespaceRoot).join(' ')} <command> [args] [options]`, |
| 285 | + description: opts.description ?? namespaceRoot.description(), |
| 286 | + command_count: leaves.length, |
| 287 | + commands: leaves.map(command => compactCommanderCommand(namespaceRoot, command, opts)), |
| 288 | + namespace_options: compactCommanderOptions(namespaceRoot.options), |
| 289 | + ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}), |
| 290 | + structured_help: { |
| 291 | + formats: ['yaml', 'json'], |
| 292 | + usage: `${commanderPath(namespaceRoot).join(' ')} --help -f yaml`, |
| 293 | + }, |
| 294 | + }; |
| 295 | +} |
| 296 | + |
| 297 | +export function commanderCommandHelpData( |
| 298 | + namespaceRoot: Command, |
| 299 | + command: Command, |
| 300 | + opts: { globalCommand?: Command } = {}, |
| 301 | +): Record<string, unknown> { |
| 302 | + return { |
| 303 | + namespace: namespaceRoot.name(), |
| 304 | + ...compactCommanderCommand(namespaceRoot, command, opts), |
| 305 | + namespace_options: compactCommanderOptions(namespaceRoot.options), |
| 306 | + ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}), |
| 307 | + structured_help: { |
| 308 | + formats: ['yaml', 'json'], |
| 309 | + usage: `${commanderPath(command).join(' ')} --help -f yaml`, |
| 310 | + }, |
| 311 | + }; |
| 312 | +} |
| 313 | + |
| 314 | +export function installCommanderNamespaceStructuredHelp( |
| 315 | + namespaceRoot: Command, |
| 316 | + opts: { globalCommand?: Command; description?: string } = {}, |
| 317 | +): void { |
| 318 | + installStructuredHelp(namespaceRoot, () => commanderNamespaceHelpData(namespaceRoot, opts)); |
| 319 | + for (const command of collectLeafCommands(namespaceRoot)) { |
| 320 | + if (command === namespaceRoot) continue; |
| 321 | + installStructuredHelp(command, () => commanderCommandHelpData(namespaceRoot, command, opts)); |
| 322 | + } |
| 323 | +} |
| 324 | + |
161 | 325 | function positionals(cmd: CliCommand): Arg[] { |
162 | 326 | return cmd.args.filter(arg => arg.positional); |
163 | 327 | } |
|
0 commit comments