diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a60bdc9..47575ca 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,6 @@ /* eslint-disable no-undef */ module.exports = { - ignorePatterns: ["dist", "build", "docs", "*.md"], + ignorePatterns: ["dist", "build", "*.md"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: 2018, diff --git a/examples/git.ts b/examples/git.ts index 969a611..330c914 100644 --- a/examples/git.ts +++ b/examples/git.ts @@ -4,6 +4,7 @@ import * as Command from "@effect/cli/Command" import * as HelpDoc from "@effect/cli/HelpDoc" import * as Span from "@effect/cli/HelpDoc/Span" import * as Options from "@effect/cli/Options" +import * as NodeContext from "@effect/platform-node/NodeContext" import * as Data from "effect/Data" import * as Effect from "effect/Effect" import { pipe } from "effect/Function" @@ -131,8 +132,7 @@ const cli = CliApp.make({ footer: HelpDoc.p("Copyright 2023") }) -pipe( - Effect.sync(() => process.argv.slice(2)), +Effect.sync(() => process.argv.slice(2)).pipe( Effect.flatMap((args) => CliApp.run(cli, args, (command) => Option.match(command.subcommand, { @@ -143,5 +143,6 @@ pipe( onSome: handleGitSubcommand })) ), + Effect.provide(NodeContext.layer), Effect.runFork ) diff --git a/examples/prompt.ts b/examples/prompt.ts index 732405f..3560375 100644 --- a/examples/prompt.ts +++ b/examples/prompt.ts @@ -1,7 +1,8 @@ import * as CliApp from "@effect/cli/CliApp" import * as Command from "@effect/cli/Command" import * as Prompt from "@effect/cli/Prompt" -import { Effect } from "effect" +import * as NodeContext from "@effect/platform-node/NodeContext" +import * as Effect from "effect/Effect" const colorPrompt = Prompt.select({ message: "Pick your favorite color!", @@ -26,5 +27,6 @@ const cli = CliApp.make({ Effect.sync(() => process.argv.slice(2)).pipe( Effect.flatMap((args) => CliApp.run(cli, args, (input) => Effect.log(input))), + Effect.provide(NodeContext.layer), Effect.runFork ) diff --git a/src/Options.ts b/src/Options.ts index 0a13415..649e3eb 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -221,7 +221,7 @@ export const date: (name: string) => Options = InternalOptions. * @since 1.0.0 * @category constructors */ -export const directory: (name: string, config: Options.PathOptionsConfig) => Options = +export const directory: (name: string, config?: Options.PathOptionsConfig) => Options = InternalOptions.directory /** @@ -230,7 +230,7 @@ export const directory: (name: string, config: Options.PathOptionsConfig) => Opt * @since 1.0.0 * @category constructors */ -export const file: (name: string, config: Options.PathOptionsConfig) => Options = +export const file: (name: string, config?: Options.PathOptionsConfig) => Options = InternalOptions.file /** diff --git a/src/internal/builtInOptions.ts b/src/internal/builtInOptions.ts index 3b3dae0..7523f9d 100644 --- a/src/internal/builtInOptions.ts +++ b/src/internal/builtInOptions.ts @@ -5,8 +5,8 @@ import type * as HelpDoc from "../HelpDoc.js" import type * as Options from "../Options.js" import type * as ShellType from "../ShellType.js" import type * as Usage from "../Usage.js" -import * as options from "./options.js" -import * as _shellType from "./shellType.js" +import * as InternalOptions from "./options.js" +import * as InternalShellType from "./shellType.js" /** @internal */ export const showCompletions = ( @@ -69,21 +69,23 @@ export const builtInOptions = ( usage: Usage.Usage, helpDoc: HelpDoc.HelpDoc ): Options.Options> => { - const help = options.boolean("help").pipe(options.withAlias("h")) - // TODO: after path/file primitives added - // const completionScriptPath = options.optional(options.file("shell-completion-script")) - const shellCompletionScriptPath = options.optional(options.text("shell-completion-script")) - const shellType = options.optional(_shellType.shellOption) - const shellCompletionIndex = options.optional(options.integer("shell-completion-index")) - const wizardOption = options.boolean("wizard") - const option = options.all({ + const help = InternalOptions.boolean("help").pipe(InternalOptions.withAlias("h")) + const shellCompletionScriptPath = InternalOptions.optional( + InternalOptions.file("shell-completion-script") + ) + const shellType = InternalOptions.optional(InternalShellType.shellOption) + const shellCompletionIndex = InternalOptions.optional( + InternalOptions.integer("shell-completion-index") + ) + const wizard = InternalOptions.boolean("wizard") + const option = InternalOptions.all({ shellCompletionScriptPath, shellType, shellCompletionIndex, help, - wizard: wizardOption + wizard }) - return options.map(option, (builtIn) => { + return InternalOptions.map(option, (builtIn) => { if (builtIn.help) { return Option.some(showHelp(usage, helpDoc)) } diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts index cbcac9d..73255e8 100644 --- a/src/internal/cliApp.ts +++ b/src/internal/cliApp.ts @@ -70,21 +70,17 @@ export const run = dual< args: ReadonlyArray, execute: (a: A) => Effect.Effect ): Effect.Effect => - Effect.gen(function*(_) { - const context = yield* _(Effect.context()) - + Effect.contextWithEffect((context: Context.Context) => { // Attempt to parse the CliConfig from the environment, falling back to the // default CliConfig if none was provided const config = Option.getOrElse( Context.getOption(context, InternalCliConfig.Tag), () => InternalCliConfig.defaultConfig ) - // Prefix the command name to the command line arguments const prefixedArgs = ReadonlyArray.appendAll(prefixCommand(self.command), args) - // Handle the command - return yield* _(Effect.matchEffect(self.command.parse(prefixedArgs, config), { + return Effect.matchEffect(self.command.parse(prefixedArgs, config), { onFailure: (e) => Effect.zipRight(printDocs(e.error), Effect.fail(e)), onSuccess: unify((directive) => { switch (directive._tag) { @@ -102,7 +98,7 @@ export const run = dual< } } }) - })) + }) }).pipe(Effect.provide(MainLive))) // ============================================================================= diff --git a/src/internal/command.ts b/src/internal/command.ts index 402e1bf..8d3db84 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -112,12 +112,12 @@ export class Standard Effect.when(() => normalizedArgv0 === normalizedCommandName), Effect.flatten, Effect.catchTag("NoSuchElementException", () => { - const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + const error = InternalHelpDoc.p(`Missing command name: '${this.name}'`) return Effect.fail(InternalValidationError.commandMismatch(error)) }) ) } - const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + const error = InternalHelpDoc.p(`Missing command name: '${this.name}'`) return Effect.fail(InternalValidationError.commandMismatch(error)) } const parseBuiltInArgs = ( @@ -127,20 +127,23 @@ export class Standard ValidationError.ValidationError, CommandDirective.CommandDirective > => { - const normalizedArgv0 = InternalCliConfig.normalizeCase(config, args[0]) - const normalizedCommandName = InternalCliConfig.normalizeCase(config, this.name) - if (normalizedArgv0 === normalizedCommandName) { - const options = InternalBuiltInOptions.builtInOptions(this, this.usage, this.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) - ) + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const argv0 = ReadonlyArray.headNonEmpty(args) + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, this.name) + if (normalizedArgv0 === normalizedCommandName) { + const options = InternalBuiltInOptions.builtInOptions(this, this.usage, this.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: ${this.name}`) + const error = InternalHelpDoc.p(`Missing command name: '${this.name}'`) return Effect.fail(InternalValidationError.commandMismatch(error)) } const parseUserDefinedArgs = ( @@ -188,7 +191,7 @@ export class Standard if (ReadonlyArray.contains(args, "--wizard") || ReadonlyArray.contains(args, "-w")) { return parseBuiltInArgs(ReadonlyArray.make(this.name, "--wizard")) } - const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + const error = InternalHelpDoc.p(`Missing command name: '${this.name}'`) return Effect.fail(InternalValidationError.commandMismatch(error)) } return parseBuiltInArgs(args).pipe( @@ -218,6 +221,7 @@ export class Standard } } +/** @internal */ export class GetUserInput implements Command.Command> { @@ -361,11 +365,11 @@ export class OrElse implements Command.Command { CommandDirective.CommandDirective > { return this.left.parse(args, config).pipe( - Effect.catchSome((e) => - InternalValidationError.isValidationError(e) + Effect.catchSome((e) => { + return InternalValidationError.isCommandMismatch(e) ? Option.some(this.right.parse(args, config)) : Option.none() - ) + }) ) } diff --git a/src/internal/options.ts b/src/internal/options.ts index 0e79e80..59c5eed 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -795,7 +795,7 @@ export const date = (name: string): Options.Options => /** @internal */ export const directory = ( name: string, - config: Options.Options.PathOptionsConfig + config: Options.Options.PathOptionsConfig = {} ): Options.Options => new Single( name, @@ -806,7 +806,7 @@ export const directory = ( /** @internal */ export const file = ( name: string, - config: Options.Options.PathOptionsConfig + config: Options.Options.PathOptionsConfig = {} ): Options.Options => new Single( name, diff --git a/test/Options.test.ts b/test/Options.test.ts index aadd850..bab8a48 100644 --- a/test/Options.test.ts +++ b/test/Options.test.ts @@ -84,7 +84,6 @@ describe("Options", () => { ) const args = ReadonlyArray.make("-a", "a", "-b", "b") const result = yield* _(Effect.flip(validation(options, args, CliConfig.defaultConfig))) - console.log(result) expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( "Collision between two options detected - you can only " + "specify one of either: ['-a', '-b']" diff --git a/tsconfig.json b/tsconfig.json index 3d63f6f..00da6c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,9 @@ }, { "path": "./tsconfig.test.json" + }, + { + "path": "./tsconfig.examples.json" } ] }