diff --git a/examples/minigit.ts b/examples/minigit.ts index a936721..57c6c1f 100644 --- a/examples/minigit.ts +++ b/examples/minigit.ts @@ -1,124 +1,66 @@ import * as Args from "@effect/cli/Args" -import * as CliApp from "@effect/cli/CliApp" -import * as Command from "@effect/cli/Command" +import * as Handled from "@effect/cli/Command" import * as Options from "@effect/cli/Options" import * as NodeContext from "@effect/platform-node/NodeContext" import * as Runtime from "@effect/platform-node/Runtime" import * as Console from "effect/Console" -import * as Data from "effect/Data" import * as Effect from "effect/Effect" import { pipe } from "effect/Function" -import type * as HashMap from "effect/HashMap" import * as Option from "effect/Option" import * as ReadonlyArray from "effect/ReadonlyArray" -// ============================================================================= -// Models -// ============================================================================= - -type Subcommand = AddSubcommand | CloneSubcommand - -interface AddSubcommand extends Data.Case { - readonly _tag: "AddSubcommand" - readonly verbose: boolean -} -const AddSubcommand = Data.tagged("AddSubcommand") - -interface CloneSubcommand extends Data.Case { - readonly _tag: "CloneSubcommand" - readonly depth: Option.Option - readonly repository: string - readonly directory: Option.Option -} -const CloneSubcommand = Data.tagged("CloneSubcommand") - -// ============================================================================= -// Commands -// ============================================================================= - // minigit [--version] [-h | --help] [-c =] -const minigitOptions = Options.keyValueMap("c").pipe(Options.optional) -const minigit = Command.make("minigit", { options: minigitOptions }) +const minigit = Handled.make( + "minigit", + { configs: Options.keyValueMap("c").pipe(Options.optional) }, + ({ configs }) => + Option.match(configs, { + onNone: () => Console.log("Running 'minigit'"), + onSome: (configs) => { + const keyValuePairs = Array.from(configs).map(([key, value]) => `${key}=${value}`).join( + ", " + ) + return Console.log(`Running 'minigit' with the following configs: ${keyValuePairs}`) + } + }) +) // minigit add [-v | --verbose] [--] [...] -const minigitAddOptions = Options.boolean("verbose").pipe(Options.withAlias("v")) -const minigitAdd = Command.make("add", { options: minigitAddOptions }).pipe( - Command.map((parsed) => AddSubcommand({ verbose: parsed.options })) -) +const minigitAdd = Handled.make("add", { + verbose: Options.boolean("verbose").pipe(Options.withAlias("v")) +}, ({ verbose }) => Console.log(`Running 'minigit add' with '--verbose ${verbose}'`)) // minigit clone [--depth ] [--] [] -const minigitCloneArgs = Args.all([ - Args.text({ name: "repository" }), - Args.directory().pipe(Args.optional) -]) -const minigitCloneOptions = Options.integer("depth").pipe(Options.optional) -const minigitClone = Command.make("clone", { - options: minigitCloneOptions, - args: minigitCloneArgs -}).pipe(Command.map((parsed) => - CloneSubcommand({ - depth: parsed.options, - repository: parsed.args[0], - directory: parsed.args[1] - }) -)) +const minigitClone = Handled.make("clone", { + repository: Args.text({ name: "repository" }), + directory: Args.directory().pipe(Args.optional), + depth: Options.integer("depth").pipe(Options.optional) +}, ({ depth, directory, repository }) => { + const optionsAndArgs = pipe( + ReadonlyArray.compact([ + Option.map(depth, (depth) => `--depth ${depth}`), + Option.some(repository), + directory + ]), + ReadonlyArray.join(", ") + ) + return Console.log( + `Running 'minigit clone' with the following options and arguments: '${optionsAndArgs}'` + ) +}) -const finalCommand = minigit.pipe(Command.withSubcommands([minigitAdd, minigitClone])) +const finalCommand = minigit.pipe(Handled.withSubcommands([minigitAdd, minigitClone])) // ============================================================================= // Application // ============================================================================= -const cliApp = CliApp.make({ +const run = Handled.run(finalCommand, { name: "MiniGit Distributed Version Control", - version: "v2.42.1", - command: finalCommand -}) - -// ============================================================================= -// Execution -// ============================================================================= - -const handleRootCommand = (configs: Option.Option>) => - Option.match(configs, { - onNone: () => Console.log("Running 'minigit'"), - onSome: (configs) => { - const keyValuePairs = Array.from(configs).map(([key, value]) => `${key}=${value}`).join(", ") - return Console.log(`Running 'minigit' with the following configs: ${keyValuePairs}`) - } - }) - -const handleSubcommand = (subcommand: Subcommand) => { - switch (subcommand._tag) { - case "AddSubcommand": { - return Console.log(`Running 'minigit add' with '--verbose ${subcommand.verbose}'`) - } - case "CloneSubcommand": { - const optionsAndArgs = pipe( - ReadonlyArray.compact([ - Option.map(subcommand.depth, (depth) => `--depth ${depth}`), - Option.some(subcommand.repository), - subcommand.directory - ]), - ReadonlyArray.join(", ") - ) - return Console.log( - `Running 'minigit clone' with the following options and arguments: '${optionsAndArgs}'` - ) - } - } -} - -const program = Effect.gen(function*(_) { - const args = yield* _(Effect.sync(() => globalThis.process.argv.slice(2))) - return yield* _(CliApp.run(cliApp, args, (parsed) => - Option.match(parsed.subcommand, { - onNone: () => handleRootCommand(parsed.options), - onSome: (subcommand) => handleSubcommand(subcommand) - }))) + version: "v2.42.1" }) -program.pipe( +Effect.suspend(() => run(process.argv.slice(2))).pipe( Effect.provide(NodeContext.layer), Runtime.runMain ) diff --git a/examples/naval-fate.ts b/examples/naval-fate.ts index f2c8041..11633ad 100644 --- a/examples/naval-fate.ts +++ b/examples/naval-fate.ts @@ -1,4 +1,4 @@ -import { Args, Command, HandledCommand, Options } from "@effect/cli" +import { Args, Command, Options } from "@effect/cli" import * as KeyValueStore from "@effect/platform-node/KeyValueStore" import * as NodeContext from "@effect/platform-node/NodeContext" import * as Runtime from "@effect/platform-node/Runtime" @@ -32,11 +32,11 @@ const speedOption = Options.integer("speed").pipe( Options.withDefault(10) ) -const shipCommandParent = HandledCommand.makeHelp("ship", { +const shipCommandParent = Command.makeHelp("ship", { verbose: Options.boolean("verbose") }) -const newShipCommand = HandledCommand.make("new", { +const newShipCommand = Command.make("new", { name: nameArg }, ({ name }) => Effect.gen(function*(_) { @@ -48,7 +48,7 @@ const newShipCommand = HandledCommand.make("new", { } })) -const moveShipCommand = HandledCommand.make("move", { +const moveShipCommand = Command.make("move", { ...nameAndCoordinatesArg, speed: speedOption }, ({ name, speed, x, y }) => @@ -57,7 +57,7 @@ const moveShipCommand = HandledCommand.make("move", { yield* _(Console.log(`Moving ship '${name}' to coordinates (${x}, ${y}) at ${speed} knots`)) })) -const shootShipCommand = HandledCommand.make( +const shootShipCommand = Command.make( "shoot", { ...coordinatesArg }, ({ x, y }) => @@ -67,15 +67,15 @@ const shootShipCommand = HandledCommand.make( }) ) -const shipCommand = HandledCommand.withSubcommands(shipCommandParent, [ +const shipCommand = Command.withSubcommands(shipCommandParent, [ newShipCommand, moveShipCommand, shootShipCommand ]) -const mineCommandParent = HandledCommand.makeHelp("mine") +const mineCommandParent = Command.makeHelp("mine") -const setMineCommand = HandledCommand.make("set", { +const setMineCommand = Command.make("set", { ...coordinatesArg, moored: mooredOption }, ({ moored, x, y }) => @@ -86,7 +86,7 @@ const setMineCommand = HandledCommand.make("set", { ) })) -const removeMineCommand = HandledCommand.make("remove", { +const removeMineCommand = Command.make("remove", { ...coordinatesArg }, ({ x, y }) => Effect.gen(function*(_) { @@ -94,16 +94,15 @@ const removeMineCommand = HandledCommand.make("remove", { yield* _(Console.log(`Removing mine at coordinates (${x}, ${y}), if present`)) })) -const mineCommand = HandledCommand.withSubcommands(mineCommandParent, [ +const mineCommand = Command.withSubcommands(mineCommandParent, [ setMineCommand, removeMineCommand ]) -const run = Command.make("naval_fate").pipe( +const run = Command.makeHelp("naval_fate").pipe( Command.withDescription("An implementation of the Naval Fate CLI application."), - HandledCommand.fromCommandHelp, - HandledCommand.withSubcommands([shipCommand, mineCommand]), - HandledCommand.toAppAndRun({ + Command.withSubcommands([shipCommand, mineCommand]), + Command.run({ name: "Naval Fate", version: "1.0.0" }) diff --git a/examples/prompt.ts b/examples/prompt.ts index f1d95b6..4f2e450 100644 --- a/examples/prompt.ts +++ b/examples/prompt.ts @@ -1,5 +1,5 @@ import * as CliApp from "@effect/cli/CliApp" -import * as Command from "@effect/cli/Command" +import * as Command from "@effect/cli/CommandDescriptor" import * as Prompt from "@effect/cli/Prompt" import * as NodeContext from "@effect/platform-node/NodeContext" import * as Runtime from "@effect/platform-node/Runtime" diff --git a/src/BuiltInOptions.ts b/src/BuiltInOptions.ts index bda5ebd..1225123 100644 --- a/src/BuiltInOptions.ts +++ b/src/BuiltInOptions.ts @@ -3,7 +3,7 @@ */ import type { Option } from "effect/Option" -import type { Command } from "./Command.js" +import type { Command } from "./CommandDescriptor.js" import type { HelpDoc } from "./HelpDoc.js" import * as InternalBuiltInOptions from "./internal/builtInOptions.js" import type { Options } from "./Options.js" diff --git a/src/CliApp.ts b/src/CliApp.ts index 69651c2..741dc06 100644 --- a/src/CliApp.ts +++ b/src/CliApp.ts @@ -7,7 +7,7 @@ import type { Path } from "@effect/platform/Path" import type { Terminal } from "@effect/platform/Terminal" import type { Effect } from "effect/Effect" import type { Pipeable } from "effect/Pipeable" -import type { Command } from "./Command.js" +import type { Command } from "./CommandDescriptor.js" import type { HelpDoc } from "./HelpDoc.js" import type { Span } from "./HelpDoc/Span.js" import * as InternalCliApp from "./internal/cliApp.js" diff --git a/src/Command.ts b/src/Command.ts index 8cd5ff2..0d09606 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -1,289 +1,296 @@ /** * @since 1.0.0 */ -import type { FileSystem } from "@effect/platform/FileSystem" -import type { Terminal } from "@effect/platform/Terminal" +import type { Tag } from "effect/Context" import type { Effect } from "effect/Effect" -import type { Either } from "effect/Either" -import type { HashMap } from "effect/HashMap" -import type { HashSet } from "effect/HashSet" import type { Option } from "effect/Option" -import type { Pipeable } from "effect/Pipeable" -import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" +import { type Pipeable } from "effect/Pipeable" +import type * as Types from "effect/Types" import type { Args } from "./Args.js" -import type { CliConfig } from "./CliConfig.js" -import type { CommandDirective } from "./CommandDirective.js" +import type { CliApp } from "./CliApp.js" +import type * as Descriptor from "./CommandDescriptor.js" import type { HelpDoc } from "./HelpDoc.js" -import * as InternalCommand from "./internal/command.js" +import type { Span } from "./HelpDoc/Span.js" +import * as Internal from "./internal/command.js" import type { Options } from "./Options.js" import type { Prompt } from "./Prompt.js" -import type { Usage } from "./Usage.js" import type { ValidationError } from "./ValidationError.js" /** * @since 1.0.0 - * @category symbols + * @category type ids */ -export const CommandTypeId: unique symbol = InternalCommand.CommandTypeId +export const TypeId: unique symbol = Internal.TypeId /** * @since 1.0.0 - * @category symbols + * @category type ids */ -export type CommandTypeId = typeof CommandTypeId +export type TypeId = typeof TypeId /** - * A `Command` represents a command in a command-line application. - * - * Every command-line application will have at least one command: the - * application itself. Other command-line applications may support multiple - * commands. - * * @since 1.0.0 * @category models */ -export interface Command extends Command.Variance, Pipeable {} +export interface Command + extends Pipeable, Effect, never, A> +{ + readonly [TypeId]: TypeId + readonly descriptor: Descriptor.Command + readonly handler: (_: A) => Effect + readonly tag: Tag, A> +} /** * @since 1.0.0 + * @category models */ export declare namespace Command { /** * @since 1.0.0 * @category models */ - export interface Variance { - readonly [CommandTypeId]: { - readonly _A: (_: never) => A - } + export interface ConfigBase { + readonly [key: string]: + | Args + | Options + | ReadonlyArray | Options | ConfigBase> + | ConfigBase } /** * @since 1.0.0 * @category models */ - export interface ConstructorConfig { - readonly options?: Options - readonly args?: Args - } - - /** - * @since 1.0.0 - * @category models - */ - export type ParsedStandardCommand = - Command.ComputeParsedType<{ - readonly name: Name - readonly options: OptionsType - readonly args: ArgsType - }> - - /** - * @since 1.0.0 - * @category models - */ - export type ParsedUserInputCommand = Command.ComputeParsedType<{ - readonly name: Name - readonly value: ValueType - }> + export type ParseConfig = Types.Simplify< + { readonly [Key in keyof A]: ParseConfigValue } + > - /** - * @since 1.0.0 - * @category models - */ - export type ParsedSubcommand> = A[number] extends - Command ? GetParsedType + type ParseConfigValue = A extends ReadonlyArray ? + { readonly [Key in keyof A]: ParseConfigValue } : + A extends Args ? Value + : A extends Options ? Value + : A extends ConfigBase ? ParseConfig : never - /** - * @since 1.0.0 - * @category models - */ - export type GetParsedType = C extends Command ? P : never + interface ParsedConfigTree { + [key: string]: ParsedConfigNode + } - /** - * @since 1.0.0 - * @category models - */ - export type ComputeParsedType = { [K in keyof A]: A[K] } extends infer X ? X : never + type ParsedConfigNode = { + readonly _tag: "Args" + readonly index: number + } | { + readonly _tag: "Options" + readonly index: number + } | { + readonly _tag: "Array" + readonly children: ReadonlyArray + } | { + readonly _tag: "ParsedConfig" + readonly tree: ParsedConfigTree + } /** * @since 1.0.0 * @category models */ - export type Subcommands>> = GetParsedType + export interface ParsedConfig { + readonly args: ReadonlyArray> + readonly options: ReadonlyArray> + readonly tree: ParsedConfigTree + } } /** * @since 1.0.0 - * @category combinators - */ -export const getHelp: (self: Command) => HelpDoc = InternalCommand.getHelp - -/** - * @since 1.0.0 - * @category combinators - */ -export const getBashCompletions: ( - self: Command, - programName: string -) => Effect> = InternalCommand.getBashCompletions - -/** - * @since 1.0.0 - * @category combinators - */ -export const getFishCompletions: ( - self: Command, - programName: string -) => Effect> = InternalCommand.getFishCompletions - -/** - * @since 1.0.0 - * @category combinators - */ -export const getZshCompletions: ( - self: Command, - programName: string -) => Effect> = InternalCommand.getZshCompletions - -/** - * @since 1.0.0 - * @category combinators - */ -export const getNames: (self: Command) => HashSet = InternalCommand.getNames - -/** - * @since 1.0.0 - * @category combinators + * @category constructors */ -export const getSubcommands: (self: Command) => HashMap> = - InternalCommand.getSubcommands +export const fromDescriptor: { + ( + handler: (_: A) => Effect + ): (command: Descriptor.Command) => Command + ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect + ): Command +} = Internal.fromDescriptor /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const getUsage: (self: Command) => Usage = InternalCommand.getUsage +export const fromDescriptorHelp: ( + descriptor: Descriptor.Command +) => Command = Internal.fromDescriptorHelp /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const map: { - (f: (a: A) => B): (self: Command) => Command - (self: Command, f: (a: A) => B): Command -} = InternalCommand.map +export const fromDescriptorUnit: ( + descriptor: Descriptor.Command +) => Command = Internal.fromDescriptorUnit /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const mapOrFail: { - (f: (a: A) => Either): (self: Command) => Command - (self: Command, f: (a: A) => Either): Command -} = InternalCommand.mapOrFail +export const make: ( + name: Name, + config: Config, + handler: (_: Types.Simplify>) => Effect +) => Command< + Name, + R, + E, + Types.Simplify> +> = Internal.make /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const orElse: { - (that: Command): (self: Command) => Command - (self: Command, that: Command): Command -} = InternalCommand.orElse +export const makeHelp: { + (name: Name): Command + ( + name: Name, + config: Config + ): Command< + Name, + never, + ValidationError, + Types.Simplify< + Types.Simplify<{ readonly [Key in keyof Config]: Command.ParseConfigValue }> + > + > +} = Internal.makeHelp /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const orElseEither: { - (that: Command): (self: Command) => Command> - (self: Command, that: Command): Command> -} = InternalCommand.orElseEither +export const makeUnit: { + (name: Name): Command + ( + name: Name, + config: Config + ): Command< + Name, + never, + never, + Types.Simplify< + Types.Simplify<{ readonly [Key in keyof Config]: Command.ParseConfigValue }> + > + > +} = Internal.makeUnit /** * @since 1.0.0 * @category combinators */ -export const parse: { - ( - args: ReadonlyArray, - config: CliConfig - ): ( - self: Command - ) => Effect> +export const mapDescriptor: { ( - self: Command, - args: ReadonlyArray, - config: CliConfig - ): Effect> -} = InternalCommand.parse + f: (_: Descriptor.Command) => Descriptor.Command + ): (self: Command) => Command + ( + self: Command, + f: (_: Descriptor.Command) => Descriptor.Command + ): Command +} = Internal.mapDescriptor /** * @since 1.0.0 * @category constructors */ -export const prompt: ( +export const prompt: ( name: Name, - prompt: Prompt -) => Command<{ readonly name: Name; readonly value: A }> = InternalCommand.prompt - -/** - * @since 1.0.0 - * @category constructors - */ -export const make: ( - name: Name, - config?: Command.ConstructorConfig -) => Command<{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType }> = - InternalCommand.make + prompt: Prompt, + handler: (_: A) => Effect +) => Command = Internal.prompt /** * @since 1.0.0 * @category combinators */ export const withDescription: { - (description: string | HelpDoc): (self: Command) => Command - (self: Command, description: string | HelpDoc): Command -} = InternalCommand.withDescription + ( + help: string | HelpDoc + ): (self: Command) => Command + ( + self: Command, + help: string | HelpDoc + ): Command +} = Internal.withDescription /** * @since 1.0.0 * @category combinators */ export const withSubcommands: { - >>( - subcommands: [...Subcommands] - ): ( - self: Command + < + Subcommand extends readonly [Command, ...Array>] + >( + subcommands: Subcommand + ): ( + self: Command ) => Command< - Command.ComputeParsedType< - A & Readonly<{ subcommand: Option> }> + Name, + | R + | Exclude>, Descriptor.Command>, + E | Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { subcommand: Option> } + > > > - >>( - self: Command, - subcommands: [...Subcommands] + < + Name extends string, + R, + E, + A, + Subcommand extends readonly [Command, ...Array>] + >( + self: Command, + subcommands: Subcommand ): Command< - Command.ComputeParsedType< - A & Readonly<{ subcommand: Option> }> + Name, + | R + | Exclude>, Descriptor.Command>, + E | Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { subcommand: Option> } + > > > -} = InternalCommand.withSubcommands +} = Internal.withSubcommands /** * @since 1.0.0 - * @category combinators + * @category conversions */ -export const wizard: { +export const run: { ( - config: CliConfig - ): ( - self: Command - ) => Effect> - ( - self: Command, - config: CliConfig - ): Effect> -} = InternalCommand.wizard + config: { + readonly name: string + readonly version: string + readonly summary?: Span | undefined + readonly footer?: HelpDoc | undefined + } + ): ( + self: Command + ) => (args: ReadonlyArray) => Effect + ( + self: Command, + config: { + readonly name: string + readonly version: string + readonly summary?: Span | undefined + readonly footer?: HelpDoc | undefined + } + ): (args: ReadonlyArray) => Effect +} = Internal.run diff --git a/src/CommandDescriptor.ts b/src/CommandDescriptor.ts new file mode 100644 index 0000000..5b57fc7 --- /dev/null +++ b/src/CommandDescriptor.ts @@ -0,0 +1,287 @@ +/** + * @since 1.0.0 + */ +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Terminal } from "@effect/platform/Terminal" +import type { Effect } from "effect/Effect" +import type { Either } from "effect/Either" +import type { HashMap } from "effect/HashMap" +import type { HashSet } from "effect/HashSet" +import type { Option } from "effect/Option" +import type { Pipeable } from "effect/Pipeable" +import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" +import type { Args } from "./Args.js" +import type { CliConfig } from "./CliConfig.js" +import type { CommandDirective } from "./CommandDirective.js" +import type { HelpDoc } from "./HelpDoc.js" +import * as Internal from "./internal/commandDescriptor.js" +import type { Options } from "./Options.js" +import type { Prompt } from "./Prompt.js" +import type { Usage } from "./Usage.js" +import type { ValidationError } from "./ValidationError.js" + +/** + * @since 1.0.0 + * @category symbols + */ +export const TypeId: unique symbol = Internal.TypeId + +/** + * @since 1.0.0 + * @category symbols + */ +export type TypeId = typeof TypeId + +/** + * A `Command` represents a command in a command-line application. + * + * Every command-line application will have at least one command: the + * application itself. Other command-line applications may support multiple + * commands. + * + * @since 1.0.0 + * @category models + */ +export interface Command extends Command.Variance, Pipeable {} + +/** + * @since 1.0.0 + */ +export declare namespace Command { + /** + * @since 1.0.0 + * @category models + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: (_: never) => A + } + } + + /** + * @since 1.0.0 + * @category models + */ + export type ParsedStandardCommand = + Command.ComputeParsedType<{ + readonly name: Name + readonly options: OptionsType + readonly args: ArgsType + }> + + /** + * @since 1.0.0 + * @category models + */ + export type ParsedUserInputCommand = Command.ComputeParsedType<{ + readonly name: Name + readonly value: ValueType + }> + + /** + * @since 1.0.0 + * @category models + */ + export type GetParsedType = C extends Command ? P : never + + /** + * @since 1.0.0 + * @category models + */ + export type ComputeParsedType = { [K in keyof A]: A[K] } extends infer X ? X : never + + /** + * @since 1.0.0 + * @category models + */ + export type Subcommands< + A extends NonEmptyReadonlyArray]> + > = A[number] extends readonly [infer Id, Command] ? readonly [id: Id, value: Value] + : never +} + +/** + * @since 1.0.0 + * @category combinators + */ +export const getHelp: (self: Command) => HelpDoc = Internal.getHelp + +/** + * @since 1.0.0 + * @category combinators + */ +export const getBashCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getBashCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getFishCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getFishCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getZshCompletions: ( + self: Command, + programName: string +) => Effect> = Internal.getZshCompletions + +/** + * @since 1.0.0 + * @category combinators + */ +export const getNames: (self: Command) => HashSet = Internal.getNames + +/** + * @since 1.0.0 + * @category combinators + */ +export const getSubcommands: (self: Command) => HashMap> = + Internal.getSubcommands + +/** + * @since 1.0.0 + * @category combinators + */ +export const getUsage: (self: Command) => Usage = Internal.getUsage + +/** + * @since 1.0.0 + * @category combinators + */ +export const map: { + (f: (a: A) => B): (self: Command) => Command + (self: Command, f: (a: A) => B): Command +} = Internal.map + +/** + * @since 1.0.0 + * @category combinators + */ +export const mapOrFail: { + (f: (a: A) => Either): (self: Command) => Command + (self: Command, f: (a: A) => Either): Command +} = Internal.mapOrFail + +/** + * @since 1.0.0 + * @category combinators + */ +export const orElse: { + (that: Command): (self: Command) => Command + (self: Command, that: Command): Command +} = Internal.orElse + +/** + * @since 1.0.0 + * @category combinators + */ +export const orElseEither: { + (that: Command): (self: Command) => Command> + (self: Command, that: Command): Command> +} = Internal.orElseEither + +/** + * @since 1.0.0 + * @category combinators + */ +export const parse: { + ( + args: ReadonlyArray, + config: CliConfig + ): ( + self: Command + ) => Effect> + ( + self: Command, + args: ReadonlyArray, + config: CliConfig + ): Effect> +} = Internal.parse + +/** + * @since 1.0.0 + * @category constructors + */ +export const prompt: ( + name: Name, + prompt: Prompt +) => Command<{ readonly name: Name; readonly value: A }> = Internal.prompt + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + name: Name, + options?: Options, + args?: Args +) => Command<{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType }> = + Internal.make + +/** + * @since 1.0.0 + * @category combinators + */ +export const withDescription: { + (description: string | HelpDoc): (self: Command) => Command + (self: Command, description: string | HelpDoc): Command +} = Internal.withDescription + +/** + * @since 1.0.0 + * @category combinators + */ +export const withSubcommands: { + < + Subcommands extends readonly [ + readonly [id: unknown, command: Command], + ...Array]> + ] + >( + subcommands: [...Subcommands] + ): ( + self: Command + ) => Command< + Command.ComputeParsedType< + A & Readonly<{ subcommand: Option> }> + > + > + < + A, + Subcommands extends readonly [ + readonly [id: unknown, command: Command], + ...Array]> + ] + >( + self: Command, + subcommands: [...Subcommands] + ): Command< + Command.ComputeParsedType< + A & Readonly<{ subcommand: Option> }> + > + > +} = Internal.withSubcommands + +/** + * @since 1.0.0 + * @category combinators + */ +export const wizard: { + ( + config: CliConfig + ): ( + self: Command + ) => Effect> + ( + self: Command, + config: CliConfig + ): Effect> +} = Internal.wizard diff --git a/src/HandledCommand.ts b/src/HandledCommand.ts deleted file mode 100644 index d7644d3..0000000 --- a/src/HandledCommand.ts +++ /dev/null @@ -1,441 +0,0 @@ -/** - * @since 1.0.0 - */ -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import * as Effectable from "effect/Effectable" -import { dual } from "effect/Function" -import { globalValue } from "effect/GlobalValue" -import type * as Option from "effect/Option" -import { type Pipeable, pipeArguments } from "effect/Pipeable" -import * as Predicate from "effect/Predicate" -import * as ReadonlyArray from "effect/ReadonlyArray" -import type * as Types from "effect/Types" -import * as Args from "./Args.js" -import * as CliApp from "./CliApp.js" -import * as Command from "./Command.js" -import type { HelpDoc } from "./HelpDoc.js" -import type { Span } from "./HelpDoc/Span.js" -import * as Options from "./Options.js" -import * as ValidationError from "./ValidationError.js" - -/** - * @since 1.0.0 - * @category type ids - */ -export const TypeId = Symbol.for("@effect/cli/HandledCommand") - -/** - * @since 1.0.0 - * @category type ids - */ -export type TypeId = typeof TypeId - -/** - * @since 1.0.0 - * @category models - */ -export interface HandledCommand - extends Pipeable, Effect.Effect, never, A> -{ - readonly [TypeId]: TypeId - readonly command: Command.Command - readonly handler: (_: A) => Effect.Effect - readonly tag: Context.Tag, A> -} - -/** - * @since 1.0.0 - * @category models - */ -export declare namespace HandledCommand { - /** - * @since 1.0.0 - * @category models - */ - export interface ConfigBase { - readonly [key: string]: - | Args.Args - | Options.Options - | ReadonlyArray | Options.Options | ConfigBase> - | ConfigBase - } - - /** - * @since 1.0.0 - * @category models - */ - export type ParseConfig = Types.Simplify< - { readonly [Key in keyof A]: ParseConfigValue } - > - - type ParseConfigValue = A extends ReadonlyArray ? - { readonly [Key in keyof A]: ParseConfigValue } : - A extends Args.Args ? Value - : A extends Options.Options ? Value - : A extends ConfigBase ? ParseConfig - : never - - interface ParsedConfigTree { - [key: string]: ParsedConfigNode - } - - type ParsedConfigNode = { - readonly _tag: "Args" - readonly index: number - } | { - readonly _tag: "Options" - readonly index: number - } | { - readonly _tag: "Array" - readonly children: ReadonlyArray - } | { - readonly _tag: "ParsedConfig" - readonly tree: ParsedConfigTree - } - - /** - * @since 1.0.0 - * @category models - */ - export interface ParsedConfig { - readonly args: ReadonlyArray> - readonly options: ReadonlyArray> - readonly tree: ParsedConfigTree - } -} - -const parseConfig = (config: HandledCommand.ConfigBase): HandledCommand.ParsedConfig => { - const args: Array> = [] - let argsIndex = 0 - const options: Array> = [] - let optionsIndex = 0 - const tree: HandledCommand.ParsedConfigTree = {} - - function parse(config: HandledCommand.ConfigBase) { - for (const key in config) { - tree[key] = parseValue(config[key]) - } - return tree - } - - function parseValue( - value: - | Args.Args - | Options.Options - | ReadonlyArray | Options.Options | HandledCommand.ConfigBase> - | HandledCommand.ConfigBase - ): HandledCommand.ParsedConfigNode { - if (Array.isArray(value)) { - return { - _tag: "Array", - children: ReadonlyArray.map(value, parseValue) - } - } else if (Args.isArgs(value)) { - args.push(value) - return { - _tag: "Args", - index: argsIndex++ - } - } else if (Options.isOptions(value)) { - options.push(value) - return { - _tag: "Options", - index: optionsIndex++ - } - } else { - return { - _tag: "ParsedConfig", - tree: parse(value as any) - } - } - } - - return { - args, - options, - tree: parse(config) - } -} - -const reconstructConfigTree = ( - tree: HandledCommand.ParsedConfigTree, - args: ReadonlyArray, - options: ReadonlyArray -): Record => { - const output: Record = {} - - for (const key in tree) { - output[key] = nodeValue(tree[key]) - } - - return output - - function nodeValue(node: HandledCommand.ParsedConfigNode): any { - if (node._tag === "Args") { - return args[node.index] - } else if (node._tag === "Options") { - return options[node.index] - } else if (node._tag === "Array") { - return ReadonlyArray.map(node.children, nodeValue) - } else { - return reconstructConfigTree(node.tree, args, options) - } - } -} - -const Prototype = { - ...Effectable.CommitPrototype, - [TypeId]: TypeId, - commit(this: HandledCommand) { - return this.tag - }, - pipe() { - return pipeArguments(this, arguments) - } -} - -const modifiedCommands = globalValue( - "@effect/cli/HandledCommand/modifiedCommands", - () => new WeakMap, Command.Command>() -) - -const getCommand = (self: HandledCommand) => - modifiedCommands.get(self.tag) ?? self.command - -const HandledCommand = ( - command: Command.Command, - handler: (_: A) => Effect.Effect, - tag?: Context.Tag -): HandledCommand => { - const self = Object.create(Prototype) - self.command = Command.map(command, (args) => - Predicate.hasProperty(args, TypeId) ? - args : - new Proxy(args as any, { - get(target, p, _receiver) { - if (p === TypeId) { - return self.tag - } - return target[p as any] - }, - has(target, p) { - return p === TypeId || p in target - } - })) - self.handler = handler - self.tag = tag ?? Context.Tag() - modifiedCommands.set(self.tag, self.command) - return self -} - -/** - * @since 1.0.0 - * @category constructors - */ -export const fromCommand = dual< - ( - handler: (_: A) => Effect.Effect - ) => (command: Command.Command) => HandledCommand, - ( - command: Command.Command, - handler: (_: A) => Effect.Effect - ) => HandledCommand ->(2, (command, handler) => HandledCommand(command, handler)) - -/** - * @since 1.0.0 - * @category constructors - */ -export const fromCommandUnit = ( - command: Command.Command -): HandledCommand => HandledCommand(command, (_) => Effect.unit) - -/** - * @since 1.0.0 - * @category constructors - */ -export const fromCommandHelp = ( - command: Command.Command -): HandledCommand => { - const self: HandledCommand = HandledCommand( - command, - (_) => Effect.fail(ValidationError.helpRequested(getCommand(self))) - ) - return self -} - -const makeCommand = ( - name: string, - config: Config -): Command.Command>> => { - const { args, options, tree } = parseConfig(config) - return Command.map( - Command.make(name, { - args: Args.all(args), - options: Options.all(options) - }), - ({ args, options }) => reconstructConfigTree(tree, args, options) - ) as any -} - -/** - * @since 1.0.0 - * @category constructors - */ -export const make = ( - name: Name, - config: Config, - handler: (_: Types.Simplify>) => Effect.Effect -): HandledCommand< - Name, - Types.Simplify>, - R, - E -> => HandledCommand(makeCommand(name, config), handler) - -/** - * @since 1.0.0 - * @category constructors - */ -export const makeUnit: { - ( - name: Name - ): HandledCommand - ( - name: Name, - config: Config - ): HandledCommand< - Name, - Types.Simplify>, - never, - never - > -} = (name: string, config = {}) => fromCommandUnit(makeCommand(name, config) as any) as any - -/** - * @since 1.0.0 - * @category constructors - */ -export const makeHelp: { - ( - name: Name - ): HandledCommand - ( - name: Name, - config: Config - ): HandledCommand< - Name, - Types.Simplify>, - never, - ValidationError.ValidationError - > -} = (name: string, config = {}) => fromCommandHelp(makeCommand(name, config) as any) as any - -/** - * @since 1.0.0 - * @category combinators - */ -export const withSubcommands = dual< - >>( - subcommands: Subcommand - ) => (self: HandledCommand) => HandledCommand< - Name, - Command.Command.ComputeParsedType< - & A - & Readonly< - { subcommand: Option.Option> } - > - >, - | R - | Exclude< - Effect.Effect.Context>, - Command.Command - >, - E | Effect.Effect.Error> - >, - < - Name extends string, - A, - R, - E, - Subcommand extends ReadonlyArray.NonEmptyReadonlyArray> - >( - self: HandledCommand, - subcommands: Subcommand - ) => HandledCommand< - Name, - Command.Command.ComputeParsedType< - & A - & Readonly< - { subcommand: Option.Option> } - > - >, - | R - | Exclude< - Effect.Effect.Context>, - Command.Command - >, - E | Effect.Effect.Error> - > ->(2, (self, subcommands) => { - const command = Command.withSubcommands( - self.command, - ReadonlyArray.map(subcommands, (_) => _.command) - ) - const handlers = ReadonlyArray.reduce( - subcommands, - new Map, (_: any) => Effect.Effect>(), - (handlers, subcommand) => { - handlers.set(subcommand.tag, subcommand.handler) - return handlers - } - ) - function handler( - args: { - readonly name: string - readonly subcommand: Option.Option<{ readonly name: string }> - } - ) { - if (args.subcommand._tag === "Some" && TypeId in args.subcommand.value) { - return Effect.provideService( - handlers.get(args.subcommand.value[TypeId] as any)!(args.subcommand.value), - self.tag, - args as any - ) - } - return self.handler(args as any) - } - return HandledCommand(command as any, handler, self.tag) as any -}) - -/** - * @since 1.0.0 - * @category combinators - */ -export const toAppAndRun = dual< - (config: { - readonly name: string - readonly version: string - readonly summary?: Span | undefined - readonly footer?: HelpDoc | undefined - }) => ( - self: HandledCommand - ) => ( - args: ReadonlyArray - ) => Effect.Effect, - (self: HandledCommand, config: { - readonly name: string - readonly version: string - readonly summary?: Span | undefined - readonly footer?: HelpDoc | undefined - }) => ( - args: ReadonlyArray - ) => Effect.Effect ->(2, (self, config) => { - const app = CliApp.make({ - ...config, - command: self.command - }) - return (args) => CliApp.run(app, args, self.handler) -}) diff --git a/src/ValidationError.ts b/src/ValidationError.ts index 4923a5d..a3e6dee 100644 --- a/src/ValidationError.ts +++ b/src/ValidationError.ts @@ -2,9 +2,9 @@ * @since 1.0.0 */ import type { BuiltInOptions } from "./BuiltInOptions.js" -import type { Command } from "./Command.js" +import type { Command } from "./CommandDescriptor.js" import type { HelpDoc } from "./HelpDoc.js" -import * as InternalCommand from "./internal/command.js" +import * as InternalCommand from "./internal/commandDescriptor.js" import * as InternalValidationError from "./internal/validationError.js" /** diff --git a/src/index.ts b/src/index.ts index f41ef51..3688e12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,12 +31,12 @@ export * as Command from "./Command.js" /** * @since 1.0.0 */ -export * as CommandDirective from "./CommandDirective.js" +export * as CommandDescriptor from "./CommandDescriptor.js" /** * @since 1.0.0 */ -export * as HandledCommand from "./HandledCommand.js" +export * as CommandDirective from "./CommandDirective.js" /** * @since 1.0.0 diff --git a/src/internal/builtInOptions.ts b/src/internal/builtInOptions.ts index cbdff68..0c7685e 100644 --- a/src/internal/builtInOptions.ts +++ b/src/internal/builtInOptions.ts @@ -1,6 +1,6 @@ import * as Option from "effect/Option" import type * as BuiltInOptions from "../BuiltInOptions.js" -import type * as Command from "../Command.js" +import type * as Command from "../CommandDescriptor.js" import type * as HelpDoc from "../HelpDoc.js" import type * as Options from "../Options.js" import type * as Usage from "../Usage.js" diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts index 8378639..d3699ca 100644 --- a/src/internal/cliApp.ts +++ b/src/internal/cliApp.ts @@ -9,12 +9,12 @@ import * as ReadonlyArray from "effect/ReadonlyArray" import type * as BuiltInOptions from "../BuiltInOptions.js" import type * as CliApp from "../CliApp.js" import type * as CliConfig from "../CliConfig.js" -import type * as Command from "../Command.js" +import type * as Command from "../CommandDescriptor.js" import type * as HelpDoc from "../HelpDoc.js" import type * as Span from "../HelpDoc/Span.js" import type * as ValidationError from "../ValidationError.js" import * as InternalCliConfig from "./cliConfig.js" -import * as InternalCommand from "./command.js" +import * as InternalCommand from "./commandDescriptor.js" import * as InternalHelpDoc from "./helpDoc.js" import * as InternalSpan from "./helpDoc/span.js" import * as InternalUsage from "./usage.js" diff --git a/src/internal/command.ts b/src/internal/command.ts index 472bd25..bb97a76 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -1,1409 +1,387 @@ -import type * as FileSystem from "@effect/platform/FileSystem" -import type * as Terminal from "@effect/platform/Terminal" -import * as Console from "effect/Console" +import * as Context from "effect/Context" import * as Effect from "effect/Effect" -import * as Either from "effect/Either" -import { dual, pipe } from "effect/Function" -import * as HashMap from "effect/HashMap" -import * as HashSet from "effect/HashSet" -import * as Option from "effect/Option" -import * as Order from "effect/Order" +import * as Effectable from "effect/Effectable" +import { dual } from "effect/Function" +import { globalValue } from "effect/GlobalValue" +import type * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" -import * as SynchronizedRef from "effect/SynchronizedRef" +import type * as Types from "effect/Types" import type * as Args from "../Args.js" -import type * as CliConfig from "../CliConfig.js" +import type * as CliApp from "../CliApp.js" import type * as Command from "../Command.js" -import type * as CommandDirective from "../CommandDirective.js" -import * as HelpDoc from "../HelpDoc.js" -import type * as Span from "../HelpDoc/Span.js" -import * as Options from "../Options.js" +import type * as Descriptor from "../CommandDescriptor.js" +import type { HelpDoc } from "../HelpDoc.js" +import type { Span } from "../HelpDoc/Span.js" +import type * as Options from "../Options.js" import type * as Prompt from "../Prompt.js" -import type * as Usage from "../Usage.js" -import type * as ValidationError from "../ValidationError.js" +import * as ValidationError from "../ValidationError.js" import * as InternalArgs from "./args.js" -import * as InternalBuiltInOptions from "./builtInOptions.js" -import * as InternalCliConfig from "./cliConfig.js" -import * as InternalCommandDirective from "./commandDirective.js" -import * as InternalHelpDoc from "./helpDoc.js" -import * as InternalSpan from "./helpDoc/span.js" +import * as InternalCliApp from "./cliApp.js" +import * as InternalDescriptor from "./commandDescriptor.js" import * as InternalOptions from "./options.js" -import * as InternalPrompt from "./prompt.js" -import * as InternalSelectPrompt from "./prompt/select.js" -import * as InternalUsage from "./usage.js" -import * as InternalValidationError from "./validationError.js" const CommandSymbolKey = "@effect/cli/Command" /** @internal */ -export const CommandTypeId: Command.CommandTypeId = Symbol.for( +export const TypeId: Command.TypeId = Symbol.for( CommandSymbolKey -) as Command.CommandTypeId +) as Command.TypeId -/** @internal */ -export type Op = Command.Command & Body & { - readonly _tag: Tag -} +const parseConfig = (config: Command.Command.ConfigBase): Command.Command.ParsedConfig => { + const args: Array> = [] + let argsIndex = 0 + const options: Array> = [] + let optionsIndex = 0 + const tree: Command.Command.ParsedConfigTree = {} -const proto = { - [CommandTypeId]: { - _A: (_: never) => _ - }, - pipe() { - return pipeArguments(this, arguments) + function parse(config: Command.Command.ConfigBase) { + for (const key in config) { + tree[key] = parseValue(config[key]) + } + return tree } -} - -/** @internal */ -export type Instruction = - | Standard - | GetUserInput - | Map - | OrElse - | Subcommands - -/** @internal */ -export interface Standard extends - Op<"Standard", { - readonly name: string - readonly description: HelpDoc.HelpDoc - readonly options: Options.Options - readonly args: Args.Args - }> -{} - -/** @internal */ -export interface GetUserInput extends - Op<"GetUserInput", { - readonly name: string - readonly description: HelpDoc.HelpDoc - readonly prompt: Prompt.Prompt - }> -{} -/** @internal */ -export interface Map extends - Op<"Map", { - readonly command: Command.Command - readonly f: (value: unknown) => Either.Either - }> -{} - -/** @internal */ -export interface OrElse extends - Op<"OrElse", { - readonly left: Command.Command - readonly right: Command.Command - }> -{} - -/** @internal */ -export interface Subcommands extends - Op<"Subcommands", { - readonly parent: Command.Command - readonly child: Command.Command - }> -{} - -// ============================================================================= -// Refinements -// ============================================================================= - -/** @internal */ -export const isCommand = (u: unknown): u is Command.Command => - typeof u === "object" && u != null && CommandTypeId in u - -/** @internal */ -export const isStandard = (self: Instruction): self is Standard => self._tag === "Standard" - -/** @internal */ -export const isGetUserInput = (self: Instruction): self is GetUserInput => - self._tag === "GetUserInput" + function parseValue( + value: + | Args.Args + | Options.Options + | ReadonlyArray | Options.Options | Command.Command.ConfigBase> + | Command.Command.ConfigBase + ): Command.Command.ParsedConfigNode { + if (Array.isArray(value)) { + return { + _tag: "Array", + children: ReadonlyArray.map(value, parseValue) + } + } else if (InternalArgs.isArgs(value)) { + args.push(value) + return { + _tag: "Args", + index: argsIndex++ + } + } else if (InternalOptions.isOptions(value)) { + options.push(value) + return { + _tag: "Options", + index: optionsIndex++ + } + } else { + return { + _tag: "ParsedConfig", + tree: parse(value as any) + } + } + } -/** @internal */ -export const isMap = (self: Instruction): self is Map => self._tag === "Map" + return { + args, + options, + tree: parse(config) + } +} -/** @internal */ -export const isOrElse = (self: Instruction): self is OrElse => self._tag === "OrElse" +const reconstructConfigTree = ( + tree: Command.Command.ParsedConfigTree, + args: ReadonlyArray, + options: ReadonlyArray +): Record => { + const output: Record = {} -/** @internal */ -export const isSubcommands = (self: Instruction): self is Subcommands => self._tag === "Subcommands" + for (const key in tree) { + output[key] = nodeValue(tree[key]) + } -// ============================================================================= -// Constructors -// ============================================================================= + return output -const defaultConstructorConfig = { - options: InternalOptions.none, - args: InternalArgs.none + function nodeValue(node: Command.Command.ParsedConfigNode): any { + if (node._tag === "Args") { + return args[node.index] + } else if (node._tag === "Options") { + return options[node.index] + } else if (node._tag === "Array") { + return ReadonlyArray.map(node.children, nodeValue) + } else { + return reconstructConfigTree(node.tree, args, options) + } + } } -/** @internal */ -export const make = ( - name: Name, - config: Command.Command.ConstructorConfig = defaultConstructorConfig as any -): Command.Command> => { - const { args, options } = { ...defaultConstructorConfig, ...config } - const op = Object.create(proto) - op._tag = "Standard" - op.name = name - op.description = InternalHelpDoc.empty - op.options = options - op.args = args - return op +const Prototype = { + ...Effectable.CommitPrototype, + [TypeId]: TypeId, + commit(this: Command.Command) { + return this.tag + }, + pipe() { + return pipeArguments(this, arguments) + } } -/** @internal */ -export const prompt = ( - name: Name, - prompt: Prompt.Prompt -): Command.Command> => { - const op = Object.create(proto) - op._tag = "GetUserInput" - op.name = name - op.description = InternalHelpDoc.empty - op.prompt = prompt - return op +const modifiedCommands = globalValue( + "@effect/cli/Command/modifiedCommands", + () => new WeakMap, Descriptor.Command>() +) + +const getDescriptor = (self: Command.Command) => + modifiedCommands.get(self.tag) ?? self.descriptor + +const makeProto = ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect.Effect, + tag?: Context.Tag +): Command.Command => { + console.log("makeProto", descriptor, handler, tag) + const self = Object.create(Prototype) + self.descriptor = descriptor + self.handler = handler + self.tag = tag ?? Context.Tag() + modifiedCommands.set(self.tag, self.descriptor) + return self } -// ============================================================================= -// Combinators -// ============================================================================= - -/** @internal */ -export const getHelp = (self: Command.Command): HelpDoc.HelpDoc => - getHelpInternal(self as Instruction) - /** @internal */ -export const getNames = (self: Command.Command): HashSet.HashSet => - getNamesInternal(self as Instruction) +export const fromDescriptor = dual< + ( + handler: (_: A) => Effect.Effect + ) => (command: Descriptor.Command) => Command.Command, + ( + descriptor: Descriptor.Command, + handler: (_: A) => Effect.Effect + ) => Command.Command +>(2, (descriptor, handler) => makeProto(descriptor, handler)) /** @internal */ -export const getBashCompletions = ( - self: Command.Command, - programName: string -): Effect.Effect> => - getBashCompletionsInternal(self as Instruction, programName) +export const fromDescriptorUnit = ( + descriptor: Descriptor.Command +): Command.Command => makeProto(descriptor, (_) => Effect.unit) /** @internal */ -export const getFishCompletions = ( - self: Command.Command, - programName: string -): Effect.Effect> => - getFishCompletionsInternal(self as Instruction, programName) - -/** @internal */ -export const getZshCompletions = ( - self: Command.Command, - programName: string -): Effect.Effect> => - getZshCompletionsInternal(self as Instruction, programName) +export const fromDescriptorHelp = ( + descriptor: Descriptor.Command +): Command.Command => { + const self: Command.Command = makeProto( + descriptor, + (_) => Effect.fail(ValidationError.helpRequested(getDescriptor(self))) + ) + return self +} -/** @internal */ -export const getSubcommands = ( - self: Command.Command -): HashMap.HashMap> => getSubcommandsInternal(self as Instruction) +const makeDescriptor = ( + name: string, + config: Config +): Descriptor.Command>> => { + const { args, options, tree } = parseConfig(config) + return InternalDescriptor.map( + InternalDescriptor.make(name, InternalOptions.all(options), InternalArgs.all(args)), + ({ args, options }) => reconstructConfigTree(tree, args, options) + ) as any +} /** @internal */ -export const getUsage = (self: Command.Command): Usage.Usage => - getUsageInternal(self as Instruction) +export const make = ( + name: Name, + config: Config, + handler: (_: Types.Simplify>) => Effect.Effect +): Command.Command< + Name, + R, + E, + Types.Simplify> +> => makeProto(makeDescriptor(name, config), handler) + +/** @internal */ +export const makeUnit: { + ( + name: Name + ): Command.Command + ( + name: Name, + config: Config + ): Command.Command< + Name, + never, + never, + Types.Simplify> + > +} = (name: string, config = {}) => fromDescriptorUnit(makeDescriptor(name, config) as any) as any + +/** @internal */ +export const makeHelp: { + ( + name: Name + ): Command.Command + ( + name: Name, + config: Config + ): Command.Command< + Name, + never, + ValidationError.ValidationError, + Types.Simplify> + > +} = (name: string, config = {}) => fromDescriptorHelp(makeDescriptor(name, config) as any) as any /** @internal */ -export const map = dual< - (f: (a: A) => B) => (self: Command.Command) => Command.Command, - (self: Command.Command, f: (a: A) => B) => Command.Command ->(2, (self, f) => mapOrFail(self, (a) => Either.right(f(a)))) +export const mapDescriptor = dual< + (f: (_: Descriptor.Command) => Descriptor.Command) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + f: (_: Descriptor.Command) => Descriptor.Command + ) => Command.Command +>(2, (self, f) => makeProto(f(getDescriptor(self)), self.handler, self.tag)) /** @internal */ -export const mapOrFail = dual< +export const mapBoth = dual< ( - f: (a: A) => Either.Either - ) => (self: Command.Command) => Command.Command, - ( - self: Command.Command, - f: (a: A) => Either.Either - ) => Command.Command ->(2, (self, f) => { - const op = Object.create(proto) - op._tag = "Map" - op.command = self - op.f = f - return op -}) - -/** @internal */ -export const orElse = dual< - (that: Command.Command) => (self: Command.Command) => Command.Command, - (self: Command.Command, that: Command.Command) => Command.Command ->(2, (self, that) => { - const op = Object.create(proto) - op._tag = "OrElse" - op.left = self - op.right = that - return op -}) - -/** @internal */ -export const orElseEither = dual< - ( - that: Command.Command - ) => (self: Command.Command) => Command.Command>, - (self: Command.Command, that: Command.Command) => Command.Command> ->(2, (self, that) => orElse(map(self, Either.left), map(that, Either.right))) - -/** @internal */ -export const parse = dual< - ( - args: ReadonlyArray, - config: CliConfig.CliConfig - ) => (self: Command.Command) => Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - CommandDirective.CommandDirective - >, - ( - self: Command.Command, - args: ReadonlyArray, - config: CliConfig.CliConfig - ) => Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - CommandDirective.CommandDirective - > ->(3, (self, args, config) => parseInternal(self as Instruction, args, config)) + f: (_: Descriptor.Command) => Descriptor.Command, + g: (_: B) => A + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + f: (_: Descriptor.Command) => Descriptor.Command, + g: (_: B) => A + ) => Command.Command +>(3, (self, f, g) => makeProto(f(getDescriptor(self)), (_) => self.handler(g(_)), self.tag)) + +/** @internal */ +export const prompt = ( + name: Name, + prompt: Prompt.Prompt, + handler: (_: A) => Effect.Effect +) => + makeProto( + InternalDescriptor.map( + InternalDescriptor.prompt(name, prompt), + (_) => _.value + ), + handler + ) /** @internal */ export const withDescription = dual< - (help: string | HelpDoc.HelpDoc) => (self: Command.Command) => Command.Command, - (self: Command.Command, help: string | HelpDoc.HelpDoc) => Command.Command ->(2, (self, help) => withDescriptionInternal(self as Instruction, help)) + ( + help: string | HelpDoc + ) => ( + self: Command.Command + ) => Command.Command, + ( + self: Command.Command, + help: string | HelpDoc + ) => Command.Command +>(2, (self, help) => mapDescriptor(self, InternalDescriptor.withDescription(help))) /** @internal */ export const withSubcommands = dual< - >>( - subcommands: [...Subcommands] - ) => ( - self: Command.Command - ) => Command.Command< - Command.Command.ComputeParsedType< - A & Readonly<{ subcommand: Option.Option> }> + >>( + subcommands: Subcommand + ) => (self: Command.Command) => Command.Command< + Name, + | R + | Exclude< + Effect.Effect.Context>, + Descriptor.Command + >, + E | Effect.Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { + subcommand: Option.Option< + Descriptor.Command.GetParsedType + > + } + > > >, - >>( - self: Command.Command, - subcommands: [...Subcommands] + < + Name extends string, + R, + E, + A, + Subcommand extends ReadonlyArray.NonEmptyReadonlyArray> + >( + self: Command.Command, + subcommands: Subcommand ) => Command.Command< - Command.Command.ComputeParsedType< - A & Readonly<{ subcommand: Option.Option> }> + Name, + | R + | Exclude< + Effect.Effect.Context>, + Descriptor.Command + >, + E | Effect.Effect.Error>, + Descriptor.Command.ComputeParsedType< + & A + & Readonly< + { + subcommand: Option.Option< + Descriptor.Command.GetParsedType + > + } + > > > >(2, (self, subcommands) => { - const op = Object.create(proto) - op._tag = "Subcommands" - op.parent = self - if (ReadonlyArray.isNonEmptyReadonlyArray(subcommands)) { - const head = ReadonlyArray.headNonEmpty>(subcommands) - const tail = ReadonlyArray.tailNonEmpty>(subcommands) - op.child = ReadonlyArray.isNonEmptyReadonlyArray(tail) - ? ReadonlyArray.reduce(tail, head, orElse) - : head - return op - } - throw new Error("[BUG]: Command.subcommands - received empty list of subcommands") -}) - -/** @internal */ -export const wizard = dual< - (config: CliConfig.CliConfig) => (self: Command.Command) => Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - ReadonlyArray - >, - (self: Command.Command, config: CliConfig.CliConfig) => Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - ReadonlyArray - > ->(2, (self, config) => wizardInternal(self as Instruction, config)) - -// ============================================================================= -// Internals -// ============================================================================= - -const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { - switch (self._tag) { - case "Standard": { - const header = InternalHelpDoc.isEmpty(self.description) - ? InternalHelpDoc.empty - : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) - const argsHelp = InternalArgs.getHelp(self.args) - const argsSection = InternalHelpDoc.isEmpty(argsHelp) - ? InternalHelpDoc.empty - : InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp) - const optionsHelp = InternalOptions.getHelp(self.options) - const optionsSection = InternalHelpDoc.isEmpty(optionsHelp) - ? InternalHelpDoc.empty - : InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp) - return InternalHelpDoc.sequence(header, InternalHelpDoc.sequence(argsSection, optionsSection)) - } - case "GetUserInput": { - return InternalHelpDoc.isEmpty(self.description) - ? InternalHelpDoc.empty - : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) - } - case "Map": { - return getHelpInternal(self.command as Instruction) - } - case "OrElse": { - return InternalHelpDoc.sequence( - getHelpInternal(self.left as Instruction), - getHelpInternal(self.right as Instruction) - ) - } - case "Subcommands": { - const getUsage = ( - command: Instruction, - preceding: ReadonlyArray - ): ReadonlyArray<[Span.Span, Span.Span]> => { - switch (command._tag) { - case "Standard": - case "GetUserInput": { - const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(getUsageInternal(command))) - const usages = ReadonlyArray.append(preceding, usage) - const finalUsage = ReadonlyArray.reduce( - usages, - InternalSpan.empty, - (acc, next) => - InternalSpan.isText(acc) && acc.value === "" - ? next - : InternalSpan.isText(next) && next.value === "" - ? acc - : InternalSpan.spans([acc, InternalSpan.space, next]) - ) - const description = InternalHelpDoc.getSpan(command.description) - return ReadonlyArray.of([finalUsage, description]) - } - case "Map": { - return getUsage(command.command as Instruction, preceding) - } - case "OrElse": { - return ReadonlyArray.appendAll( - getUsage(command.left as Instruction, preceding), - getUsage(command.right as Instruction, preceding) - ) - } - case "Subcommands": { - const parentUsage = getUsage(command.parent as Instruction, preceding) - return Option.match(ReadonlyArray.head(parentUsage), { - onNone: () => getUsage(command.child as Instruction, preceding), - onSome: ([usage]) => { - const childUsage = getUsage( - command.child as Instruction, - ReadonlyArray.append(preceding, usage) - ) - return ReadonlyArray.appendAll(parentUsage, childUsage) - } - }) - } - } - } - const printSubcommands = ( - subcommands: ReadonlyArray<[Span.Span, Span.Span]> - ): HelpDoc.HelpDoc => { - const maxUsageLength = ReadonlyArray.reduceRight( - subcommands, - 0, - (max, [usage]) => Math.max(InternalSpan.size(usage), max) - ) - const documents = ReadonlyArray.map(subcommands, ([usage, desc]) => - InternalHelpDoc.p( - InternalSpan.spans([ - usage, - InternalSpan.text(" ".repeat(maxUsageLength - InternalSpan.size(usage) + 2)), - desc - ]) - )) - if (ReadonlyArray.isNonEmptyReadonlyArray(documents)) { - return InternalHelpDoc.enumeration(documents) - } - throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print") - } - return InternalHelpDoc.sequence( - getHelpInternal(self.parent as Instruction), - InternalHelpDoc.sequence( - InternalHelpDoc.h1("COMMANDS"), - printSubcommands(getUsage(self.child as Instruction, ReadonlyArray.empty())) - ) - ) - } - } -} - -const getNamesInternal = (self: Instruction): HashSet.HashSet => { - switch (self._tag) { - case "Standard": - case "GetUserInput": { - return HashSet.make(self.name) - } - case "Map": { - return getNamesInternal(self.command as Instruction) - } - case "OrElse": { - return HashSet.union( - getNamesInternal(self.right as Instruction), - getNamesInternal(self.left as Instruction) - ) - } - case "Subcommands": { - return getNamesInternal(self.parent as Instruction) - } - } -} - -const getSubcommandsInternal = ( - self: Instruction -): HashMap.HashMap => { - switch (self._tag) { - case "Standard": - case "GetUserInput": { - return HashMap.make([self.name, self]) - } - case "Map": { - return getSubcommandsInternal(self.command as Instruction) - } - case "OrElse": { - return HashMap.union( - getSubcommandsInternal(self.left as Instruction), - getSubcommandsInternal(self.right as Instruction) - ) - } - case "Subcommands": { - return getSubcommandsInternal(self.parent as Instruction) - } - } -} - -const getUsageInternal = (self: Instruction): Usage.Usage => { - switch (self._tag) { - case "Standard": { - return InternalUsage.concat( - InternalUsage.named(ReadonlyArray.of(self.name), Option.none()), - InternalUsage.concat( - InternalOptions.getUsage(self.options), - InternalArgs.getUsage(self.args) - ) - ) - } - case "GetUserInput": { - return InternalUsage.named(ReadonlyArray.of(self.name), Option.none()) - } - case "Map": { - return getUsageInternal(self.command as Instruction) - } - case "OrElse": { - return InternalUsage.mixed - } - case "Subcommands": { - return InternalUsage.concat( - getUsageInternal(self.parent as Instruction), - getUsageInternal(self.child as Instruction) - ) - } - } -} - -const parseInternal = ( - self: Instruction, - args: ReadonlyArray, - config: CliConfig.CliConfig -): Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - CommandDirective.CommandDirective -> => { - switch (self._tag) { - case "Standard": { - const parseCommandLine = ( - args: ReadonlyArray - ): Effect.Effect> => { - if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { - const head = ReadonlyArray.headNonEmpty(args) - const tail = ReadonlyArray.tailNonEmpty(args) - const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) - const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) - return Effect.succeed(tail).pipe( - Effect.when(() => normalizedArgv0 === normalizedCommandName), - Effect.flatten, - Effect.catchTag("NoSuchElementException", () => { - const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) - return Effect.fail(InternalValidationError.commandMismatch(error)) - }) - ) - } - const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) - return Effect.fail(InternalValidationError.commandMismatch(error)) - } - const parseBuiltInArgs = ( - args: ReadonlyArray - ): Effect.Effect< - FileSystem.FileSystem, - ValidationError.ValidationError, - CommandDirective.CommandDirective - > => { - if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { - const argv0 = ReadonlyArray.headNonEmpty(args) - const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0) - const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) - if (normalizedArgv0 === normalizedCommandName) { - const help = getHelpInternal(self) - const usage = getUsageInternal(self) - const options = InternalBuiltInOptions.builtInOptions(self, usage, help) - return InternalOptions.validate(options, ReadonlyArray.drop(args, 1), config).pipe( - Effect.flatMap((tuple) => tuple[2]), - Effect.catchTag("NoSuchElementException", () => { - const error = InternalHelpDoc.p("No built-in option was matched") - return Effect.fail(InternalValidationError.noBuiltInMatch(error)) - }), - Effect.map(InternalCommandDirective.builtIn) - ) - } - } - const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) - return Effect.fail(InternalValidationError.commandMismatch(error)) - } - const parseUserDefinedArgs = ( - args: ReadonlyArray - ): Effect.Effect< - FileSystem.FileSystem, - ValidationError.ValidationError, - CommandDirective.CommandDirective - > => - parseCommandLine(args).pipe(Effect.flatMap((commandOptionsAndArgs) => { - const [optionsAndArgs, forcedCommandArgs] = splitForcedArgs(commandOptionsAndArgs) - return InternalOptions.validate(self.options, optionsAndArgs, config).pipe( - Effect.flatMap(([error, commandArgs, optionsType]) => - InternalArgs.validate( - self.args, - ReadonlyArray.appendAll(commandArgs, forcedCommandArgs), - config - ).pipe( - Effect.catchAll((e) => - Option.match(error, { - onNone: () => Effect.fail(e), - onSome: (err) => Effect.fail(err) - }) - ), - Effect.map(([argsLeftover, argsType]) => - InternalCommandDirective.userDefined(argsLeftover, { - name: self.name, - options: optionsType, - args: argsType - }) - ) - ) - ) - ) - })) - const exhaustiveSearch = ( - args: ReadonlyArray - ): Effect.Effect< - FileSystem.FileSystem, - ValidationError.ValidationError, - CommandDirective.CommandDirective - > => { - if (ReadonlyArray.contains(args, "--help") || ReadonlyArray.contains(args, "-h")) { - return parseBuiltInArgs(ReadonlyArray.make(self.name, "--help")) - } - if (ReadonlyArray.contains(args, "--wizard")) { - return parseBuiltInArgs(ReadonlyArray.make(self.name, "--wizard")) - } - if (ReadonlyArray.contains(args, "--version")) { - return parseBuiltInArgs(ReadonlyArray.make(self.name, "--version")) - } - const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) - return Effect.fail(InternalValidationError.commandMismatch(error)) - } - return parseBuiltInArgs(args).pipe( - Effect.orElse(() => parseUserDefinedArgs(args)), - Effect.catchSome((e) => { - if (InternalValidationError.isValidationError(e)) { - if (config.finalCheckBuiltIn) { - return Option.some( - exhaustiveSearch(args).pipe( - Effect.catchSome((_) => - InternalValidationError.isValidationError(_) - ? Option.some(Effect.fail(e)) - : Option.none() - ) - ) - ) - } - return Option.some(Effect.fail(e)) - } - return Option.none() - }) - ) - } - case "GetUserInput": { - return InternalPrompt.run(self.prompt).pipe( - Effect.map((value) => - InternalCommandDirective.userDefined(ReadonlyArray.drop(args, 1), { - name: self.name, - value - }) - ) - ) - } - case "Map": { - return parseInternal(self.command as Instruction, args, config).pipe( - Effect.flatMap((directive) => { - if (InternalCommandDirective.isUserDefined(directive)) { - const either = self.f(directive.value) - return Either.isLeft(either) - ? Effect.fail(either.left) - : Effect.succeed(InternalCommandDirective.userDefined( - directive.leftover, - either.right - )) - } - return Effect.succeed(directive) - }) - ) - } - case "OrElse": { - return parseInternal(self.left as Instruction, args, config).pipe( - Effect.catchSome((e) => { - return InternalValidationError.isCommandMismatch(e) - ? Option.some(parseInternal(self.right as Instruction, args, config)) - : Option.none() - }) - ) - } - case "Subcommands": { - const names = Array.from(getNamesInternal(self)) - const subcommands = getSubcommandsInternal(self.child as Instruction) - const [parentArgs, childArgs] = ReadonlyArray.span( - args, - (name) => !HashMap.has(subcommands, name) - ) - const helpDirectiveForParent = Effect.sync(() => { - return InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( - getUsageInternal(self), - getHelpInternal(self) - )) - }) - const helpDirectiveForChild = Effect.suspend(() => { - return parseInternal(self.child as Instruction, childArgs, config).pipe( - Effect.flatMap((directive) => { - if ( - InternalCommandDirective.isBuiltIn(directive) && - InternalBuiltInOptions.isShowHelp(directive.option) - ) { - const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") - const newDirective = InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( - InternalUsage.concat( - InternalUsage.named(ReadonlyArray.of(parentName), Option.none()), - directive.option.usage - ), - directive.option.helpDoc - )) - return Effect.succeed(newDirective) - } - return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) - }) - ) - }) - const wizardDirectiveForParent = Effect.sync(() => - InternalCommandDirective.builtIn(InternalBuiltInOptions.showWizard(self)) - ) - const wizardDirectiveForChild = Effect.suspend(() => - parseInternal(self.child as Instruction, childArgs, config).pipe( - Effect.flatMap((directive) => { - if ( - InternalCommandDirective.isBuiltIn(directive) && - InternalBuiltInOptions.isShowWizard(directive.option) - ) { - return Effect.succeed(directive) - } - return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) - }) - ) - ) - return parseInternal(self.parent as Instruction, parentArgs, config).pipe( - Effect.flatMap((directive) => { - switch (directive._tag) { - case "BuiltIn": { - if (InternalBuiltInOptions.isShowHelp(directive.option)) { - // We do not want to display the child help docs if there are - // no arguments indicating the CLI command was for the child - return ReadonlyArray.isNonEmptyReadonlyArray(childArgs) - ? Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent) - : helpDirectiveForParent - } - if (InternalBuiltInOptions.isShowWizard(directive.option)) { - return Effect.orElse(wizardDirectiveForChild, () => wizardDirectiveForParent) - } - return Effect.succeed(directive) - } - case "UserDefined": { - const args = ReadonlyArray.appendAll(directive.leftover, childArgs) - if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { - return parseInternal(self.child as Instruction, args, config).pipe(Effect.mapBoth({ - onFailure: (err) => { - if (InternalValidationError.isCommandMismatch(err)) { - const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") - const subcommandNames = pipe( - ReadonlyArray.fromIterable(HashMap.keys(subcommands)), - ReadonlyArray.map((name) => `'${name}'`) - ) - const oneOf = subcommandNames.length === 1 ? "" : " one of" - const error = InternalHelpDoc.p( - `Invalid subcommand for ${parentName} - use${oneOf} ${ - ReadonlyArray.join(subcommandNames, ", ") - }` - ) - return InternalValidationError.commandMismatch(error) - } - return err - }, - onSuccess: InternalCommandDirective.map((subcommand) => ({ - ...directive.value as any, - subcommand: Option.some(subcommand) - })) - })) - } - return Effect.succeed(InternalCommandDirective.userDefined(directive.leftover, { - ...directive.value as any, - subcommand: Option.none() - })) - } - } - }), - Effect.catchSome(() => - ReadonlyArray.isEmptyReadonlyArray(args) - ? Option.some(helpDirectiveForParent) : - Option.none() - ) - ) - } - } -} - -const splitForcedArgs = ( - args: ReadonlyArray -): [ReadonlyArray, ReadonlyArray] => { - const [remainingArgs, forcedArgs] = ReadonlyArray.span(args, (str) => str !== "--") - return [remainingArgs, ReadonlyArray.drop(forcedArgs, 1)] -} - -const withDescriptionInternal = ( - self: Instruction, - description: string | HelpDoc.HelpDoc -): Command.Command => { - switch (self._tag) { - case "Standard": { - const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description - const op = Object.create(proto) - op._tag = "Standard" - op.name = self.name - op.description = helpDoc - op.options = self.options - op.args = self.args - return op - } - case "GetUserInput": { - const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description - const op = Object.create(proto) - op._tag = "GetUserInput" - op.name = self.name - op.description = helpDoc - op.prompt = self.prompt - return op - } - case "Map": { - return map(withDescriptionInternal(self.command as Instruction, description), self.f) - } - case "OrElse": { - // TODO: if both the left and right commands also have help defined, that - // help will be overwritten by this method which may be undesirable - return orElse( - withDescriptionInternal(self.left as Instruction, description), - withDescriptionInternal(self.right as Instruction, description) - ) - } - case "Subcommands": { - const op = Object.create(proto) - op._tag = "Subcommands" - op.parent = withDescriptionInternal(self.parent as Instruction, description) - op.child = self.child - return op - } - } -} - -const wizardInternal = ( - self: Instruction, - config: CliConfig.CliConfig -): Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - ReadonlyArray -> => { - const loop = (self: WizardCommandSequence): Effect.Effect< - FileSystem.FileSystem | Terminal.Terminal, - ValidationError.ValidationError, - ReadonlyArray - > => { - switch (self._tag) { - case "SingleCommandWizard": { - const optionsWizard = isStandard(self.command) - ? InternalOptions.wizard(self.command.options, config) - : Effect.succeed(ReadonlyArray.empty()) - const argsWizard = isStandard(self.command) - ? InternalArgs.wizard(self.command.args, config) - : Effect.succeed(ReadonlyArray.empty()) - const help = InternalHelpDoc.p(pipe( - InternalSpan.text("\n"), - InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))), - InternalSpan.concat(InternalSpan.space), - InternalSpan.concat(InternalSpan.code(self.command.name)) - )) - const message = InternalHelpDoc.toAnsiText(help) - return Console.log(message).pipe( - Effect.zipRight(Effect.zipWith(optionsWizard, argsWizard, (options, args) => - pipe( - ReadonlyArray.appendAll(options, args), - ReadonlyArray.prepend(self.command.name) - ))) - ) - } - case "AlternativeCommandWizard": { - const makeChoice = (title: string, value: WizardCommandSequence) => ({ title, value }) - const choices = self.alternatives.map((alternative) => { - switch (alternative._tag) { - case "SingleCommandWizard": { - return makeChoice(alternative.command.name, alternative) - } - case "SubcommandWizard": { - return makeChoice(alternative.names, alternative) - } - } - }) - const description = InternalHelpDoc.p("Select which command you would like to execute") - const message = InternalHelpDoc.toAnsiText(description).trimEnd() - return InternalSelectPrompt.select({ message, choices }).pipe( - Effect.flatMap((nextSequence) => loop(nextSequence)) - ) - } - case "SubcommandWizard": { - console.log(self.parent) - return Effect.zipWith( - loop(self.parent), - loop(self.child), - (parent, child) => ReadonlyArray.appendAll(parent, child) - ) - } - } - } - return loop(getWizardCommandSequence(self)) -} - -type WizardCommandSequence = SingleCommandWizard | AlternativeCommandWizard | SubcommandWizard - -interface SingleCommandWizard { - readonly _tag: "SingleCommandWizard" - readonly command: GetUserInput | Standard -} - -interface AlternativeCommandWizard { - readonly _tag: "AlternativeCommandWizard" - readonly alternatives: ReadonlyArray -} - -interface SubcommandWizard { - _tag: "SubcommandWizard" - readonly names: string - readonly parent: WizardCommandSequence - readonly child: WizardCommandSequence -} - -/** - * Creates an intermediate data structure that allows commands to be properly - * sequenced by the prompts of Wizard Mode. - */ -const getWizardCommandSequence = (self: Instruction): WizardCommandSequence => { - switch (self._tag) { - case "Standard": - case "GetUserInput": { - return { _tag: "SingleCommandWizard", command: self } - } - case "Map": { - return getWizardCommandSequence(self.command as Instruction) - } - case "OrElse": { - const left = getWizardCommandSequence(self.left as Instruction) - const leftAlternatives = left._tag === "AlternativeCommandWizard" - ? left.alternatives - : ReadonlyArray.of(left) - const right = getWizardCommandSequence(self.right as Instruction) - const rightAlternatives = right._tag === "AlternativeCommandWizard" - ? right.alternatives - : ReadonlyArray.of(right) - const alternatives = ReadonlyArray.appendAll(leftAlternatives, rightAlternatives) - return { _tag: "AlternativeCommandWizard", alternatives } + const command = InternalDescriptor.withSubcommands( + self.descriptor, + ReadonlyArray.map(subcommands, (_) => [_.tag, _.descriptor]) + ) + const handlers = ReadonlyArray.reduce( + subcommands, + new Map, (_: any) => Effect.Effect>(), + (handlers, subcommand) => { + handlers.set(subcommand.tag, subcommand.handler) + return handlers } - case "Subcommands": { - const names = pipe( - ReadonlyArray.fromIterable(getNamesInternal(self.parent as Instruction)), - ReadonlyArray.join(" | ") + ) + function handler( + args: { + readonly name: string + readonly subcommand: Option.Option, value: unknown]> + } + ) { + if (args.subcommand._tag === "Some") { + const [tag, value] = args.subcommand.value + return Effect.provideService( + handlers.get(tag)!(value), + self.tag, + args as any ) - const parent = getWizardCommandSequence(self.parent as Instruction) - const child = getWizardCommandSequence(self.child as Instruction) - return { _tag: "SubcommandWizard", names, parent, child } - } - } -} - -// ============================================================================= -// Completion Internals -// ============================================================================= - -const getShortDescription = (self: Instruction): string => { - switch (self._tag) { - case "Standard": { - return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) - } - case "GetUserInput": { - return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) - } - case "Map": { - return getShortDescription(self.command as Instruction) - } - case "OrElse": - case "Subcommands": { - return "" } + return self.handler(args as any) } -} - -interface CommandInfo { - readonly command: Standard | GetUserInput - readonly parentCommands: ReadonlyArray - readonly subcommands: ReadonlyArray<[string, Standard | GetUserInput]> - readonly level: number -} - -/** - * Allows for linear traversal of a `Command` data structure, accumulating state - * based on information acquired from the command. - */ -const traverseCommand = ( - self: Instruction, - initialState: S, - f: (state: S, info: CommandInfo) => Effect.Effect -): Effect.Effect => - SynchronizedRef.make(initialState).pipe(Effect.flatMap((ref) => { - const loop = ( - self: Instruction, - parentCommands: ReadonlyArray, - subcommands: ReadonlyArray<[string, Standard | GetUserInput]>, - level: number - ): Effect.Effect => { - switch (self._tag) { - case "Standard": { - const info: CommandInfo = { - command: self, - parentCommands, - subcommands, - level - } - return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) - } - case "GetUserInput": { - const info: CommandInfo = { - command: self, - parentCommands, - subcommands, - level - } - return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) - } - case "Map": { - return loop(self.command as Instruction, parentCommands, subcommands, level) - } - case "OrElse": { - const left = loop(self.left as Instruction, parentCommands, subcommands, level) - const right = loop(self.right as Instruction, parentCommands, subcommands, level) - return Effect.zipRight(left, right) - } - case "Subcommands": { - const parentNames = Array.from(getNamesInternal(self.parent as Instruction)) - const nextSubcommands = Array.from(getSubcommandsInternal(self.child as Instruction)) - const nextParentCommands = ReadonlyArray.appendAll(parentCommands, parentNames) - // Traverse the parent command using old parent names and next subcommands - return loop(self.parent as Instruction, parentCommands, nextSubcommands, level).pipe( - Effect.zipRight( - // Traverse the child command using next parent names and old subcommands - loop(self.child as Instruction, nextParentCommands, subcommands, level + 1) - ) - ) - } - } - } - return Effect.suspend(() => loop(self, ReadonlyArray.empty(), ReadonlyArray.empty(), 0)).pipe( - Effect.zipRight(SynchronizedRef.get(ref)) - ) - })) - -const indentAll = dual< - (indent: number) => (self: ReadonlyArray) => ReadonlyArray, - (self: ReadonlyArray, indent: number) => ReadonlyArray ->(2, (self: ReadonlyArray, indent: number): ReadonlyArray => { - const indentation = new Array(indent + 1).join(" ") - return ReadonlyArray.map(self, (line) => `${indentation}${line}`) + return makeProto(command as any, handler, self.tag) as any }) -const getBashCompletionsInternal = ( - self: Instruction, - rootCommand: string -): Effect.Effect> => - traverseCommand( - self, - ReadonlyArray.empty<[ReadonlyArray, ReadonlyArray]>(), - (state, info) => { - const options = isStandard(info.command) - ? Options.all([info.command.options, InternalBuiltInOptions.builtIns]) - : InternalBuiltInOptions.builtIns - const optionNames = InternalOptions.getNames(options as InternalOptions.Instruction) - const optionCases = isStandard(info.command) - ? InternalOptions.getBashCompletions(info.command.options as InternalOptions.Instruction) - : ReadonlyArray.empty() - const subcommandNames = pipe( - info.subcommands, - ReadonlyArray.map(([name]) => name), - ReadonlyArray.sort(Order.string) - ) - const wordList = ReadonlyArray.appendAll(optionNames, subcommandNames) - const preformatted = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) - ? ReadonlyArray.of(info.command.name) - : pipe( - info.parentCommands, - ReadonlyArray.append(info.command.name), - ReadonlyArray.map((command) => command.replace("-", "__")) - ) - const caseName = ReadonlyArray.join(preformatted, ",") - const funcName = ReadonlyArray.join(preformatted, "__") - const funcLines = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) - ? ReadonlyArray.empty() - : [ - `${caseName})`, - ` cmd="${funcName}"`, - " ;;" - ] - const cmdLines = [ - `${funcName})`, - ` opts="${ReadonlyArray.join(wordList, " ")}"`, - ` if [[ \${cur} == -* || \${COMP_CWORD} -eq ${info.level + 1} ]] ; then`, - " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", - " return 0", - " fi", - " case \"${prev}\" in", - ...indentAll(optionCases, 8), - " *)", - " COMPREPLY=()", - " ;;", - " esac", - " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", - " return 0", - " ;;" - ] - const lines = ReadonlyArray.append( - state, - [funcLines, cmdLines] as [ReadonlyArray, ReadonlyArray] - ) - return Effect.succeed(lines) - } - ).pipe(Effect.map((lines) => { - const scriptName = `_${rootCommand}_bash_completions` - const funcCases = ReadonlyArray.flatMap(lines, ([funcLines]) => funcLines) - const cmdCases = ReadonlyArray.flatMap(lines, ([, cmdLines]) => cmdLines) - return [ - `function ${scriptName}() {`, - " local i cur prev opts cmd", - " COMPREPLY=()", - " cur=\"${COMP_WORDS[COMP_CWORD]}\"", - " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"", - " cmd=\"\"", - " opts=\"\"", - " for i in \"${COMP_WORDS[@]}\"; do", - " case \"${cmd},${i}\" in", - " \",$1\")", - ` cmd="${rootCommand}"`, - " ;;", - ...indentAll(funcCases, 12), - " *)", - " ;;", - " esac", - " done", - " case \"${cmd}\" in", - ...indentAll(cmdCases, 8), - " esac", - "}", - `complete -F ${scriptName} -o nosort -o bashdefault -o default ${rootCommand}` - ] - })) - -const getFishCompletionsInternal = ( - self: Instruction, - rootCommand: string -): Effect.Effect> => - traverseCommand(self, ReadonlyArray.empty(), (state, info) => { - const baseTemplate = ReadonlyArray.make("complete", "-c", rootCommand) - const options = isStandard(info.command) - ? InternalOptions.all([InternalBuiltInOptions.builtIns, info.command.options]) - : InternalBuiltInOptions.builtIns - const optionsCompletions = InternalOptions.getFishCompletions( - options as InternalOptions.Instruction - ) - const argsCompletions = isStandard(info.command) - ? InternalArgs.getFishCompletions(info.command.args as InternalArgs.Instruction) - : ReadonlyArray.empty() - const rootCompletions = (conditionals: ReadonlyArray) => - pipe( - ReadonlyArray.map(optionsCompletions, (option) => - pipe( - baseTemplate, - ReadonlyArray.appendAll(conditionals), - ReadonlyArray.append(option), - ReadonlyArray.join(" ") - )), - ReadonlyArray.appendAll( - ReadonlyArray.map(argsCompletions, (option) => - pipe( - baseTemplate, - ReadonlyArray.appendAll(conditionals), - ReadonlyArray.append(option), - ReadonlyArray.join(" ") - )) - ) - ) - const subcommandCompletions = (conditionals: ReadonlyArray) => - ReadonlyArray.map(info.subcommands, ([name, subcommand]) => { - const description = getShortDescription(subcommand) - return pipe( - baseTemplate, - ReadonlyArray.appendAll(conditionals), - ReadonlyArray.appendAll(ReadonlyArray.make("-f", "-a", `"${name}"`)), - ReadonlyArray.appendAll( - description.length === 0 - ? ReadonlyArray.empty() - : ReadonlyArray.make("-d", `'${description}'`) - ), - ReadonlyArray.join(" ") - ) - }) - // If parent commands are empty, then the info is for the root command - if (ReadonlyArray.isEmptyReadonlyArray(info.parentCommands)) { - const conditionals = ReadonlyArray.make("-n", "\"__fish_use_subcommand\"") - return Effect.succeed(pipe( - state, - ReadonlyArray.appendAll(rootCompletions(conditionals)), - ReadonlyArray.appendAll(subcommandCompletions(conditionals)) - )) - } - // Otherwise the info is for a subcommand - const parentConditionals = pipe( - info.parentCommands, - // Drop the root command name from the subcommand conditionals - ReadonlyArray.drop(1), - ReadonlyArray.append(info.command.name), - ReadonlyArray.map((command) => `__fish_seen_subcommand_from ${command}`) - ) - const subcommandConditionals = ReadonlyArray.map( - info.subcommands, - ([name]) => `not __fish_seen_subcommand_from ${name}` - ) - const baseConditionals = pipe( - ReadonlyArray.appendAll(parentConditionals, subcommandConditionals), - ReadonlyArray.join("; and ") - ) - const conditionals = ReadonlyArray.make("-n", `"${baseConditionals}"`) - return Effect.succeed(pipe( - state, - ReadonlyArray.appendAll(rootCompletions(conditionals)), - ReadonlyArray.appendAll(subcommandCompletions(conditionals)) - )) - }) - -const getZshCompletionsInternal = ( - self: Instruction, - rootCommand: string -): Effect.Effect> => - traverseCommand(self, ReadonlyArray.empty(), (state, info) => { - const preformatted = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) - ? ReadonlyArray.of(info.command.name) - : pipe( - info.parentCommands, - ReadonlyArray.append(info.command.name), - ReadonlyArray.map((command) => command.replace("-", "__")) - ) - const underscoreName = ReadonlyArray.join(preformatted, "__") - const spaceName = ReadonlyArray.join(preformatted, " ") - const subcommands = pipe( - info.subcommands, - ReadonlyArray.map(([name, subcommand]) => { - const desc = getShortDescription(subcommand) - return `'${name}:${desc}' \\` - }) - ) - const commands = ReadonlyArray.isEmptyReadonlyArray(subcommands) - ? `commands=()` - : `commands=(\n${ReadonlyArray.join(indentAll(subcommands, 8), "\n")}\n )` - const handlerLines = [ - `(( $+functions[_${underscoreName}_commands] )) ||`, - `_${underscoreName}_commands() {`, - ` local commands; ${commands}`, - ` _describe -t commands '${spaceName} commands' commands "$@"`, - "}" - ] - return Effect.succeed(ReadonlyArray.appendAll(state, handlerLines)) - }).pipe(Effect.map((handlers) => { - const cases = getZshSubcommandCases(self, ReadonlyArray.empty(), ReadonlyArray.empty()) - const scriptName = `_${rootCommand}_zsh_completions` - return [ - `#compdef ${rootCommand}`, - "", - "autoload -U is-at-least", - "", - `function ${scriptName}() {`, - " typeset -A opt_args", - " typeset -a _arguments_options", - " local ret=1", - "", - " if is-at-least 5.2; then", - " _arguments_options=(-s -S -C)", - " else", - " _arguments_options=(-s -C)", - " fi", - "", - " local context curcontext=\"$curcontext\" state line", - ...indentAll(cases, 4), - "}", - "", - ...handlers, - "", - `if [ "$funcstack[1]" = "${scriptName}" ]; then`, - ` ${scriptName} "$@"`, - "else", - ` compdef ${scriptName} ${rootCommand}`, - "fi" - ] - })) - -const getZshSubcommandCases = ( - self: Instruction, - parentCommands: ReadonlyArray, - subcommands: ReadonlyArray<[string, Standard | GetUserInput]> -): ReadonlyArray => { - switch (self._tag) { - case "Standard": - case "GetUserInput": { - const options = isStandard(self) - ? InternalOptions.all([InternalBuiltInOptions.builtIns, self.options]) - : InternalBuiltInOptions.builtIns - const optionCompletions = pipe( - InternalOptions.getZshCompletions(options as InternalOptions.Instruction), - ReadonlyArray.map((completion) => `'${completion}' \\`) - ) - if (ReadonlyArray.isEmptyReadonlyArray(parentCommands)) { - return [ - "_arguments \"${_arguments_options[@]}\" \\", - ...indentAll(optionCompletions, 4), - ` ":: :_${self.name}_commands" \\`, - ` "*::: :->${self.name}" \\`, - " && ret=0" - ] - } - if (ReadonlyArray.isEmptyReadonlyArray(subcommands)) { - return [ - `(${self.name})`, - "_arguments \"${_arguments_options[@]}\" \\", - ...indentAll(optionCompletions, 4), - " && ret=0", - ";;" - ] - } - return [ - `(${self.name})`, - "_arguments \"${_arguments_options[@]}\" \\", - ...indentAll(optionCompletions, 4), - ` ":: :_${ReadonlyArray.append(parentCommands, self.name).join("__")}_commands" \\`, - ` "*::: :->${self.name}" \\`, - " && ret=0" - ] - } - case "Map": { - return getZshSubcommandCases(self.command as Instruction, parentCommands, subcommands) - } - case "OrElse": { - const left = getZshSubcommandCases(self.left as Instruction, parentCommands, subcommands) - const right = getZshSubcommandCases(self.right as Instruction, parentCommands, subcommands) - return ReadonlyArray.appendAll(left, right) - } - case "Subcommands": { - const nextSubcommands = Array.from(getSubcommandsInternal(self.child as Instruction)) - const parentNames = Array.from(getNamesInternal(self.parent as Instruction)) - const parentLines = getZshSubcommandCases( - self.parent as Instruction, - parentCommands, - ReadonlyArray.appendAll(subcommands, nextSubcommands) - ) - const childCases = getZshSubcommandCases( - self.child as Instruction, - ReadonlyArray.appendAll(parentCommands, parentNames), - subcommands - ) - const hyphenName = pipe( - ReadonlyArray.appendAll(parentCommands, parentNames), - ReadonlyArray.join("-") - ) - const childLines = pipe( - parentNames, - ReadonlyArray.flatMap((parentName) => [ - "case $state in", - ` (${parentName})`, - ` words=($line[1] "\${words[@]}")`, - " (( CURRENT += 1 ))", - ` curcontext="\${curcontext%:*:*}:${hyphenName}-command-$line[1]:"`, - ` case $line[1] in`, - ...indentAll(childCases, 8), - " esac", - " ;;", - "esac" - ]), - ReadonlyArray.appendAll( - ReadonlyArray.isEmptyReadonlyArray(parentCommands) - ? ReadonlyArray.empty() - : ReadonlyArray.of(";;") - ) - ) - return ReadonlyArray.appendAll(parentLines, childLines) - } - } -} - -// Circular with ValidationError - /** @internal */ -export const helpRequestedError = ( - command: Command.Command -): ValidationError.ValidationError => { - const op = Object.create(InternalValidationError.proto) - op._tag = "HelpRequested" - op.error = InternalHelpDoc.empty - op.showHelp = InternalBuiltInOptions.showHelp( - getUsageInternal(command as Instruction), - getHelpInternal(command as Instruction) - ) - return op -} +export const run = dual< + (config: { + readonly name: string + readonly version: string + readonly summary?: Span | undefined + readonly footer?: HelpDoc | undefined + }) => ( + self: Command.Command + ) => ( + args: ReadonlyArray + ) => Effect.Effect, + (self: Command.Command, config: { + readonly name: string + readonly version: string + readonly summary?: Span | undefined + readonly footer?: HelpDoc | undefined + }) => ( + args: ReadonlyArray + ) => Effect.Effect +>(2, (self, config) => { + const app = InternalCliApp.make({ + ...config, + command: self.descriptor + }) + return (args) => InternalCliApp.run(app, args, self.handler) +}) diff --git a/src/internal/commandDescriptor.ts b/src/internal/commandDescriptor.ts new file mode 100644 index 0000000..37c035d --- /dev/null +++ b/src/internal/commandDescriptor.ts @@ -0,0 +1,1434 @@ +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Terminal from "@effect/platform/Terminal" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import { dual, pipe } from "effect/Function" +import * as HashMap from "effect/HashMap" +import * as HashSet from "effect/HashSet" +import * as Option from "effect/Option" +import * as Order from "effect/Order" +import { pipeArguments } from "effect/Pipeable" +import * as ReadonlyArray from "effect/ReadonlyArray" +import * as SynchronizedRef from "effect/SynchronizedRef" +import type * as Args from "../Args.js" +import type * as CliConfig from "../CliConfig.js" +import type * as Descriptor from "../CommandDescriptor.js" +import type * as Directive from "../CommandDirective.js" +import * as HelpDoc from "../HelpDoc.js" +import type * as Span from "../HelpDoc/Span.js" +import * as Options from "../Options.js" +import type * as Prompt from "../Prompt.js" +import type * as Usage from "../Usage.js" +import type * as ValidationError from "../ValidationError.js" +import * as InternalArgs from "./args.js" +import * as InternalBuiltInOptions from "./builtInOptions.js" +import * as InternalCliConfig from "./cliConfig.js" +import * as InternalCommandDirective from "./commandDirective.js" +import * as InternalHelpDoc from "./helpDoc.js" +import * as InternalSpan from "./helpDoc/span.js" +import * as InternalOptions from "./options.js" +import * as InternalPrompt from "./prompt.js" +import * as InternalSelectPrompt from "./prompt/select.js" +import * as InternalUsage from "./usage.js" +import * as InternalValidationError from "./validationError.js" + +const CommandDescriptorSymbolKey = "@effect/cli/CommandDescriptor" + +/** @internal */ +export const TypeId: Descriptor.TypeId = Symbol.for( + CommandDescriptorSymbolKey +) as Descriptor.TypeId + +/** @internal */ +export type Op = Descriptor.Command & Body & { + readonly _tag: Tag +} + +const proto = { + [TypeId]: { + _A: (_: never) => _ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export type Instruction = + | Standard + | GetUserInput + | Map + | OrElse + | Subcommands + +/** @internal */ +export interface Standard extends + Op<"Standard", { + readonly name: string + readonly description: HelpDoc.HelpDoc + readonly options: Options.Options + readonly args: Args.Args + }> +{} + +/** @internal */ +export interface GetUserInput extends + Op<"GetUserInput", { + readonly name: string + readonly description: HelpDoc.HelpDoc + readonly prompt: Prompt.Prompt + }> +{} + +/** @internal */ +export interface Map extends + Op<"Map", { + readonly command: Descriptor.Command + readonly f: (value: unknown) => Either.Either + }> +{} + +/** @internal */ +export interface OrElse extends + Op<"OrElse", { + readonly left: Descriptor.Command + readonly right: Descriptor.Command + }> +{} + +/** @internal */ +export interface Subcommands extends + Op<"Subcommands", { + readonly parent: Descriptor.Command + readonly child: Descriptor.Command + }> +{} + +// ============================================================================= +// Refinements +// ============================================================================= + +/** @internal */ +export const isCommand = (u: unknown): u is Descriptor.Command => + typeof u === "object" && u != null && TypeId in u + +/** @internal */ +export const isStandard = (self: Instruction): self is Standard => self._tag === "Standard" + +/** @internal */ +export const isGetUserInput = (self: Instruction): self is GetUserInput => + self._tag === "GetUserInput" + +/** @internal */ +export const isMap = (self: Instruction): self is Map => self._tag === "Map" + +/** @internal */ +export const isOrElse = (self: Instruction): self is OrElse => self._tag === "OrElse" + +/** @internal */ +export const isSubcommands = (self: Instruction): self is Subcommands => self._tag === "Subcommands" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const make = ( + name: Name, + options: Options.Options = InternalOptions.none as any, + args: Args.Args = InternalArgs.none as any +): Descriptor.Command< + Descriptor.Command.ParsedStandardCommand +> => { + const op = Object.create(proto) + op._tag = "Standard" + op.name = name + op.description = InternalHelpDoc.empty + op.options = options + op.args = args + return op +} + +/** @internal */ +export const prompt = ( + name: Name, + prompt: Prompt.Prompt +): Descriptor.Command> => { + const op = Object.create(proto) + op._tag = "GetUserInput" + op.name = name + op.description = InternalHelpDoc.empty + op.prompt = prompt + return op +} + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const getHelp = (self: Descriptor.Command): HelpDoc.HelpDoc => + getHelpInternal(self as Instruction) + +/** @internal */ +export const getNames = (self: Descriptor.Command): HashSet.HashSet => + getNamesInternal(self as Instruction) + +/** @internal */ +export const getBashCompletions = ( + self: Descriptor.Command, + programName: string +): Effect.Effect> => + getBashCompletionsInternal(self as Instruction, programName) + +/** @internal */ +export const getFishCompletions = ( + self: Descriptor.Command, + programName: string +): Effect.Effect> => + getFishCompletionsInternal(self as Instruction, programName) + +/** @internal */ +export const getZshCompletions = ( + self: Descriptor.Command, + programName: string +): Effect.Effect> => + getZshCompletionsInternal(self as Instruction, programName) + +/** @internal */ +export const getSubcommands = ( + self: Descriptor.Command +): HashMap.HashMap> => + getSubcommandsInternal(self as Instruction) + +/** @internal */ +export const getUsage = (self: Descriptor.Command): Usage.Usage => + getUsageInternal(self as Instruction) + +/** @internal */ +export const map = dual< + (f: (a: A) => B) => (self: Descriptor.Command) => Descriptor.Command, + (self: Descriptor.Command, f: (a: A) => B) => Descriptor.Command +>(2, (self, f) => mapOrFail(self, (a) => Either.right(f(a)))) + +/** @internal */ +export const mapOrFail = dual< + ( + f: (a: A) => Either.Either + ) => (self: Descriptor.Command) => Descriptor.Command, + ( + self: Descriptor.Command, + f: (a: A) => Either.Either + ) => Descriptor.Command +>(2, (self, f) => { + const op = Object.create(proto) + op._tag = "Map" + op.command = self + op.f = f + return op +}) + +/** @internal */ +export const orElse = dual< + ( + that: Descriptor.Command + ) => (self: Descriptor.Command) => Descriptor.Command, + ( + self: Descriptor.Command, + that: Descriptor.Command + ) => Descriptor.Command +>(2, (self, that) => { + const op = Object.create(proto) + op._tag = "OrElse" + op.left = self + op.right = that + return op +}) + +/** @internal */ +export const orElseEither = dual< + ( + that: Descriptor.Command + ) => (self: Descriptor.Command) => Descriptor.Command>, + ( + self: Descriptor.Command, + that: Descriptor.Command + ) => Descriptor.Command> +>(2, (self, that) => orElse(map(self, Either.left), map(that, Either.right))) + +/** @internal */ +export const parse = dual< + ( + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => (self: Descriptor.Command) => Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + Directive.CommandDirective + >, + ( + self: Descriptor.Command, + args: ReadonlyArray, + config: CliConfig.CliConfig + ) => Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + Directive.CommandDirective + > +>(3, (self, args, config) => parseInternal(self as Instruction, args, config)) + +/** @internal */ +export const withDescription = dual< + ( + help: string | HelpDoc.HelpDoc + ) => (self: Descriptor.Command) => Descriptor.Command, + ( + self: Descriptor.Command, + help: string | HelpDoc.HelpDoc + ) => Descriptor.Command +>(2, (self, help) => withDescriptionInternal(self as Instruction, help)) + +/** @internal */ +export const withSubcommands = dual< + < + Subcommands extends ReadonlyArray.NonEmptyReadonlyArray< + readonly [id: unknown, command: Descriptor.Command] + > + >( + subcommands: [...Subcommands] + ) => ( + self: Descriptor.Command + ) => Descriptor.Command< + Descriptor.Command.ComputeParsedType< + & A + & Readonly<{ subcommand: Option.Option> }> + > + >, + < + A, + Subcommands extends ReadonlyArray.NonEmptyReadonlyArray< + readonly [id: unknown, command: Descriptor.Command] + > + >( + self: Descriptor.Command, + subcommands: [...Subcommands] + ) => Descriptor.Command< + Descriptor.Command.ComputeParsedType< + & A + & Readonly<{ subcommand: Option.Option> }> + > + > +>(2, (self, subcommands) => { + const op = Object.create(proto) + op._tag = "Subcommands" + op.parent = self + if (ReadonlyArray.isNonEmptyReadonlyArray(subcommands)) { + const mapped = ReadonlyArray.map( + subcommands, + ([id, command]) => map(command, (a) => [id, a] as const) + ) + const head = ReadonlyArray.headNonEmpty>(mapped) + const tail = ReadonlyArray.tailNonEmpty>(mapped) + op.child = ReadonlyArray.isNonEmptyReadonlyArray(tail) + ? ReadonlyArray.reduce(tail, head, orElse) + : head + return op + } + throw new Error("[BUG]: Command.subcommands - received empty list of subcommands") +}) + +/** @internal */ +export const wizard = dual< + (config: CliConfig.CliConfig) => (self: Descriptor.Command) => Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + >, + (self: Descriptor.Command, config: CliConfig.CliConfig) => Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > +>(2, (self, config) => wizardInternal(self as Instruction, config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => { + switch (self._tag) { + case "Standard": { + const header = InternalHelpDoc.isEmpty(self.description) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) + const argsHelp = InternalArgs.getHelp(self.args) + const argsSection = InternalHelpDoc.isEmpty(argsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp) + const optionsHelp = InternalOptions.getHelp(self.options) + const optionsSection = InternalHelpDoc.isEmpty(optionsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp) + return InternalHelpDoc.sequence(header, InternalHelpDoc.sequence(argsSection, optionsSection)) + } + case "GetUserInput": { + return InternalHelpDoc.isEmpty(self.description) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description) + } + case "Map": { + return getHelpInternal(self.command as Instruction) + } + case "OrElse": { + return InternalHelpDoc.sequence( + getHelpInternal(self.left as Instruction), + getHelpInternal(self.right as Instruction) + ) + } + case "Subcommands": { + const getUsage = ( + command: Instruction, + preceding: ReadonlyArray + ): ReadonlyArray<[Span.Span, Span.Span]> => { + switch (command._tag) { + case "Standard": + case "GetUserInput": { + const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(getUsageInternal(command))) + const usages = ReadonlyArray.append(preceding, usage) + const finalUsage = ReadonlyArray.reduce( + usages, + InternalSpan.empty, + (acc, next) => + InternalSpan.isText(acc) && acc.value === "" + ? next + : InternalSpan.isText(next) && next.value === "" + ? acc + : InternalSpan.spans([acc, InternalSpan.space, next]) + ) + const description = InternalHelpDoc.getSpan(command.description) + return ReadonlyArray.of([finalUsage, description]) + } + case "Map": { + return getUsage(command.command as Instruction, preceding) + } + case "OrElse": { + return ReadonlyArray.appendAll( + getUsage(command.left as Instruction, preceding), + getUsage(command.right as Instruction, preceding) + ) + } + case "Subcommands": { + const parentUsage = getUsage(command.parent as Instruction, preceding) + return Option.match(ReadonlyArray.head(parentUsage), { + onNone: () => getUsage(command.child as Instruction, preceding), + onSome: ([usage]) => { + const childUsage = getUsage( + command.child as Instruction, + ReadonlyArray.append(preceding, usage) + ) + return ReadonlyArray.appendAll(parentUsage, childUsage) + } + }) + } + } + } + const printSubcommands = ( + subcommands: ReadonlyArray<[Span.Span, Span.Span]> + ): HelpDoc.HelpDoc => { + const maxUsageLength = ReadonlyArray.reduceRight( + subcommands, + 0, + (max, [usage]) => Math.max(InternalSpan.size(usage), max) + ) + const documents = ReadonlyArray.map(subcommands, ([usage, desc]) => + InternalHelpDoc.p( + InternalSpan.spans([ + usage, + InternalSpan.text(" ".repeat(maxUsageLength - InternalSpan.size(usage) + 2)), + desc + ]) + )) + if (ReadonlyArray.isNonEmptyReadonlyArray(documents)) { + return InternalHelpDoc.enumeration(documents) + } + throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print") + } + return InternalHelpDoc.sequence( + getHelpInternal(self.parent as Instruction), + InternalHelpDoc.sequence( + InternalHelpDoc.h1("COMMANDS"), + printSubcommands(getUsage(self.child as Instruction, ReadonlyArray.empty())) + ) + ) + } + } +} + +const getNamesInternal = (self: Instruction): HashSet.HashSet => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + return HashSet.make(self.name) + } + case "Map": { + return getNamesInternal(self.command as Instruction) + } + case "OrElse": { + return HashSet.union( + getNamesInternal(self.right as Instruction), + getNamesInternal(self.left as Instruction) + ) + } + case "Subcommands": { + return getNamesInternal(self.parent as Instruction) + } + } +} + +const getSubcommandsInternal = ( + self: Instruction +): HashMap.HashMap => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + return HashMap.make([self.name, self]) + } + case "Map": { + return getSubcommandsInternal(self.command as Instruction) + } + case "OrElse": { + return HashMap.union( + getSubcommandsInternal(self.left as Instruction), + getSubcommandsInternal(self.right as Instruction) + ) + } + case "Subcommands": { + return getSubcommandsInternal(self.parent as Instruction) + } + } +} + +const getUsageInternal = (self: Instruction): Usage.Usage => { + switch (self._tag) { + case "Standard": { + return InternalUsage.concat( + InternalUsage.named(ReadonlyArray.of(self.name), Option.none()), + InternalUsage.concat( + InternalOptions.getUsage(self.options), + InternalArgs.getUsage(self.args) + ) + ) + } + case "GetUserInput": { + return InternalUsage.named(ReadonlyArray.of(self.name), Option.none()) + } + case "Map": { + return getUsageInternal(self.command as Instruction) + } + case "OrElse": { + return InternalUsage.mixed + } + case "Subcommands": { + return InternalUsage.concat( + getUsageInternal(self.parent as Instruction), + getUsageInternal(self.child as Instruction) + ) + } + } +} + +const parseInternal = ( + self: Instruction, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + Directive.CommandDirective +> => { + switch (self._tag) { + case "Standard": { + const parseCommandLine = ( + args: ReadonlyArray + ): Effect.Effect> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) + return Effect.succeed(tail).pipe( + Effect.when(() => normalizedArgv0 === normalizedCommandName), + Effect.flatten, + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + }) + ) + } + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + const parseBuiltInArgs = ( + args: ReadonlyArray + ): Effect.Effect< + FileSystem.FileSystem, + ValidationError.ValidationError, + Directive.CommandDirective + > => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const argv0 = ReadonlyArray.headNonEmpty(args) + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name) + if (normalizedArgv0 === normalizedCommandName) { + const help = getHelpInternal(self) + const usage = getUsageInternal(self) + const options = InternalBuiltInOptions.builtInOptions(self, usage, help) + return InternalOptions.validate(options, ReadonlyArray.drop(args, 1), config).pipe( + Effect.flatMap((tuple) => tuple[2]), + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p("No built-in option was matched") + return Effect.fail(InternalValidationError.noBuiltInMatch(error)) + }), + Effect.map(InternalCommandDirective.builtIn) + ) + } + } + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + const parseUserDefinedArgs = ( + args: ReadonlyArray + ): Effect.Effect< + FileSystem.FileSystem, + ValidationError.ValidationError, + Directive.CommandDirective + > => + parseCommandLine(args).pipe(Effect.flatMap((commandOptionsAndArgs) => { + const [optionsAndArgs, forcedCommandArgs] = splitForcedArgs(commandOptionsAndArgs) + return InternalOptions.validate(self.options, optionsAndArgs, config).pipe( + Effect.flatMap(([error, commandArgs, optionsType]) => + InternalArgs.validate( + self.args, + ReadonlyArray.appendAll(commandArgs, forcedCommandArgs), + config + ).pipe( + Effect.catchAll((e) => + Option.match(error, { + onNone: () => Effect.fail(e), + onSome: (err) => Effect.fail(err) + }) + ), + Effect.map(([argsLeftover, argsType]) => + InternalCommandDirective.userDefined(argsLeftover, { + name: self.name, + options: optionsType, + args: argsType + }) + ) + ) + ) + ) + })) + const exhaustiveSearch = ( + args: ReadonlyArray + ): Effect.Effect< + FileSystem.FileSystem, + ValidationError.ValidationError, + Directive.CommandDirective + > => { + if (ReadonlyArray.contains(args, "--help") || ReadonlyArray.contains(args, "-h")) { + return parseBuiltInArgs(ReadonlyArray.make(self.name, "--help")) + } + if (ReadonlyArray.contains(args, "--wizard")) { + return parseBuiltInArgs(ReadonlyArray.make(self.name, "--wizard")) + } + if (ReadonlyArray.contains(args, "--version")) { + return parseBuiltInArgs(ReadonlyArray.make(self.name, "--version")) + } + const error = InternalHelpDoc.p(`Missing command name: '${self.name}'`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + return parseBuiltInArgs(args).pipe( + Effect.orElse(() => parseUserDefinedArgs(args)), + Effect.catchSome((e) => { + if (InternalValidationError.isValidationError(e)) { + if (config.finalCheckBuiltIn) { + return Option.some( + exhaustiveSearch(args).pipe( + Effect.catchSome((_) => + InternalValidationError.isValidationError(_) + ? Option.some(Effect.fail(e)) + : Option.none() + ) + ) + ) + } + return Option.some(Effect.fail(e)) + } + return Option.none() + }) + ) + } + case "GetUserInput": { + return InternalPrompt.run(self.prompt).pipe( + Effect.map((value) => + InternalCommandDirective.userDefined(ReadonlyArray.drop(args, 1), { + name: self.name, + value + }) + ) + ) + } + case "Map": { + return parseInternal(self.command as Instruction, args, config).pipe( + Effect.flatMap((directive) => { + if (InternalCommandDirective.isUserDefined(directive)) { + const either = self.f(directive.value) + return Either.isLeft(either) + ? Effect.fail(either.left) + : Effect.succeed(InternalCommandDirective.userDefined( + directive.leftover, + either.right + )) + } + return Effect.succeed(directive) + }) + ) + } + case "OrElse": { + return parseInternal(self.left as Instruction, args, config).pipe( + Effect.catchSome((e) => { + return InternalValidationError.isCommandMismatch(e) + ? Option.some(parseInternal(self.right as Instruction, args, config)) + : Option.none() + }) + ) + } + case "Subcommands": { + const names = Array.from(getNamesInternal(self)) + const subcommands = getSubcommandsInternal(self.child as Instruction) + const [parentArgs, childArgs] = ReadonlyArray.span( + args, + (name) => !HashMap.has(subcommands, name) + ) + const helpDirectiveForParent = Effect.sync(() => { + return InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( + getUsageInternal(self), + getHelpInternal(self) + )) + }) + const helpDirectiveForChild = Effect.suspend(() => { + return parseInternal(self.child as Instruction, childArgs, config).pipe( + Effect.flatMap((directive) => { + if ( + InternalCommandDirective.isBuiltIn(directive) && + InternalBuiltInOptions.isShowHelp(directive.option) + ) { + const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") + const newDirective = InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( + InternalUsage.concat( + InternalUsage.named(ReadonlyArray.of(parentName), Option.none()), + directive.option.usage + ), + directive.option.helpDoc + )) + return Effect.succeed(newDirective) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + }) + ) + }) + const wizardDirectiveForParent = Effect.sync(() => + InternalCommandDirective.builtIn(InternalBuiltInOptions.showWizard(self)) + ) + const wizardDirectiveForChild = Effect.suspend(() => + parseInternal(self.child as Instruction, childArgs, config).pipe( + Effect.flatMap((directive) => { + if ( + InternalCommandDirective.isBuiltIn(directive) && + InternalBuiltInOptions.isShowWizard(directive.option) + ) { + return Effect.succeed(directive) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + }) + ) + ) + return parseInternal(self.parent as Instruction, parentArgs, config).pipe( + Effect.flatMap((directive) => { + switch (directive._tag) { + case "BuiltIn": { + if (InternalBuiltInOptions.isShowHelp(directive.option)) { + // We do not want to display the child help docs if there are + // no arguments indicating the CLI command was for the child + return ReadonlyArray.isNonEmptyReadonlyArray(childArgs) + ? Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent) + : helpDirectiveForParent + } + if (InternalBuiltInOptions.isShowWizard(directive.option)) { + return Effect.orElse(wizardDirectiveForChild, () => wizardDirectiveForParent) + } + return Effect.succeed(directive) + } + case "UserDefined": { + const args = ReadonlyArray.appendAll(directive.leftover, childArgs) + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + return parseInternal(self.child as Instruction, args, config).pipe(Effect.mapBoth({ + onFailure: (err) => { + if (InternalValidationError.isCommandMismatch(err)) { + const parentName = Option.getOrElse(ReadonlyArray.head(names), () => "") + const subcommandNames = pipe( + ReadonlyArray.fromIterable(HashMap.keys(subcommands)), + ReadonlyArray.map((name) => `'${name}'`) + ) + const oneOf = subcommandNames.length === 1 ? "" : " one of" + const error = InternalHelpDoc.p( + `Invalid subcommand for ${parentName} - use${oneOf} ${ + ReadonlyArray.join(subcommandNames, ", ") + }` + ) + return InternalValidationError.commandMismatch(error) + } + return err + }, + onSuccess: InternalCommandDirective.map((subcommand) => ({ + ...directive.value as any, + subcommand: Option.some(subcommand) + })) + })) + } + return Effect.succeed(InternalCommandDirective.userDefined(directive.leftover, { + ...directive.value as any, + subcommand: Option.none() + })) + } + } + }), + Effect.catchSome(() => + ReadonlyArray.isEmptyReadonlyArray(args) + ? Option.some(helpDirectiveForParent) : + Option.none() + ) + ) + } + } +} + +const splitForcedArgs = ( + args: ReadonlyArray +): [ReadonlyArray, ReadonlyArray] => { + const [remainingArgs, forcedArgs] = ReadonlyArray.span(args, (str) => str !== "--") + return [remainingArgs, ReadonlyArray.drop(forcedArgs, 1)] +} + +const withDescriptionInternal = ( + self: Instruction, + description: string | HelpDoc.HelpDoc +): Descriptor.Command => { + switch (self._tag) { + case "Standard": { + const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description + const op = Object.create(proto) + op._tag = "Standard" + op.name = self.name + op.description = helpDoc + op.options = self.options + op.args = self.args + return op + } + case "GetUserInput": { + const helpDoc = typeof description === "string" ? HelpDoc.p(description) : description + const op = Object.create(proto) + op._tag = "GetUserInput" + op.name = self.name + op.description = helpDoc + op.prompt = self.prompt + return op + } + case "Map": { + return map(withDescriptionInternal(self.command as Instruction, description), self.f) + } + case "OrElse": { + // TODO: if both the left and right commands also have help defined, that + // help will be overwritten by this method which may be undesirable + return orElse( + withDescriptionInternal(self.left as Instruction, description), + withDescriptionInternal(self.right as Instruction, description) + ) + } + case "Subcommands": { + const op = Object.create(proto) + op._tag = "Subcommands" + op.parent = withDescriptionInternal(self.parent as Instruction, description) + op.child = self.child + return op + } + } +} + +const wizardInternal = ( + self: Instruction, + config: CliConfig.CliConfig +): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray +> => { + const loop = (self: WizardCommandSequence): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > => { + switch (self._tag) { + case "SingleCommandWizard": { + const optionsWizard = isStandard(self.command) + ? InternalOptions.wizard(self.command.options, config) + : Effect.succeed(ReadonlyArray.empty()) + const argsWizard = isStandard(self.command) + ? InternalArgs.wizard(self.command.args, config) + : Effect.succeed(ReadonlyArray.empty()) + const help = InternalHelpDoc.p(pipe( + InternalSpan.text("\n"), + InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))), + InternalSpan.concat(InternalSpan.space), + InternalSpan.concat(InternalSpan.code(self.command.name)) + )) + const message = InternalHelpDoc.toAnsiText(help) + return Console.log(message).pipe( + Effect.zipRight(Effect.zipWith(optionsWizard, argsWizard, (options, args) => + pipe( + ReadonlyArray.appendAll(options, args), + ReadonlyArray.prepend(self.command.name) + ))) + ) + } + case "AlternativeCommandWizard": { + const makeChoice = (title: string, value: WizardCommandSequence) => ({ title, value }) + const choices = self.alternatives.map((alternative) => { + switch (alternative._tag) { + case "SingleCommandWizard": { + return makeChoice(alternative.command.name, alternative) + } + case "SubcommandWizard": { + return makeChoice(alternative.names, alternative) + } + } + }) + const description = InternalHelpDoc.p("Select which command you would like to execute") + const message = InternalHelpDoc.toAnsiText(description).trimEnd() + return InternalSelectPrompt.select({ message, choices }).pipe( + Effect.flatMap((nextSequence) => loop(nextSequence)) + ) + } + case "SubcommandWizard": { + return Effect.zipWith( + loop(self.parent), + loop(self.child), + (parent, child) => ReadonlyArray.appendAll(parent, child) + ) + } + } + } + return loop(getWizardCommandSequence(self)) +} + +type WizardCommandSequence = SingleCommandWizard | AlternativeCommandWizard | SubcommandWizard + +interface SingleCommandWizard { + readonly _tag: "SingleCommandWizard" + readonly command: GetUserInput | Standard +} + +interface AlternativeCommandWizard { + readonly _tag: "AlternativeCommandWizard" + readonly alternatives: ReadonlyArray +} + +interface SubcommandWizard { + _tag: "SubcommandWizard" + readonly names: string + readonly parent: WizardCommandSequence + readonly child: WizardCommandSequence +} + +/** + * Creates an intermediate data structure that allows commands to be properly + * sequenced by the prompts of Wizard Mode. + */ +const getWizardCommandSequence = (self: Instruction): WizardCommandSequence => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + return { _tag: "SingleCommandWizard", command: self } + } + case "Map": { + return getWizardCommandSequence(self.command as Instruction) + } + case "OrElse": { + const left = getWizardCommandSequence(self.left as Instruction) + const leftAlternatives = left._tag === "AlternativeCommandWizard" + ? left.alternatives + : ReadonlyArray.of(left) + const right = getWizardCommandSequence(self.right as Instruction) + const rightAlternatives = right._tag === "AlternativeCommandWizard" + ? right.alternatives + : ReadonlyArray.of(right) + const alternatives = ReadonlyArray.appendAll(leftAlternatives, rightAlternatives) + return { _tag: "AlternativeCommandWizard", alternatives } + } + case "Subcommands": { + const names = pipe( + ReadonlyArray.fromIterable(getNamesInternal(self.parent as Instruction)), + ReadonlyArray.join(" | ") + ) + const parent = getWizardCommandSequence(self.parent as Instruction) + const child = getWizardCommandSequence(self.child as Instruction) + return { _tag: "SubcommandWizard", names, parent, child } + } + } +} + +// ============================================================================= +// Completion Internals +// ============================================================================= + +const getShortDescription = (self: Instruction): string => { + switch (self._tag) { + case "Standard": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "GetUserInput": { + return InternalSpan.getText(InternalHelpDoc.getSpan(self.description)) + } + case "Map": { + return getShortDescription(self.command as Instruction) + } + case "OrElse": + case "Subcommands": { + return "" + } + } +} + +interface CommandInfo { + readonly command: Standard | GetUserInput + readonly parentCommands: ReadonlyArray + readonly subcommands: ReadonlyArray<[string, Standard | GetUserInput]> + readonly level: number +} + +/** + * Allows for linear traversal of a `Command` data structure, accumulating state + * based on information acquired from the command. + */ +const traverseCommand = ( + self: Instruction, + initialState: S, + f: (state: S, info: CommandInfo) => Effect.Effect +): Effect.Effect => + SynchronizedRef.make(initialState).pipe(Effect.flatMap((ref) => { + const loop = ( + self: Instruction, + parentCommands: ReadonlyArray, + subcommands: ReadonlyArray<[string, Standard | GetUserInput]>, + level: number + ): Effect.Effect => { + switch (self._tag) { + case "Standard": { + const info: CommandInfo = { + command: self, + parentCommands, + subcommands, + level + } + return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) + } + case "GetUserInput": { + const info: CommandInfo = { + command: self, + parentCommands, + subcommands, + level + } + return SynchronizedRef.updateEffect(ref, (state) => f(state, info)) + } + case "Map": { + return loop(self.command as Instruction, parentCommands, subcommands, level) + } + case "OrElse": { + const left = loop(self.left as Instruction, parentCommands, subcommands, level) + const right = loop(self.right as Instruction, parentCommands, subcommands, level) + return Effect.zipRight(left, right) + } + case "Subcommands": { + const parentNames = Array.from(getNamesInternal(self.parent as Instruction)) + const nextSubcommands = Array.from(getSubcommandsInternal(self.child as Instruction)) + const nextParentCommands = ReadonlyArray.appendAll(parentCommands, parentNames) + // Traverse the parent command using old parent names and next subcommands + return loop(self.parent as Instruction, parentCommands, nextSubcommands, level).pipe( + Effect.zipRight( + // Traverse the child command using next parent names and old subcommands + loop(self.child as Instruction, nextParentCommands, subcommands, level + 1) + ) + ) + } + } + } + return Effect.suspend(() => loop(self, ReadonlyArray.empty(), ReadonlyArray.empty(), 0)).pipe( + Effect.zipRight(SynchronizedRef.get(ref)) + ) + })) + +const indentAll = dual< + (indent: number) => (self: ReadonlyArray) => ReadonlyArray, + (self: ReadonlyArray, indent: number) => ReadonlyArray +>(2, (self: ReadonlyArray, indent: number): ReadonlyArray => { + const indentation = new Array(indent + 1).join(" ") + return ReadonlyArray.map(self, (line) => `${indentation}${line}`) +}) + +const getBashCompletionsInternal = ( + self: Instruction, + rootCommand: string +): Effect.Effect> => + traverseCommand( + self, + ReadonlyArray.empty<[ReadonlyArray, ReadonlyArray]>(), + (state, info) => { + const options = isStandard(info.command) + ? Options.all([info.command.options, InternalBuiltInOptions.builtIns]) + : InternalBuiltInOptions.builtIns + const optionNames = InternalOptions.getNames(options as InternalOptions.Instruction) + const optionCases = isStandard(info.command) + ? InternalOptions.getBashCompletions(info.command.options as InternalOptions.Instruction) + : ReadonlyArray.empty() + const subcommandNames = pipe( + info.subcommands, + ReadonlyArray.map(([name]) => name), + ReadonlyArray.sort(Order.string) + ) + const wordList = ReadonlyArray.appendAll(optionNames, subcommandNames) + const preformatted = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) + ? ReadonlyArray.of(info.command.name) + : pipe( + info.parentCommands, + ReadonlyArray.append(info.command.name), + ReadonlyArray.map((command) => command.replace("-", "__")) + ) + const caseName = ReadonlyArray.join(preformatted, ",") + const funcName = ReadonlyArray.join(preformatted, "__") + const funcLines = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) + ? ReadonlyArray.empty() + : [ + `${caseName})`, + ` cmd="${funcName}"`, + " ;;" + ] + const cmdLines = [ + `${funcName})`, + ` opts="${ReadonlyArray.join(wordList, " ")}"`, + ` if [[ \${cur} == -* || \${COMP_CWORD} -eq ${info.level + 1} ]] ; then`, + " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", + " return 0", + " fi", + " case \"${prev}\" in", + ...indentAll(optionCases, 8), + " *)", + " COMPREPLY=()", + " ;;", + " esac", + " COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )", + " return 0", + " ;;" + ] + const lines = ReadonlyArray.append( + state, + [funcLines, cmdLines] as [ReadonlyArray, ReadonlyArray] + ) + return Effect.succeed(lines) + } + ).pipe(Effect.map((lines) => { + const scriptName = `_${rootCommand}_bash_completions` + const funcCases = ReadonlyArray.flatMap(lines, ([funcLines]) => funcLines) + const cmdCases = ReadonlyArray.flatMap(lines, ([, cmdLines]) => cmdLines) + return [ + `function ${scriptName}() {`, + " local i cur prev opts cmd", + " COMPREPLY=()", + " cur=\"${COMP_WORDS[COMP_CWORD]}\"", + " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"", + " cmd=\"\"", + " opts=\"\"", + " for i in \"${COMP_WORDS[@]}\"; do", + " case \"${cmd},${i}\" in", + " \",$1\")", + ` cmd="${rootCommand}"`, + " ;;", + ...indentAll(funcCases, 12), + " *)", + " ;;", + " esac", + " done", + " case \"${cmd}\" in", + ...indentAll(cmdCases, 8), + " esac", + "}", + `complete -F ${scriptName} -o nosort -o bashdefault -o default ${rootCommand}` + ] + })) + +const getFishCompletionsInternal = ( + self: Instruction, + rootCommand: string +): Effect.Effect> => + traverseCommand(self, ReadonlyArray.empty(), (state, info) => { + const baseTemplate = ReadonlyArray.make("complete", "-c", rootCommand) + const options = isStandard(info.command) + ? InternalOptions.all([InternalBuiltInOptions.builtIns, info.command.options]) + : InternalBuiltInOptions.builtIns + const optionsCompletions = InternalOptions.getFishCompletions( + options as InternalOptions.Instruction + ) + const argsCompletions = isStandard(info.command) + ? InternalArgs.getFishCompletions(info.command.args as InternalArgs.Instruction) + : ReadonlyArray.empty() + const rootCompletions = (conditionals: ReadonlyArray) => + pipe( + ReadonlyArray.map(optionsCompletions, (option) => + pipe( + baseTemplate, + ReadonlyArray.appendAll(conditionals), + ReadonlyArray.append(option), + ReadonlyArray.join(" ") + )), + ReadonlyArray.appendAll( + ReadonlyArray.map(argsCompletions, (option) => + pipe( + baseTemplate, + ReadonlyArray.appendAll(conditionals), + ReadonlyArray.append(option), + ReadonlyArray.join(" ") + )) + ) + ) + const subcommandCompletions = (conditionals: ReadonlyArray) => + ReadonlyArray.map(info.subcommands, ([name, subcommand]) => { + const description = getShortDescription(subcommand) + return pipe( + baseTemplate, + ReadonlyArray.appendAll(conditionals), + ReadonlyArray.appendAll(ReadonlyArray.make("-f", "-a", `"${name}"`)), + ReadonlyArray.appendAll( + description.length === 0 + ? ReadonlyArray.empty() + : ReadonlyArray.make("-d", `'${description}'`) + ), + ReadonlyArray.join(" ") + ) + }) + // If parent commands are empty, then the info is for the root command + if (ReadonlyArray.isEmptyReadonlyArray(info.parentCommands)) { + const conditionals = ReadonlyArray.make("-n", "\"__fish_use_subcommand\"") + return Effect.succeed(pipe( + state, + ReadonlyArray.appendAll(rootCompletions(conditionals)), + ReadonlyArray.appendAll(subcommandCompletions(conditionals)) + )) + } + // Otherwise the info is for a subcommand + const parentConditionals = pipe( + info.parentCommands, + // Drop the root command name from the subcommand conditionals + ReadonlyArray.drop(1), + ReadonlyArray.append(info.command.name), + ReadonlyArray.map((command) => `__fish_seen_subcommand_from ${command}`) + ) + const subcommandConditionals = ReadonlyArray.map( + info.subcommands, + ([name]) => `not __fish_seen_subcommand_from ${name}` + ) + const baseConditionals = pipe( + ReadonlyArray.appendAll(parentConditionals, subcommandConditionals), + ReadonlyArray.join("; and ") + ) + const conditionals = ReadonlyArray.make("-n", `"${baseConditionals}"`) + return Effect.succeed(pipe( + state, + ReadonlyArray.appendAll(rootCompletions(conditionals)), + ReadonlyArray.appendAll(subcommandCompletions(conditionals)) + )) + }) + +const getZshCompletionsInternal = ( + self: Instruction, + rootCommand: string +): Effect.Effect> => + traverseCommand(self, ReadonlyArray.empty(), (state, info) => { + const preformatted = ReadonlyArray.isEmptyReadonlyArray(info.parentCommands) + ? ReadonlyArray.of(info.command.name) + : pipe( + info.parentCommands, + ReadonlyArray.append(info.command.name), + ReadonlyArray.map((command) => command.replace("-", "__")) + ) + const underscoreName = ReadonlyArray.join(preformatted, "__") + const spaceName = ReadonlyArray.join(preformatted, " ") + const subcommands = pipe( + info.subcommands, + ReadonlyArray.map(([name, subcommand]) => { + const desc = getShortDescription(subcommand) + return `'${name}:${desc}' \\` + }) + ) + const commands = ReadonlyArray.isEmptyReadonlyArray(subcommands) + ? `commands=()` + : `commands=(\n${ReadonlyArray.join(indentAll(subcommands, 8), "\n")}\n )` + const handlerLines = [ + `(( $+functions[_${underscoreName}_commands] )) ||`, + `_${underscoreName}_commands() {`, + ` local commands; ${commands}`, + ` _describe -t commands '${spaceName} commands' commands "$@"`, + "}" + ] + return Effect.succeed(ReadonlyArray.appendAll(state, handlerLines)) + }).pipe(Effect.map((handlers) => { + const cases = getZshSubcommandCases(self, ReadonlyArray.empty(), ReadonlyArray.empty()) + const scriptName = `_${rootCommand}_zsh_completions` + return [ + `#compdef ${rootCommand}`, + "", + "autoload -U is-at-least", + "", + `function ${scriptName}() {`, + " typeset -A opt_args", + " typeset -a _arguments_options", + " local ret=1", + "", + " if is-at-least 5.2; then", + " _arguments_options=(-s -S -C)", + " else", + " _arguments_options=(-s -C)", + " fi", + "", + " local context curcontext=\"$curcontext\" state line", + ...indentAll(cases, 4), + "}", + "", + ...handlers, + "", + `if [ "$funcstack[1]" = "${scriptName}" ]; then`, + ` ${scriptName} "$@"`, + "else", + ` compdef ${scriptName} ${rootCommand}`, + "fi" + ] + })) + +const getZshSubcommandCases = ( + self: Instruction, + parentCommands: ReadonlyArray, + subcommands: ReadonlyArray<[string, Standard | GetUserInput]> +): ReadonlyArray => { + switch (self._tag) { + case "Standard": + case "GetUserInput": { + const options = isStandard(self) + ? InternalOptions.all([InternalBuiltInOptions.builtIns, self.options]) + : InternalBuiltInOptions.builtIns + const optionCompletions = pipe( + InternalOptions.getZshCompletions(options as InternalOptions.Instruction), + ReadonlyArray.map((completion) => `'${completion}' \\`) + ) + if (ReadonlyArray.isEmptyReadonlyArray(parentCommands)) { + return [ + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + ` ":: :_${self.name}_commands" \\`, + ` "*::: :->${self.name}" \\`, + " && ret=0" + ] + } + if (ReadonlyArray.isEmptyReadonlyArray(subcommands)) { + return [ + `(${self.name})`, + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + " && ret=0", + ";;" + ] + } + return [ + `(${self.name})`, + "_arguments \"${_arguments_options[@]}\" \\", + ...indentAll(optionCompletions, 4), + ` ":: :_${ReadonlyArray.append(parentCommands, self.name).join("__")}_commands" \\`, + ` "*::: :->${self.name}" \\`, + " && ret=0" + ] + } + case "Map": { + return getZshSubcommandCases(self.command as Instruction, parentCommands, subcommands) + } + case "OrElse": { + const left = getZshSubcommandCases(self.left as Instruction, parentCommands, subcommands) + const right = getZshSubcommandCases(self.right as Instruction, parentCommands, subcommands) + return ReadonlyArray.appendAll(left, right) + } + case "Subcommands": { + const nextSubcommands = Array.from(getSubcommandsInternal(self.child as Instruction)) + const parentNames = Array.from(getNamesInternal(self.parent as Instruction)) + const parentLines = getZshSubcommandCases( + self.parent as Instruction, + parentCommands, + ReadonlyArray.appendAll(subcommands, nextSubcommands) + ) + const childCases = getZshSubcommandCases( + self.child as Instruction, + ReadonlyArray.appendAll(parentCommands, parentNames), + subcommands + ) + const hyphenName = pipe( + ReadonlyArray.appendAll(parentCommands, parentNames), + ReadonlyArray.join("-") + ) + const childLines = pipe( + parentNames, + ReadonlyArray.flatMap((parentName) => [ + "case $state in", + ` (${parentName})`, + ` words=($line[1] "\${words[@]}")`, + " (( CURRENT += 1 ))", + ` curcontext="\${curcontext%:*:*}:${hyphenName}-command-$line[1]:"`, + ` case $line[1] in`, + ...indentAll(childCases, 8), + " esac", + " ;;", + "esac" + ]), + ReadonlyArray.appendAll( + ReadonlyArray.isEmptyReadonlyArray(parentCommands) + ? ReadonlyArray.empty() + : ReadonlyArray.of(";;") + ) + ) + return ReadonlyArray.appendAll(parentLines, childLines) + } + } +} + +// Circular with ValidationError + +/** @internal */ +export const helpRequestedError = ( + command: Descriptor.Command +): ValidationError.ValidationError => { + const op = Object.create(InternalValidationError.proto) + op._tag = "HelpRequested" + op.error = InternalHelpDoc.empty + op.showHelp = InternalBuiltInOptions.showHelp( + getUsageInternal(command as Instruction), + getHelpInternal(command as Instruction) + ) + return op +}