| /** | |
| * Command Implementation | |
| * ====================== | |
| * | |
| * Internal implementation details for CLI commands. | |
| * Public API is in ../Command.ts | |
| */ | |
| import * as Arr from "../../../Array.ts" | |
| import * as Context from "../../../Context.ts" | |
| import * as Effect from "../../../Effect.ts" | |
| import * as Effectable from "../../../Effectable.ts" | |
| import * as Option from "../../../Option.ts" | |
| import * as Predicate from "../../../Predicate.ts" | |
| import * as CliError from "../CliError.ts" | |
| import type * as GlobalFlag from "../GlobalFlag.ts" | |
| import type { ArgDoc, ExampleDoc, FlagDoc, HelpDoc, SubcommandGroupDoc } from "../HelpDoc.ts" | |
| import * as Param from "../Param.ts" | |
| import * as Primitive from "../Primitive.ts" | |
| import { type ConfigInternal, emptyConfig, reconstructTree } from "./config.ts" | |
| /* ========================================================================== */ | |
| /* Types */ | |
| /* ========================================================================== */ | |
| import type { Command, CommandContext, Environment, ParsedTokens } from "../Command.ts" | |
| interface SubcommandGroup { | |
| readonly group: string | undefined | |
| readonly commands: Arr.NonEmptyReadonlyArray<Command<any, unknown, any, unknown, unknown>> | |
| } | |
| /** | |
| * Internal implementation interface with all the machinery. | |
| * Use toImpl() to access from internal code. | |
| */ | |
| export interface CommandInternal<Name extends string, Input, E, R, ContextInput> | |
| extends Command<Name, Input, ContextInput, E, R> | |
| { | |
| readonly config: ConfigInternal | |
| readonly contextConfig: ConfigInternal | |
| readonly service: Context.Key<CommandContext<Name>, ContextInput> | |
| readonly annotations: Context.Context<never> | |
| readonly globalFlags: ReadonlyArray<GlobalFlag.GlobalFlag<any>> | |
| readonly parse: (input: ParsedTokens) => Effect.Effect<Input, CliError.CliError, Environment> | |
| readonly parseContext: (input: ParsedTokens) => Effect.Effect<ContextInput, CliError.CliError, Environment> | |
| readonly handle: ( | |
| input: Input, | |
| commandPath: ReadonlyArray<string> | |
| ) => Effect.Effect<void, E | CliError.CliError, R | Environment> | |
| readonly buildHelpDoc: (commandPath: ReadonlyArray<string>) => HelpDoc | |
| } | |
| /* ========================================================================== */ | |
| /* Type ID */ | |
| /* ========================================================================== */ | |
| export const TypeId = "~effect/cli/Command" as const | |
| /* ========================================================================== */ | |
| /* Casting */ | |
| /* ========================================================================== */ | |
| /** | |
| * Casts a Command to its internal implementation. | |
| * For use by internal modules that need access to config, parse, handle, etc. | |
| */ | |
| export const toImpl = <Name extends string, Input, E, R, ContextInput = {}>( | |
| self: Command<Name, Input, ContextInput, E, R> | |
| ): CommandInternal<Name, Input, E, R, ContextInput> => self as CommandInternal<Name, Input, E, R, ContextInput> | |
| /* ========================================================================== */ | |
| /* Proto */ | |
| /* ========================================================================== */ | |
| export const Proto: Effect.Effect<any, never, any> = { | |
| ...Effectable.Prototype<Command.Any>({ | |
| label: "Command", | |
| evaluate() { | |
| return toImpl(this).service | |
| } | |
| }) | |
| } | |
| /* ========================================================================== */ | |
| /* Constructor */ | |
| /* ========================================================================== */ | |
| /** | |
| * Internal command constructor. Only accepts already-parsed ConfigInternal. | |
| */ | |
| export const makeCommand = <const Name extends string, Input, E, R, ContextInput = {}>(options: { | |
| readonly name: Name | |
| readonly config: ConfigInternal | |
| readonly contextConfig?: ConfigInternal | undefined | |
| readonly service?: Context.Key<CommandContext<Name>, ContextInput> | undefined | |
| readonly annotations?: Context.Context<never> | undefined | |
| readonly globalFlags?: ReadonlyArray<GlobalFlag.GlobalFlag<any>> | undefined | |
| readonly description?: string | undefined | |
| readonly shortDescription?: string | undefined | |
| readonly alias?: string | undefined | |
| readonly hidden?: boolean | undefined | |
| readonly examples?: ReadonlyArray<Command.Example> | undefined | |
| readonly subcommands?: ReadonlyArray<SubcommandGroup> | undefined | |
| readonly parse?: ((input: ParsedTokens) => Effect.Effect<Input, CliError.CliError, Environment>) | undefined | |
| readonly parseContext?: | |
| | ((input: ParsedTokens) => Effect.Effect<ContextInput, CliError.CliError, Environment>) | |
| | undefined | |
| readonly handle?: | |
| | ((input: Input, commandPath: ReadonlyArray<string>) => Effect.Effect<void, E, R | Environment>) | |
| | undefined | |
| }): Command<Name, Input, ContextInput, E, R> => { | |
| const config = options.config | |
| const contextConfig = options.contextConfig ?? emptyConfig | |
| const service = options.service ?? Context.Service<CommandContext<Name>, ContextInput>(`${TypeId}/${options.name}`) | |
| const annotations = options.annotations ?? Context.empty() | |
| const globalFlags = options.globalFlags ?? [] | |
| const subcommands = options.subcommands ?? [] | |
| const handle = ( | |
| input: Input, | |
| commandPath: ReadonlyArray<string> | |
| ): Effect.Effect<void, CliError.CliError | E, R | Environment> => | |
| Predicate.isNotUndefined(options.handle) | |
| ? options.handle(input, commandPath) | |
| : Effect.fail(new CliError.ShowHelp({ commandPath, errors: [] })) | |
| const parse = options.parse ?? makeParser(config) as any | |
| const parseContext = options.parseContext ?? makeParser(contextConfig) as any | |
| const buildHelpDoc = (commandPath: ReadonlyArray<string>): HelpDoc => { | |
| const args: Array<ArgDoc> = [] | |
| const flags: Array<FlagDoc> = [] | |
| for (const arg of config.arguments) { | |
| const singles = Param.extractSingleParams(arg) | |
| const metadata = Param.getParamMetadata(arg) | |
| for (const single of singles) { | |
| args.push({ | |
| name: single.name, | |
| type: single.typeName ?? Primitive.getTypeName(single.primitiveType), | |
| description: single.description, | |
| required: !metadata.isOptional, | |
| variadic: metadata.isVariadic | |
| }) | |
| } | |
| } | |
| let usage = commandPath.length > 0 ? commandPath.join(" ") : options.name | |
| // Only render `<subcommand>` in usage when at least one visible subcommand | |
| // exists; an all-hidden subcommand tree should look like a leaf command. | |
| if (subcommands.some((group) => group.commands.some((c) => !c.hidden))) { | |
| usage += " <subcommand>" | |
| } | |
| usage += " [flags]" | |
| for (const arg of args) { | |
| const argName = arg.variadic ? `<${arg.name}...>` : `<${arg.name}>` | |
| usage += ` ${arg.required ? argName : `[${argName}]`}` | |
| } | |
| for (const option of config.flags) { | |
| const singles = Param.extractSingleParams(option) | |
| for (const single of singles) { | |
| // Hidden flags still parse on the command line but are omitted from | |
| // generated --help output. | |
| if (single.hidden) continue | |
| flags.push(toFlagDoc(single)) | |
| } | |
| } | |
| const subcommandDocs: Array<SubcommandGroupDoc> = [] | |
| for (const group of subcommands) { | |
| // Hidden subcommands still parse on the command line but are omitted | |
| // from --help. Drop the whole group when nothing visible remains so we | |
| // don't render an empty heading. | |
| const visible = group.commands.filter((c) => !c.hidden) | |
| if (visible.length === 0) continue | |
| subcommandDocs.push({ | |
| group: group.group, | |
| commands: Arr.map(visible as unknown as Arr.NonEmptyReadonlyArray<Command.Any>, (subcommand) => ({ | |
| name: subcommand.name, | |
| alias: subcommand.alias, | |
| shortDescription: subcommand.shortDescription, | |
| description: subcommand.description ?? "" | |
| })) | |
| }) | |
| } | |
| const examples: ReadonlyArray<ExampleDoc> = options.examples ?? [] | |
| return { | |
| description: options.description ?? "", | |
| usage, | |
| flags, | |
| annotations, | |
| ...(args.length > 0 && { args }), | |
| ...(subcommandDocs.length > 0 && { subcommands: subcommandDocs }), | |
| ...(examples.length > 0 && { examples }) | |
| } | |
| } | |
| return Object.assign(Object.create(Proto), { | |
| [TypeId]: TypeId, | |
| name: options.name, | |
| examples: options.examples ?? [], | |
| annotations, | |
| globalFlags, | |
| subcommands, | |
| hidden: options.hidden ?? false, | |
| config, | |
| contextConfig, | |
| service, | |
| parse, | |
| parseContext, | |
| handle, | |
| buildHelpDoc, | |
| ...(Predicate.isNotUndefined(options.description) | |
| ? { description: options.description } | |
| : {}), | |
| ...(Predicate.isNotUndefined(options.shortDescription) | |
| ? { shortDescription: options.shortDescription } | |
| : {}), | |
| ...(Predicate.isNotUndefined(options.alias) | |
| ? { alias: options.alias } | |
| : {}) | |
| }) | |
| } | |
| /* ========================================================================== */ | |
| /* Helpers */ | |
| /* ========================================================================== */ | |
| /** | |
| * Converts a single flag param into a FlagDoc for help display. | |
| */ | |
| export const toFlagDoc = (single: Param.Single<typeof Param.flagKind, unknown>): FlagDoc => { | |
| const formattedAliases = single.aliases.map((alias) => alias.length === 1 ? `-${alias}` : `--${alias}`) | |
| return { | |
| name: single.name, | |
| aliases: formattedAliases, | |
| type: single.typeName ?? Primitive.getTypeName(single.primitiveType), | |
| description: appendChoiceKeys(single.description, Primitive.getChoiceKeys(single.primitiveType)), | |
| required: single.primitiveType._tag !== "Boolean" | |
| } | |
| } | |
| const appendChoiceKeys = ( | |
| description: Option.Option<string>, | |
| choiceKeys: ReadonlyArray<string> | undefined | |
| ): Option.Option<string> => { | |
| if (choiceKeys === undefined || choiceKeys.length === 0) { | |
| return description | |
| } | |
| const choiceSuffix = `(choices: ${choiceKeys.join(", ")})` | |
| return Option.match(description, { | |
| onNone: () => Option.some(choiceSuffix), | |
| onSome: (value) => Option.some(`${value} ${choiceSuffix}`) | |
| }) | |
| } | |
| /** | |
| * Creates a parser for a given config. Used as the default for both `parse` | |
| * and `parseContext`, and also by `withSharedFlags` to avoid constructing a | |
| * full throwaway command. | |
| */ | |
| export const makeParser = (cfg: ConfigInternal) => | |
| Effect.fnUntraced(function*(input: ParsedTokens) { | |
| const parsedArgs: Param.ParsedArgs = { flags: input.flags, arguments: input.arguments } | |
| const values = yield* parseParams(parsedArgs, cfg.orderedParams) | |
| return reconstructTree(cfg.tree, values) | |
| }) | |
| /** | |
| * Parses param values from parsed command arguments into their typed | |
| * representations. | |
| */ | |
| const parseParams: (parsedArgs: Param.ParsedArgs, params: ReadonlyArray<Param.Any>) => Effect.Effect< | |
| ReadonlyArray<unknown>, | |
| CliError.CliError, | |
| Environment | |
| > = Effect.fnUntraced(function*(parsedArgs, params) { | |
| const results: Array<unknown> = [] | |
| let currentArguments = parsedArgs.arguments | |
| for (const option of params) { | |
| const [remainingArguments, parsed] = yield* option.parse({ | |
| flags: parsedArgs.flags, | |
| arguments: currentArguments | |
| }) | |
| results.push(parsed) | |
| currentArguments = remainingArguments | |
| } | |
| return results | |
| }) | |
| /** | |
| * Checks that inherited parent context flags do not reuse names declared by | |
| * child command flags. | |
| * | |
| * When `contextConfig` is supplied, it is used as the inherited flag set; | |
| * otherwise the parent's current context config is checked. | |
| */ | |
| export const checkForDuplicateFlags = <Name extends string, Input, ContextInput, E, R>( | |
| parent: Command<Name, Input, ContextInput, E, R>, | |
| subcommands: ReadonlyArray<Command.Any>, | |
| options?: { | |
| readonly contextConfig?: ConfigInternal | undefined | |
| } | undefined | |
| ): void => { | |
| const parentImpl = toImpl(parent) | |
| const parentOptionNames = new Set<string>() | |
| const extractNames = (flags: ReadonlyArray<Param.Any>): void => { | |
| for (const option of flags) { | |
| const singles = Param.extractSingleParams(option) | |
| for (const single of singles) { | |
| parentOptionNames.add(single.name) | |
| } | |
| } | |
| } | |
| extractNames((options?.contextConfig ?? parentImpl.contextConfig).flags) | |
| for (const subcommand of subcommands) { | |
| const subImpl = toImpl(subcommand as any) | |
| for (const option of subImpl.config.flags) { | |
| const singles = Param.extractSingleParams(option) | |
| for (const single of singles) { | |
| if (parentOptionNames.has(single.name)) { | |
| throw new CliError.DuplicateOption({ | |
| option: single.name, | |
| parentCommand: parent.name, | |
| childCommand: subcommand.name | |
| }) | |
| } | |
| } | |
| } | |
| } | |
| } | |
Xet Storage Details
- Size:
- 13 kB
- Xet hash:
- f9139434511d119fe1c212fbbcd850bed83b98594ba2e0887ff6f1678e045175
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.