brocolito
Version:
Create type-safe CLIs to align local development and pipeline workflows
70 lines (69 loc) • 3.96 kB
TypeScript
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 {};