diff --git a/src/ValidationError.ts b/src/ValidationError.ts
index 43fad2a..4923a5d 100644
--- a/src/ValidationError.ts
+++ b/src/ValidationError.ts
@@ -1,7 +1,10 @@
/**
* @since 1.0.0
*/
+import type { BuiltInOptions } from "./BuiltInOptions.js"
+import type { Command } from "./Command.js"
import type { HelpDoc } from "./HelpDoc.js"
+import * as InternalCommand from "./internal/command.js"
import * as InternalValidationError from "./internal/validationError.js"
/**
@@ -23,6 +26,7 @@ export type ValidationErrorTypeId = typeof ValidationErrorTypeId
export type ValidationError =
| CommandMismatch
| CorrectedFlag
+ | HelpRequested
| InvalidArgument
| InvalidValue
| MissingValue
@@ -50,6 +54,16 @@ export interface CorrectedFlag extends ValidationError.Proto {
readonly error: HelpDoc
}
+/**
+ * @since 1.0.0
+ * @category models
+ */
+export interface HelpRequested extends ValidationError.Proto {
+ readonly _tag: "HelpRequested"
+ readonly error: HelpDoc
+ readonly showHelp: BuiltInOptions
+}
+
/**
* @since 1.0.0
* @category models
@@ -159,6 +173,13 @@ export const isCommandMismatch: (self: ValidationError) => self is CommandMismat
export const isCorrectedFlag: (self: ValidationError) => self is CorrectedFlag =
InternalValidationError.isCorrectedFlag
+/**
+ * @since 1.0.0
+ * @category refinements
+ */
+export const isHelpRequested: (self: ValidationError) => self is HelpRequested =
+ InternalValidationError.isHelpRequested
+
/**
* @since 1.0.0
* @category refinements
@@ -229,6 +250,13 @@ export const commandMismatch: (error: HelpDoc) => ValidationError =
export const correctedFlag: (error: HelpDoc) => ValidationError =
InternalValidationError.correctedFlag
+/**
+ * @since 1.0.0
+ * @category constructors
+ */
+export const helpRequested: (command: Command) => ValidationError =
+ InternalCommand.helpRequestedError
+
/**
* @since 1.0.0
* @category constructors
diff --git a/src/internal/args.ts b/src/internal/args.ts
index 5e60aae..9948567 100644
--- a/src/internal/args.ts
+++ b/src/internal/args.ts
@@ -734,7 +734,7 @@ const withDescriptionInternal = (self: Instruction, description: string): Args.A
}
}
-const wizardHeader = InternalHelpDoc.p("ARGS WIZARD")
+const wizardHeader = InternalHelpDoc.p("ARG WIZARD")
const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect<
FileSystem.FileSystem | Terminal.Terminal,
diff --git a/src/internal/builtInOptions.ts b/src/internal/builtInOptions.ts
index df4d550..cbdff68 100644
--- a/src/internal/builtInOptions.ts
+++ b/src/internal/builtInOptions.ts
@@ -2,7 +2,7 @@ import * as Option from "effect/Option"
import type * as BuiltInOptions from "../BuiltInOptions.js"
import type * as Command from "../Command.js"
import type * as HelpDoc from "../HelpDoc.js"
-import * as Options from "../Options.js"
+import type * as Options from "../Options.js"
import type * as Usage from "../Usage.js"
import * as InternalOptions from "./options.js"
@@ -62,7 +62,7 @@ export const completionsOptions: Options.Options<
["bash", "bash" as const],
["fish", "fish" as const],
["zsh", "zsh" as const]
-]).pipe(Options.optional)
+]).pipe(InternalOptions.optional)
/** @internal */
export const helpOptions: Options.Options = InternalOptions.boolean("help").pipe(
diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts
index 55794ed..8378639 100644
--- a/src/internal/cliApp.ts
+++ b/src/internal/cliApp.ts
@@ -84,7 +84,14 @@ export const run = dual<
onSuccess: Effect.unifiedFn((directive) => {
switch (directive._tag) {
case "UserDefined": {
- return execute(directive.value)
+ return execute(directive.value).pipe(
+ Effect.catchSome((e) =>
+ InternalValidationError.isValidationError(e) &&
+ InternalValidationError.isHelpRequested(e)
+ ? Option.some(handleBuiltInOption(self, e.showHelp, config))
+ : Option.none()
+ )
+ )
}
case "BuiltIn": {
return handleBuiltInOption(self, directive.option, config).pipe(
@@ -210,7 +217,8 @@ const handleBuiltInOption = (
])
)
const help = InternalHelpDoc.sequence(header, description)
- return Console.log(InternalHelpDoc.toAnsiText(help)).pipe(
+ const text = InternalHelpDoc.toAnsiText(help).trimEnd()
+ return Console.log(text).pipe(
Effect.zipRight(InternalCommand.wizard(builtIn.command, config)),
Effect.tap((args) => Console.log(InternalHelpDoc.toAnsiText(renderWizardArgs(args))))
)
diff --git a/src/internal/cliConfig.ts b/src/internal/cliConfig.ts
index 1e4d814..cc2c040 100644
--- a/src/internal/cliConfig.ts
+++ b/src/internal/cliConfig.ts
@@ -16,7 +16,7 @@ export const Tag = Context.Tag()
export const defaultConfig: CliConfig.CliConfig = {
isCaseSensitive: false,
autoCorrectLimit: 2,
- finalCheckBuiltIn: false,
+ finalCheckBuiltIn: true,
showAllNames: true,
showTypes: true
}
diff --git a/src/internal/command.ts b/src/internal/command.ts
index e7b840e..80b2e9c 100644
--- a/src/internal/command.ts
+++ b/src/internal/command.ts
@@ -304,16 +304,9 @@ export const withSubcommands = dual<
if (ReadonlyArray.isNonEmptyReadonlyArray(subcommands)) {
const head = ReadonlyArray.headNonEmpty>(subcommands)
const tail = ReadonlyArray.tailNonEmpty>(subcommands)
- if (ReadonlyArray.isNonEmptyReadonlyArray(tail)) {
- const child = ReadonlyArray.reduce(
- ReadonlyArray.tailNonEmpty(tail),
- orElse(head, ReadonlyArray.headNonEmpty(tail)),
- orElse
- )
- op.child = child
- return op
- }
- op.child = head
+ op.child = ReadonlyArray.isNonEmptyReadonlyArray(tail)
+ ? ReadonlyArray.reduce(tail, head, orElse)
+ : head
return op
}
throw new Error("[BUG]: Command.subcommands - received empty list of subcommands")
@@ -376,7 +369,7 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
case "Standard":
case "GetUserInput": {
const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(getUsageInternal(command)))
- const usages = ReadonlyArray.prepend(preceding, usage)
+ const usages = ReadonlyArray.append(preceding, usage)
const finalUsage = ReadonlyArray.reduce(
usages,
InternalSpan.empty,
@@ -693,59 +686,59 @@ const parseInternal = (
args,
(name) => !HashMap.has(subcommands, name)
)
- const helpDirectiveForParent = Effect.succeed(
- InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(
+ const helpDirectiveForParent = Effect.sync(() => {
+ return InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(
getUsageInternal(self),
getHelpInternal(self)
))
- )
- const helpDirectiveForChild = 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.succeed(
+ })
+ 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 = 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))
- })
+ 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)) {
- return Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent)
+ // 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)
@@ -848,73 +841,124 @@ const withDescriptionInternal = (
}
}
-const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect<
+const wizardInternal = (
+ self: Instruction,
+ config: CliConfig.CliConfig
+): Effect.Effect<
FileSystem.FileSystem | Terminal.Terminal,
ValidationError.ValidationError,
ReadonlyArray
> => {
- switch (self._tag) {
- case "Standard": {
- const message = InternalHelpDoc.p(pipe(
- InternalSpan.text("\n"),
- InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))),
- InternalSpan.concat(InternalSpan.space),
- InternalSpan.concat(InternalSpan.code(self.name))
- ))
- return Console.log(InternalHelpDoc.toAnsiText(message)).pipe(Effect.zipRight(Effect.zipWith(
- InternalOptions.wizard(self.options, config),
- InternalArgs.wizard(self.args, config),
- (options, args) => ReadonlyArray.prepend(ReadonlyArray.appendAll(options, args), self.name)
- )))
+ 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 Effect.succeed(ReadonlyArray.empty())
+ return { _tag: "SingleCommandWizard", command: self }
}
case "Map": {
- return wizardInternal(self.command as Instruction, config)
+ return getWizardCommandSequence(self.command as Instruction)
}
case "OrElse": {
- const description = InternalHelpDoc.p("Select which command you would like to execute")
- const makeChoice = (title: string, value: Instruction) => ({
- title,
- value: [title, value] as const
- })
- const choices = ReadonlyArray.compact([
- Option.map(
- ReadonlyArray.head(Array.from(getNamesInternal(self.left as Instruction))),
- (title) => makeChoice(title, self.left as Instruction)
- ),
- Option.map(
- ReadonlyArray.head(Array.from(getNamesInternal(self.right as Instruction))),
- (title) => makeChoice(title, self.right as Instruction)
- )
- ])
- const message = InternalHelpDoc.toAnsiText(description).trimEnd()
- return Console.log().pipe(
- Effect.zipRight(InternalSelectPrompt.select({ message, choices })),
- Effect.flatMap(([name, command]) =>
- wizardInternal(command, config).pipe(Effect.map(ReadonlyArray.prepend(name)))
- )
- )
+ 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 description = InternalHelpDoc.p("Select which command you would like to execute")
- const makeChoice = (title: string, value: Instruction) => ({ title, value })
- const parentName = Option.getOrElse(
- ReadonlyArray.head(Array.from(getNamesInternal(self))),
- () => ""
- )
- const parentChoice = makeChoice(parentName, self.parent as Instruction)
- const childChoices = ReadonlyArray.map(
- Array.from(getSubcommandsInternal(self)),
- ([name, command]) => makeChoice(name, command as Instruction)
- )
- const choices = ReadonlyArray.prepend(childChoices, parentChoice)
- const message = InternalHelpDoc.toAnsiText(description).trimEnd()
- return Console.log().pipe(
- Effect.zipRight(InternalSelectPrompt.select({ message, choices })),
- Effect.flatMap((command) => wizardInternal(command, config))
+ 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 }
}
}
}
@@ -995,7 +1039,6 @@ const traverseCommand = (
const parentNames = Array.from(getNamesInternal(self.parent as Instruction))
const nextSubcommands = Array.from(getSubcommandsInternal(self.child as Instruction))
const nextParentCommands = ReadonlyArray.appendAll(parentCommands, parentNames)
- console.log(self.parent, self.child)
// Traverse the parent command using old parent names and next subcommands
return loop(self.parent as Instruction, parentCommands, nextSubcommands, level).pipe(
Effect.zipRight(
@@ -1347,3 +1390,19 @@ const getZshSubcommandCases = (
}
}
}
+
+// 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
+}
diff --git a/src/internal/options.ts b/src/internal/options.ts
index 4228b34..4c2c788 100644
--- a/src/internal/options.ts
+++ b/src/internal/options.ts
@@ -1350,7 +1350,7 @@ const parseInternal = (
}
}
-const wizardHeader = InternalHelpDoc.p("OPTIONS WIZARD")
+const wizardHeader = InternalHelpDoc.p("Option Wizard")
const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect<
FileSystem.FileSystem | Terminal.Terminal,
@@ -1417,12 +1417,13 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.
(title) => makeChoice(title, self.right as Instruction)
)
])
- return Console.log().pipe(Effect.zipRight(
- InternalSelectPrompt.select({
+ return Console.log().pipe(
+ Effect.zipRight(InternalSelectPrompt.select({
message: InternalHelpDoc.toAnsiText(message).trimEnd(),
choices
- }).pipe(Effect.flatMap((option) => wizardInternal(option, config)))
- ))
+ })),
+ Effect.flatMap((option) => wizardInternal(option, config))
+ )
}
case "Variadic": {
const repeatHelp = InternalHelpDoc.p(
@@ -1460,15 +1461,13 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.
InternalHelpDoc.sequence(defaultHelp)
)
return Console.log().pipe(
- Effect.zipRight(
- InternalSelectPrompt.select({
- message: InternalHelpDoc.toAnsiText(message).trimEnd(),
- choices: [
- { title: `Default ['${JSON.stringify(self.fallback)}']`, value: true },
- { title: "Custom", value: false }
- ]
- })
- ),
+ Effect.zipRight(InternalSelectPrompt.select({
+ message: InternalHelpDoc.toAnsiText(message).trimEnd(),
+ choices: [
+ { title: `Default ['${JSON.stringify(self.fallback)}']`, value: true },
+ { title: "Custom", value: false }
+ ]
+ })),
Effect.flatMap((useFallback) =>
useFallback
? Effect.succeed(ReadonlyArray.empty())
diff --git a/src/internal/primitive.ts b/src/internal/primitive.ts
index 7f2adef..0cc0462 100644
--- a/src/internal/primitive.ts
+++ b/src/internal/primitive.ts
@@ -539,7 +539,7 @@ const wizardInternal = (self: Instruction, help: HelpDoc.HelpDoc): Prompt.Prompt
case "Integer": {
const primitiveHelp = InternalHelpDoc.p("Enter an integer")
const message = InternalHelpDoc.sequence(help, primitiveHelp)
- return InternalNumberPrompt.float({
+ return InternalNumberPrompt.integer({
message: InternalHelpDoc.toAnsiText(message).trimEnd()
}).pipe(InternalPrompt.map((value) => `${value}`))
}
diff --git a/src/internal/validationError.ts b/src/internal/validationError.ts
index dca010a..7ac8c5c 100644
--- a/src/internal/validationError.ts
+++ b/src/internal/validationError.ts
@@ -8,7 +8,8 @@ export const ValidationErrorTypeId: ValidationError.ValidationErrorTypeId = Symb
ValidationErrorSymbolKey
) as ValidationError.ValidationErrorTypeId
-const proto: ValidationError.ValidationError.Proto = {
+/** @internal */
+export const proto: ValidationError.ValidationError.Proto = {
[ValidationErrorTypeId]: ValidationErrorTypeId
}
@@ -26,6 +27,11 @@ export const isCorrectedFlag = (
self: ValidationError.ValidationError
): self is ValidationError.CorrectedFlag => self._tag === "CorrectedFlag"
+/** @internal */
+export const isHelpRequested = (
+ self: ValidationError.ValidationError
+): self is ValidationError.HelpRequested => self._tag === "HelpRequested"
+
/** @internal */
export const isInvalidArgument = (
self: ValidationError.ValidationError
diff --git a/test/Command.test.ts b/test/Command.test.ts
index ae0c530..c96cee2 100644
--- a/test/Command.test.ts
+++ b/test/Command.test.ts
@@ -307,9 +307,9 @@ describe("Command", () => {
|
| - child1 help 1
|
- | - child2 child1 help 2
+ | - child1 child2 help 2
|
- | - child3 child1 help 3
+ | - child1 child3 help 3
|`
))
})