import { UnknownOptionConfig, OptionConfigToType, readDefaultValue, LocalizationDictionary, ConfigurationFiles, getEnvKey, } from '@cli-forge/parser'; import { AnyInternalCLI } from './internal-cli'; import { CLI } from './public-api'; export type Documentation = { name: string; description?: string; epilogue?: string; usage: string; examples: string[]; options: Readonly>; positionals: readonly Readonly[]; groupedOptions: Array<{ label: string; keys: Array; }>; subcommands: Documentation[]; /** * Describes how configuration is loaded for this command. * Each section is produced by a provider's `describeConfig` method. */ configurationSources?: ConfigurationFiles.ConfigurationDocSection[]; /** * Localized keys for options and commands. Maps from default key to full localization entry. * Only present if localization is configured. */ localizedKeys?: LocalizationDictionary; }; function resolveEnvKeyForOption( option: UnknownOptionConfig, optionKey: string, envInfo: { prefix?: string; enabled: boolean } ): string | undefined { const { env } = option; if (env === false) return undefined; if (env === undefined && !envInfo.enabled) return undefined; if (env === undefined || env === true) { return getEnvKey(envInfo.prefix, optionKey); } if (typeof env === 'string') { return getEnvKey(envInfo.prefix, env); } // Object form if (env.populate === false) return undefined; const envKey = env.key ?? optionKey; const prefix = env.prefix === false ? undefined : envInfo.prefix; return getEnvKey(prefix, envKey); } function normalizeOptionConfigForDocumentation( option: T, key: string, envInfo?: { prefix?: string; enabled: boolean } ) { const { default: declaredDefault, ...rest } = option; let resolvedDefault: OptionConfigToType | string | undefined; if (declaredDefault !== undefined) { const [defaultValue, description] = readDefaultValue(option); resolvedDefault = description ?? defaultValue; } const result: typeof rest & { key: string; default?: OptionConfigToType | string | undefined; resolvedEnvKey?: string; alias?: string[]; } = { ...rest, key }; // Strip aliases flagged as hidden (either explicitly via `{ hidden: true }` // or auto-generated by the parser) from generated documentation. const hiddenAliases = (option as { hiddenAliases?: string[] }).hiddenAliases; if (hiddenAliases?.length && Array.isArray(result.alias)) { const filtered = result.alias.filter( (alias): alias is string => typeof alias === 'string' && !hiddenAliases.includes(alias) ); if (filtered.length > 0) { result.alias = filtered; } else { delete result.alias; } } if (resolvedDefault !== undefined) { result.default = resolvedDefault; } if (envInfo) { const resolvedEnvKey = resolveEnvKeyForOption(option, key, envInfo); if (resolvedEnvKey !== undefined) { result.resolvedEnvKey = resolvedEnvKey; } } return result; } type NormalizedOptionConfig< T extends UnknownOptionConfig = UnknownOptionConfig > = ReturnType>; export function generateDocumentation( cli: InternalCLI, commandChain: string[] = [] ) { // Ensure current command's options are built. if (cli.configuration?.builder) { // The cli instance here is typed a bit too well // for the builder function, so we need to cast it to // a more generic form. cli.configuration.builder(cli as unknown as CLI); } const parser = cli.getParser(); const groupedOptions = cli.getGroupedOptions(); const envInfo = parser.getEnvInfo(); const options: Record = Object.fromEntries( Object.entries(parser.configuredOptions) .filter(([, c]) => !c.hidden) .map(([k, v]) => [k, normalizeOptionConfigForDocumentation(v, k, envInfo)]) ); const positionals = parser.configuredPositionals; for (const positional of positionals) { delete options[positional.key]; } const subcommands: Documentation[] = []; for (const subcommand of Object.values(cli.getSubcommands())) { if (subcommand.configuration?.hidden !== true) { const clone = subcommand.clone(); if (clone.configuration) { clone.configuration.epilogue ??= cli.configuration?.epilogue; } subcommands.push( generateDocumentation(clone, [...commandChain, cli.name]) ); } } Object.values(cli.getSubcommands()).map((cmd) => generateDocumentation(cmd.clone(), [...commandChain, cli.name]) ); // Get the localization dictionary if configured const dictionary = parser.getLocalizationDictionary(); let localizedKeys: LocalizationDictionary | undefined; if (dictionary) { // Filter to only include keys that are actually used in this CLI const usedKeys: LocalizationDictionary = {}; let hasUsedKeys = false; for (const key in parser.configuredOptions) { if (dictionary[key]) { usedKeys[key] = dictionary[key]; hasUsedKeys = true; } } // Also include command names - track unique commands by instance to avoid duplicates const seenCommands = new Set(); for (const cmdKey in cli.getSubcommands()) { const cmdInstance = cli.getSubcommands()[cmdKey]; if (!seenCommands.has(cmdInstance)) { seenCommands.add(cmdInstance); const defaultName = cmdInstance.name; if (dictionary[defaultName]) { usedKeys[defaultName] = dictionary[defaultName]; hasUsedKeys = true; } } } if (hasUsedKeys) { localizedKeys = usedKeys; } } const result: Documentation = { name: cli.name, description: cli.configuration?.description, usage: cli.configuration?.usage ? commandChain.length ? [...commandChain, cli.configuration.usage].join(' ') : cli.configuration?.usage : [ ...commandChain, cli.name, ...positionals.map((p) => (p.required ? `<${p.key}>` : `[${p.key}]`)), ].join(' '), epilogue: cli.configuration?.epilogue, examples: cli.configuration?.examples ?? [], groupedOptions, options, positionals, subcommands, }; const configDocs = parser.getConfigurationDocs(); if (configDocs.length > 0) { result.configurationSources = configDocs; } if (localizedKeys) { result.localizedKeys = localizedKeys; } return result; }