|
| 1 | +import type { CommandDef, ArgsDef, PositionalArgDef, CommandMeta } from 'citty'; |
| 2 | + |
| 3 | +type FigSpec = { |
| 4 | + name: string; |
| 5 | + description: string; |
| 6 | + options?: FigOption[]; |
| 7 | + subcommands?: FigSubcommand[]; |
| 8 | + args?: FigArg[]; |
| 9 | +}; |
| 10 | + |
| 11 | +type FigOption = { |
| 12 | + name: string; |
| 13 | + description: string; |
| 14 | + args?: FigArg[]; |
| 15 | + isRequired?: boolean; |
| 16 | +}; |
| 17 | + |
| 18 | +type FigSubcommand = { |
| 19 | + name: string; |
| 20 | + description: string; |
| 21 | + options?: FigOption[]; |
| 22 | + subcommands?: FigSubcommand[]; |
| 23 | + args?: FigArg[]; |
| 24 | +}; |
| 25 | + |
| 26 | +type FigArg = { |
| 27 | + name: string; |
| 28 | + description?: string; |
| 29 | + isOptional?: boolean; |
| 30 | + isVariadic?: boolean; |
| 31 | + suggestions?: FigSuggestion[]; |
| 32 | +}; |
| 33 | + |
| 34 | +type FigSuggestion = { |
| 35 | + name: string; |
| 36 | + description?: string; |
| 37 | +}; |
| 38 | + |
| 39 | +async function processArgs<T extends ArgsDef>( |
| 40 | + args: T |
| 41 | +): Promise<{ options: FigOption[]; args: FigArg[] }> { |
| 42 | + const options: FigOption[] = []; |
| 43 | + const positionalArgs: FigArg[] = []; |
| 44 | + |
| 45 | + for (const [name, arg] of Object.entries(args)) { |
| 46 | + if (arg.type === 'positional') { |
| 47 | + const positionalArg = arg as PositionalArgDef; |
| 48 | + positionalArgs.push({ |
| 49 | + name, |
| 50 | + description: positionalArg.description, |
| 51 | + isOptional: !positionalArg.required, |
| 52 | + // Assume variadic if the name suggests it (e.g. [...files]) |
| 53 | + isVariadic: name.startsWith('[...') || name.startsWith('<...'), |
| 54 | + }); |
| 55 | + } else { |
| 56 | + const option: FigOption = { |
| 57 | + name: `--${name}`, |
| 58 | + description: arg.description || '', |
| 59 | + isRequired: arg.required, |
| 60 | + }; |
| 61 | + |
| 62 | + if ('alias' in arg && arg.alias) { |
| 63 | + // Handle both string and array aliases |
| 64 | + const aliases = Array.isArray(arg.alias) ? arg.alias : [arg.alias]; |
| 65 | + aliases.forEach((alias) => { |
| 66 | + options.push({ |
| 67 | + ...option, |
| 68 | + name: `-${alias}`, |
| 69 | + }); |
| 70 | + }); |
| 71 | + } |
| 72 | + |
| 73 | + options.push(option); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + return { options, args: positionalArgs }; |
| 78 | +} |
| 79 | + |
| 80 | +async function processCommand<T extends ArgsDef>( |
| 81 | + command: CommandDef<T>, |
| 82 | + parentName = '' |
| 83 | +): Promise<FigSpec> { |
| 84 | + const resolvedMeta = await Promise.resolve(command.meta); |
| 85 | + const meta = resolvedMeta as CommandMeta; |
| 86 | + const subCommands = await Promise.resolve(command.subCommands); |
| 87 | + |
| 88 | + if (!meta || !meta.name) { |
| 89 | + throw new Error('Command meta or name is missing'); |
| 90 | + } |
| 91 | + |
| 92 | + const spec: FigSpec = { |
| 93 | + name: parentName ? `${parentName} ${meta.name}` : meta.name, |
| 94 | + description: meta.description || '', |
| 95 | + }; |
| 96 | + |
| 97 | + if (command.args) { |
| 98 | + const resolvedArgs = await Promise.resolve(command.args); |
| 99 | + // Cast to ArgsDef since we know the resolved value will be compatible |
| 100 | + const { options, args } = await processArgs(resolvedArgs as ArgsDef); |
| 101 | + if (options.length > 0) spec.options = options; |
| 102 | + if (args.length > 0) spec.args = args; |
| 103 | + } |
| 104 | + |
| 105 | + if (subCommands) { |
| 106 | + spec.subcommands = await Promise.all( |
| 107 | + Object.entries(subCommands).map(async ([_, subCmd]) => { |
| 108 | + const resolved = await Promise.resolve(subCmd); |
| 109 | + return processCommand(resolved, spec.name); |
| 110 | + }) |
| 111 | + ); |
| 112 | + } |
| 113 | + |
| 114 | + return spec; |
| 115 | +} |
| 116 | + |
| 117 | +export async function generateFigSpec<T extends ArgsDef>( |
| 118 | + command: CommandDef<T> |
| 119 | +): Promise<string> { |
| 120 | + const spec = await processCommand(command); |
| 121 | + return JSON.stringify(spec, null, 2); |
| 122 | +} |
0 commit comments