import type { CommandDef, ArgsDef, PositionalArgDef, CommandMeta } from 'citty'; interface FigSpec { name: string; description: string; options?: FigOption[]; subcommands?: FigSubcommand[]; args?: FigArg[]; } interface FigOption { name: string; description: string; args?: FigArg[]; isRequired?: boolean; } interface FigSubcommand { name: string; description: string; options?: FigOption[]; subcommands?: FigSubcommand[]; args?: FigArg[]; } interface FigArg { name: string; description?: string; isOptional?: boolean; isVariadic?: boolean; suggestions?: FigSuggestion[]; } interface FigSuggestion { name: string; description?: string; } async function processArgs( args: T ): Promise<{ options: FigOption[]; args: FigArg[] }> { const options: FigOption[] = []; const positionalArgs: FigArg[] = []; for (const [name, arg] of Object.entries(args)) { if (arg.type === 'positional') { const positionalArg = arg as PositionalArgDef; positionalArgs.push({ name, description: positionalArg.description, isOptional: !positionalArg.required, // Assume variadic if the name suggests it (e.g. [...files]) isVariadic: name.startsWith('[...') || name.startsWith('<...'), }); } else { const option: FigOption = { name: `--${name}`, description: arg.description || '', isRequired: arg.required, }; if ('alias' in arg && arg.alias) { // Handle both string and array aliases const aliases = Array.isArray(arg.alias) ? arg.alias : [arg.alias]; aliases.forEach((alias) => { options.push({ ...option, name: `-${alias}`, }); }); } options.push(option); } } return { options, args: positionalArgs }; } async function processCommand( command: CommandDef, parentName = '' ): Promise { const resolvedMeta = await Promise.resolve(command.meta); const meta = resolvedMeta as CommandMeta; const subCommands = await Promise.resolve(command.subCommands); if (!meta || !meta.name) { throw new Error('Command meta or name is missing'); } const spec: FigSpec = { name: parentName ? `${parentName} ${meta.name}` : meta.name, description: meta.description || '', }; if (command.args) { const resolvedArgs = await Promise.resolve(command.args); // Cast to ArgsDef since we know the resolved value will be compatible const { options, args } = await processArgs(resolvedArgs as ArgsDef); if (options.length > 0) spec.options = options; if (args.length > 0) spec.args = args; } if (subCommands) { spec.subcommands = await Promise.all( Object.entries(subCommands).map(async ([_, subCmd]) => { const resolved = await Promise.resolve(subCmd); return processCommand(resolved, spec.name); }) ); } return spec; } // TODO: this should be an extension of t.setup function and not something like this. export async function generateFigSpec( command: CommandDef ): Promise { const spec = await processCommand(command); return JSON.stringify(spec, null, 2); }