UNPKG

brocolito

Version:

Create type-safe CLIs to align local development and pipeline workflows

70 lines (69 loc) 3.96 kB
import { type CompleteItemOrString } from "./completion/tabtab.ts"; export type SnakeToCamelCase<S extends string> = S extends `${infer T}-${infer U}` ? `${Lowercase<T>}${Capitalize<SnakeToCamelCase<U>>}` : S; type FirstOptionPart<S extends string> = S extends `--${infer T} ${string}` ? T : S extends `--${infer T}` ? T : never; type RemoveMandatoryFlag<S extends string> = S extends `${infer T}!` ? T : S; type RemoveShortOption<S extends string> = S extends `${infer T}|-${string}` ? T : S; export type OptionToName<S extends string> = SnakeToCamelCase<RemoveShortOption<RemoveMandatoryFlag<FirstOptionPart<S>>>>; type RemoveArgumentBrackets<S extends string> = S extends `<${infer T}>` ? T : never; type RemoveArgumentType<S extends string> = S extends `${infer T}:${string}` ? T : S; type RemoveArgumentDots<S extends string> = S extends `${infer T}...` ? T : S; type UnionFromPipes<TType extends string> = TType extends `${infer TFirst}|${infer TRest}` ? TFirst | UnionFromPipes<TRest> : TType; type TypeFromSpec<TSpec extends string> = TSpec extends `${infer T}...` ? TypeFromSpec<T>[] : TSpec extends "file" | "string" ? string : UnionFromPipes<TSpec>; export type ArgumentToName<S extends string> = SnakeToCamelCase<RemoveArgumentType<RemoveArgumentDots<RemoveArgumentBrackets<S>>>>; export type Action<ARGS> = (args: ARGS) => unknown; export type OptionArg<USAGE extends `--${string}`> = { [arg in OptionToName<USAGE>]: USAGE extends `--${string} ${infer T}` ? T extends `<${infer U}>` ? TypeFromSpec<U> | (USAGE extends `--${string}! <${string}>` ? never : undefined) : string | undefined : boolean; }; export type ArgStates = 0 | 1 | 2 | 3; export type CompletionOpt = { completion?: (filter: string) => CompleteItemOrString[] | Promise<CompleteItemOrString[]>; }; type Option<OPTIONS, ARGS, TArgState extends ArgStates> = <USAGE extends `--${string}`>(usage: USAGE, description: string, opts?: CompletionOpt) => Command<OPTIONS & OptionArg<USAGE>, ARGS, TArgState>; export type ArgType = { type: "string" | "file" | string[]; multi: boolean; } & CompletionOpt; export type OptionMeta = { usage: string; name: string; description: string; type: "boolean" | ArgType["type"]; multi: ArgType["multi"]; mandatory: boolean; short?: string; } & CompletionOpt; export type DescriptionOrOpts = string | { description: string; alias?: string; }; export type Subcommand<OPTIONS, ARGS> = (name: string, description: DescriptionOrOpts, sub: (subcommand: Command<OPTIONS>) => void) => Command<OPTIONS, ARGS, 3>; type TypeOfArg<TUsage extends string> = TUsage extends `<${string}:${infer T}>` ? TypeFromSpec<T> : TUsage extends `<${string}...>` ? string[] : string; export type ArgumentArg<USAGE extends `<${string}>`> = { [arg in ArgumentToName<USAGE>]: TypeOfArg<USAGE>; }; type Argument<OPTIONS, ARGS> = <USAGE extends `<${string}${string}>`>(usage: USAGE, description: string, opts?: CompletionOpt) => Command<OPTIONS, ARGS & ArgumentArg<USAGE>, USAGE extends `<${string}...>` ? 2 : 1>; type Arguments<OPTIONS, ARGS> = { arg: Argument<OPTIONS, ARGS>; args: Array<{ name: string; usage: `<${string}${string}>`; description: string; } & ArgType>; }; type CommandAction<OPTIONS, ARGS> = { action: (action: Action<OPTIONS & ARGS>) => void; _action?: Action<OPTIONS & ARGS>; }; type Subcommands<OPTIONS, ARGS> = { subcommand: Subcommand<OPTIONS, ARGS>; subcommands: Record<string, Command<OPTIONS>>; }; export type Command<OPTIONS = {}, ARGS = {}, TArgState extends ArgStates = 0> = { name: string; line: string; description: string; option: Option<OPTIONS, ARGS, TArgState>; options: Record<keyof OPTIONS, OptionMeta>; alias?: string; } & (TArgState extends 3 ? {} : CommandAction<OPTIONS, ARGS>) & (TArgState extends 2 | 3 ? {} : Arguments<OPTIONS, ARGS>) & (TArgState extends 1 | 2 ? {} : Subcommands<OPTIONS, ARGS>); export {};