import { ARGUMENT_GROUP, type ArgumentDefinition, type ArgumentDefinitions, type ArgumentGroupDefinition, type BooleanArgumentDefinition, type EnumArgumentDefinition, type FlatArgumentDefinitions, type MultiEnumArgumentDefinition, type StringListArgumentDefinition, type KeyValueArgumentDefinition, type NumberArgumentDefinition, type StringArgumentDefinition, } from "./types.ts"; type DistributiveOmit = TValue extends unknown ? Omit : never; type ArgumentOptions = DistributiveOmit< TDefinition, "type" >; type EnumArgumentOptions = DistributiveOmit< EnumArgumentDefinition, "type" | "values" >; type MultiEnumArgumentOptions = DistributiveOmit< MultiEnumArgumentDefinition, "type" | "values" >; /** Create a string argument definition while preserving literal option types for inference. */ export function stringArgument>( options: TOptions, ): StringArgumentDefinition & TOptions; export function stringArgument(): StringArgumentDefinition; export function stringArgument( options: ArgumentOptions = {}, ): StringArgumentDefinition { return { type: "string", ...options }; } /** Create a number argument definition while preserving literal option types for inference. */ export function numberArgument>( options: TOptions, ): NumberArgumentDefinition & TOptions; export function numberArgument(): NumberArgumentDefinition; export function numberArgument( options: ArgumentOptions = {}, ): NumberArgumentDefinition { return { type: "number", ...options }; } /** Create a boolean flag definition while preserving literal option types for inference. */ export function booleanArgument>( options: TOptions, ): BooleanArgumentDefinition & TOptions; export function booleanArgument(): BooleanArgumentDefinition; export function booleanArgument( options: ArgumentOptions = {}, ): BooleanArgumentDefinition { return { type: "boolean", ...options }; } /** Create an enum definition whose readonly values are reflected in handler value types. */ export function enumArgument< const TValues extends readonly string[], const TOptions extends EnumArgumentOptions, >(values: TValues, options: TOptions): EnumArgumentDefinition & TOptions; export function enumArgument( values: TValues, ): EnumArgumentDefinition; export function enumArgument( values: TValues, options: EnumArgumentOptions = {}, ): EnumArgumentDefinition { return { type: "enum", values, ...options }; } /** Create a multi-enum definition whose readonly values are reflected in selected value types. */ export function multiEnumArgument< const TValues extends readonly string[], const TOptions extends MultiEnumArgumentOptions, >(values: TValues, options: TOptions): MultiEnumArgumentDefinition & TOptions; export function multiEnumArgument( values: TValues, ): MultiEnumArgumentDefinition; export function multiEnumArgument( values: TValues, options: MultiEnumArgumentOptions = {}, ): MultiEnumArgumentDefinition { return { type: "multi-enum", values, ...options }; } /** Create a repeatable freeform string-list argument. */ export function stringListArgument< const TOptions extends ArgumentOptions, >(options: TOptions): StringListArgumentDefinition & TOptions; export function stringListArgument(): StringListArgumentDefinition; export function stringListArgument( options: ArgumentOptions = {}, ): StringListArgumentDefinition { return { type: "string-list", ...options }; } /** Create a string key/value argument parsed from `key=value` entries. */ export function keyValueArgument< const TOptions extends ArgumentOptions, >(options: TOptions): KeyValueArgumentDefinition & TOptions; export function keyValueArgument(): KeyValueArgumentDefinition; export function keyValueArgument( options: ArgumentOptions = {}, ): KeyValueArgumentDefinition { return { type: "key-value", ...options }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** Group nested argument definitions while preserving nested handler value inference. */ export function group( args: TDefinitions, options: Pick, "title" | "description"> = {}, ): ArgumentGroupDefinition { return { ...options, args, [ARGUMENT_GROUP]: args, }; } /** Return whether an unknown value is a grouped definition produced by `group()`. */ export function isArgumentGroupDefinition( definition: unknown, ): definition is ArgumentGroupDefinition { return isRecord(definition) && ARGUMENT_GROUP in definition && isRecord(definition.args); } /** Return whether an argument definition map contains at least one grouped definition. */ export function hasArgumentGroups(definitions: ArgumentDefinitions): boolean { return Object.values(definitions).some(isArgumentGroupDefinition); } function groupedKey(prefix: string, name: string): string { if (prefix.length === 0) { return name; } return `${prefix}.${name}`; } export type UnknownGroupedArgumentDefinitionEntry = { readonly key: string; readonly definition: unknown; readonly sourcePath: readonly string[]; }; /** Collect parser-facing paths without erasing duplicate canonical keys. */ export function collectUnknownGroupedArgumentDefinitions( definitions: Readonly>, prefix = "", sourcePrefix: readonly string[] = [], ): readonly UnknownGroupedArgumentDefinitionEntry[] { const entries: UnknownGroupedArgumentDefinitionEntry[] = []; for (const [name, definition] of Object.entries(definitions)) { const key = groupedKey(prefix, name); const sourcePath = [...sourcePrefix, name]; if (isRecord(definition) && ARGUMENT_GROUP in definition && isRecord(definition.args)) { entries.push( ...collectUnknownGroupedArgumentDefinitions(definition.args, key, sourcePath), ); continue; } entries.push({ key, definition, sourcePath }); } return entries; } function assignUniqueDefinition( flattened: Record, key: string, definition: ArgumentDefinition, ): void { if (Object.hasOwn(flattened, key)) { throw new TypeError(`Duplicate canonical argument path ${key}`); } Object.defineProperty(flattened, key, { configurable: true, enumerable: true, value: definition, writable: true, }); } function flattenGroupedArgumentDefinitionsInto( flattened: Record, definitions: ArgumentDefinitions, prefix: string, section?: string, ): void { for (const [name, definition] of Object.entries(definitions)) { const key = groupedKey(prefix, name); if (isArgumentGroupDefinition(definition)) { const ownSection = definition.title ?? key; let nextSection = ownSection; if (section !== undefined) { nextSection = `${section} › ${ownSection}`; } flattenGroupedArgumentDefinitionsInto(flattened, definition.args, key, nextSection); continue; } if (section !== undefined && definition.ui?.section === undefined) { assignUniqueDefinition(flattened, key, { ...definition, ui: { ...definition.ui, section }, }); continue; } assignUniqueDefinition(flattened, key, definition); } } /** Flatten grouped definitions into parser-facing dotted argument names such as `database.host`. */ export function flattenGroupedArgumentDefinitions( definitions: ArgumentDefinitions, prefix = "", ): FlatArgumentDefinitions { const flattened: Record = {}; flattenGroupedArgumentDefinitionsInto(flattened, definitions, prefix); return flattened; } function flattenGroupedArgumentValuesUnchecked( values: Readonly>, definitions: ArgumentDefinitions, prefix: string, ): Record { const flattened: Record = {}; for (const [name, definition] of Object.entries(definitions)) { const key = groupedKey(prefix, name); if (isArgumentGroupDefinition(definition)) { const nested = values[name]; if (isRecord(nested)) { const nestedValues = flattenGroupedArgumentValuesUnchecked( nested, definition.args, key, ); for (const [nestedName, nestedValue] of Object.entries(nestedValues)) { Object.defineProperty(flattened, nestedName, { configurable: true, enumerable: true, value: nestedValue, writable: true, }); } } continue; } let value: unknown; if (Object.hasOwn(values, name)) { value = values[name]; } Object.defineProperty(flattened, key, { configurable: true, enumerable: true, value, writable: true, }); } return flattened; } /** Flatten nested handler values into dotted parser/serializer values using the definition tree. */ export function flattenGroupedArgumentValues( values: Readonly>, definitions: ArgumentDefinitions, prefix = "", ): Record { flattenGroupedArgumentDefinitions(definitions, prefix); return flattenGroupedArgumentValuesUnchecked(values, definitions, prefix); } function expandGroupedArgumentValuesUnchecked( values: Readonly>, definitions: ArgumentDefinitions, prefix: string, ): Record { const expanded: Record = {}; for (const [name, definition] of Object.entries(definitions)) { const key = groupedKey(prefix, name); let value: unknown; if (Object.hasOwn(values, key)) { value = values[key]; } if (isArgumentGroupDefinition(definition)) { value = expandGroupedArgumentValuesUnchecked(values, definition.args, key); } Object.defineProperty(expanded, name, { configurable: true, enumerable: true, value, writable: true, }); } return expanded; } /** Expand dotted parser values back into nested handler values using the definition tree. */ export function expandGroupedArgumentValues( values: Readonly>, definitions: ArgumentDefinitions, prefix = "", ): Record { flattenGroupedArgumentDefinitions(definitions, prefix); return expandGroupedArgumentValuesUnchecked(values, definitions, prefix); }