import type { Command } from "commander"; import { sanitizeForLog } from "../../terminal/ansi.js"; import type { NamedCommandDescriptor } from "./command-group-descriptors.js"; export type CommandDescriptorLike = Pick; const SAFE_COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/; export type CommandDescriptorCatalog = { descriptors: readonly TDescriptor[]; getDescriptors: () => readonly TDescriptor[]; getNames: () => string[]; getCommandsWithSubcommands: () => string[]; }; export function normalizeCommandDescriptorName(name: string): string | null { const normalized = name.trim(); return SAFE_COMMAND_NAME_PATTERN.test(normalized) ? normalized : null; } function assertSafeCommandDescriptorName(name: string): string { const normalized = normalizeCommandDescriptorName(name); if (!normalized) { throw new Error(`Invalid CLI command name: ${JSON.stringify(name.trim())}`); } return normalized; } export function sanitizeCommandDescriptorDescription(description: string): string { return sanitizeForLog(description).trim(); } export function getCommandDescriptorNames(descriptors: readonly CommandDescriptorLike[]): string[] { return descriptors.map((descriptor) => descriptor.name); } export function getCommandsWithSubcommands( descriptors: readonly NamedCommandDescriptor[], ): string[] { return descriptors .filter((descriptor) => descriptor.hasSubcommands) .map((descriptor) => descriptor.name); } export function collectUniqueCommandDescriptors( descriptorGroups: readonly (readonly TDescriptor[])[], ): TDescriptor[] { const seen = new Set(); const descriptors: TDescriptor[] = []; for (const group of descriptorGroups) { for (const descriptor of group) { if (seen.has(descriptor.name)) { continue; } seen.add(descriptor.name); descriptors.push(descriptor); } } return descriptors; } export function defineCommandDescriptorCatalog( descriptors: readonly TDescriptor[], ): CommandDescriptorCatalog { return { descriptors, getDescriptors: () => descriptors, getNames: () => getCommandDescriptorNames(descriptors), getCommandsWithSubcommands: () => getCommandsWithSubcommands(descriptors), }; } export function addCommandDescriptorsToProgram( program: Command, descriptors: readonly CommandDescriptorLike[], existingCommands: Set = new Set(), ): Set { for (const descriptor of descriptors) { const name = assertSafeCommandDescriptorName(descriptor.name); if (existingCommands.has(name)) { continue; } program.command(name).description(sanitizeCommandDescriptorDescription(descriptor.description)); existingCommands.add(name); } return existingCommands; }