Security News
Oracle Drags Its Feet in the JavaScript Trademark Dispute
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
@effect/cli
Advanced tools
You can install @effect/cli
using your preferred package manager:
npm install @effect/cli
# or
pnpm add @effect/cli
# or
yarn add @effect/cli
You will also need to install one of the platform-specific @effect/platform
packages based on where you intend to run your command-line application.
This is because @effect/cli
must interact with many platform-specific services to function, such as the file system and the terminal.
For example, if your command-line application will run in a NodeJS environment:
npm install @effect/platform-node
# or
pnpm add @effect/platform-node
# or
yarn add @effect/platform-node
You can then provide the NodeContext.layer
exported from @effect/platform-node
to your command-line application to ensure that @effect/cli
has access to all the platform-specific services that it needs.
For a more detailed walkthrough, take a read through the Tutorial below.
All Effect CLI programs ship with several built-in options:
[--completions (bash | sh | fish | zsh)]
- automatically generates and displays a shell completion script for your CLI application[-h | --help]
- automatically generates and displays a help documentation for your CLI application[--version]
- automatically displays the version of the CLI application[--wizard]
- starts the Wizard Mode for your CLI application which guides a user through constructing a command for your the CLI applicationIn this quick start guide, we are going to attempt to replicate a small part of the Git Distributed Version Control System command-line interface (CLI) using @effect/cli
.
Specifically, our goal will be to build a CLI application which replicates the following subset of the git
CLI which we will call minigit
:
minigit [-v | --version] [-h | --help] [-c <name>=<value>]
minigit add [-v | --verbose] [--] [<pathspec>...]
minigit clone [--depth <depth>] [--] <repository> [<directory>]
NOTE: During this quick start guide, we will focus on building the components of the CLI application that will allow us to parse the above commands into structured data. However, implementing the functionality of these commands is out of the scope of this quick start guide.
The CLI application that will be built during this tutorial is also available in the examples.
For our minigit
CLI, we have three commands that we would like to model. Let's start by using @effect/cli
to create a basic Command
to represent our top-level minigit
command.
The Command.make
constructor creates a Command
from a name, a Command
Config
object, and a Command
handler, which is a function that receives the parsed Config
and actually executes the Command
. Each of these parameters is also reflected in the type signature of Command
:
Command<Name extends string, R, E, A>
has four type arguments:
Name extends string
: the name of the commandR
: the environment required by the Command
's handlerE
: the expected errors returned by the Command
's handlerA
: the parsed Config
object provided to the Command
's handlerLet's take a look at each of parameter in more detail:
Command Name
The first parameter to Command.make
is the name of the Command
. This is the name that will be used to parse the Command
from the command-line arguments.
For example, if we have a CLI application called my-cli-app
with a single subcommand named foo
, then executing the following command will run the foo
Command
in your CLI application:
my-cli-app foo
Command Configuration
The second parameter to Command.make
is the Command
Config
. The Config
is an object of key/value pairs where the keys are just identifiers and the values are the Options
and Args
that the Command
may receive. The Config
object can have nested Config
objects or arrays of Config
objects.
When the CLI application is actually executed, the Command
Config
is parsed from the command-line options and arguments following the Command
name.
Command Handler
The Command
handler is an effectful function that receives the parsed Config
and returns an Effect
. This allows the user to execute the code associated with their Command
with the full power of Effect.
Returning to our minigit
CLI application, let's use what we've learned about Command.make
to create the top-level minigit
Command
:
import { Command, Options } from "@effect/cli"
import { Console, Effect, Option } from "effect"
// minigit [--version] [-h | --help] [-c <name>=<value>]
const configs = Options.keyValueMap("c").pipe(Options.optional)
const minigit = Command.make("minigit", { configs }, ({ 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}`)
}
}))
Some things to note in the above example:
Command
and Options
modules from @effect/cli
Console
and Option
modules from the core effect
packageOptions
object which will allow us to parse key=value
pairs with the -c
flag-c
flag an optional option using the Options.optional
combinatorCommand
named minigit
and passed configs
Options
to the minigit
command Config
Command
Config
for minigit
to execute code based upon whether the optional -c
flag was providedAn astute observer may have also noticed that in the snippet above we did not specify Options
for version and help.
This is because Effect CLI has several built-in options (see Built-In Options for more information) which are available automatically for all CLI applications built with @effect/cli
.
Let's continue with our minigit
example and and create the add
and clone
subcommands:
import { Args, Command, Options } from "@effect/cli"
import { Console, Option, ReadonlyArray } from "effect"
// minigit [--version] [-h | --help] [-c <name>=<value>]
const configs = Options.keyValueMap("c").pipe(Options.optional)
const minigit = Command.make("minigit", { configs }, ({ 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] [--] [<pathspec>...]
const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated)
const verbose = Options.boolean("verbose").pipe(Options.withAlias("v"))
const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbose }) => {
const paths = ReadonlyArray.match(pathspec, {
onEmpty: () => "",
onNonEmpty: (paths) => ` ${ReadonlyArray.join(paths, " ")}`
})
return Console.log(`Running 'minigit add${paths}' with '--verbose ${verbose}'`)
})
// minigit clone [--depth <depth>] [--] <repository> [<directory>]
const minigitClone = Command.make("clone", { repository, directory, depth }, (config) => {
const depth = Option.map(config.depth, (depth) => `--depth ${depth}`)
const repository = Option.some(config.repository)
const optionsAndArgs = ReadonlyArray.getSomes([depth, repository, config.directory])
return Console.log(
"Running 'minigit clone' with the following options and arguments: " +
`'${ReadonlyArray.join(optionsAndArgs, ", ")}'`
)
})
Some things to note in the above example:
Args
module from @effect/cli
and the ReadonlyArray
module from effect
Args
module to specify some positional arguments for our add
and clone
subcommandsOptions.withAlias
to give the --verbose
flag an alias of -v
for our add
subcommandNow that we've specified all the Command
s our application can handle, let's compose them together so that we can actually run the CLI application.
For the purposes of this example, we will assume that our CLI application is running in a NodeJS environment and that we have previously installed @effect/platform-node
(see Installation).
Our final CLI application is as follows:
import { Args, Command, Options } from "@effect/cli"
import { NodeContext, Runtime } from "@effect/platform-node"
import { Console, Effect, Option, ReadonlyArray } from "effect"
// minigit [--version] [-h | --help] [-c <name>=<value>]
const configs = Options.keyValueMap("c").pipe(Options.optional)
const minigit = Command.make("minigit", { configs }, ({ 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] [--] [<pathspec>...]
const pathspec = Args.text({ name: "pathspec" }).pipe(Args.repeated)
const verbose = Options.boolean("verbose").pipe(Options.withAlias("v"))
const minigitAdd = Command.make("add", { pathspec, verbose }, ({ pathspec, verbose }) => {
const paths = ReadonlyArray.match(pathspec, {
onEmpty: () => "",
onNonEmpty: (paths) => ` ${ReadonlyArray.join(paths, " ")}`
})
return Console.log(`Running 'minigit add${paths}' with '--verbose ${verbose}'`)
})
// minigit clone [--depth <depth>] [--] <repository> [<directory>]
const minigitClone = Command.make("clone", { repository, directory, depth }, (config) => {
const depth = Option.map(config.depth, (depth) => `--depth ${depth}`)
const repository = Option.some(config.repository)
const optionsAndArgs = ReadonlyArray.getSomes([depth, repository, config.directory])
return Console.log(
"Running 'minigit clone' with the following options and arguments: " +
`'${ReadonlyArray.join(optionsAndArgs, ", ")}'`
)
})
const command = minigit.pipe(Command.withSubcommands([minigitAdd, minigitClone]))
const cli = Command.run(command, {
name: "Minigit Distributed Version Control",
version: "v1.0.0"
})
Effect.suspend(() => cli(process.argv.slice(2))).pipe(
Effect.provide(NodeContext.layer),
Runtime.runMain
)
Some things to note in the above example:
Effect
module from effect
Runtime
and NodeContext
modules from @effect/platform-node
Command.withSubcommands
to add our add
and clone
commands as subcommands of minigit
Command.run
to create a CliApp
with a name
and a version
Effect.suspend
to lazily evaluate process.argv
, passing all but the first two command-line arguments to our CLI application
node ./my-cli.js ...
At this point, we're ready to run our CLI application!
Let's assume that we've bundled our CLI into a single file called minigit.js
. However, if you are following along using the minigit
example in this repository, you can run the same commands with pnpm tsx ./examples/minigit.ts ...
.
Let's start by getting the version of our CLi application using the built-in --version
option.
> node ./minigit.js --version
v1.0.0
We can also print out help documentation for each of our application's commands using the -h | --help
built-in option.
For example, running the top-level command with --help
produces the following output:
> node ./minigit.js --help
Minigit Distributed Version Control v1.0.0
USAGE
$ minigit [-c text]
OPTIONS
-c text
A user-defined piece of text.
This setting is a property argument which:
- May be specified a single time: '-c key1=value key2=value2'
- May be specified multiple times: '-c key1=value -c key2=value2'
This setting is optional.
COMMANDS
- add [(-v, --verbose)] <pathspec>...
- clone [--depth integer] <repository> [<directory>]
Running the add
subcommand with --help
produces the following output:
> node ./minigit.js add --help
Minigit Distributed Version Control v1.0.0
USAGE
$ add [(-v, --verbose)] <pathspec>...
ARGUMENTS
<pathspec>...
A user-defined piece of text.
This argument may be repeated zero or more times.
OPTIONS
(-v, --verbose)
A true or false value.
This setting is optional.
We can also experiment with executing our own commands:
> node ./minigit.js add .
Running 'minigit add .' with '--verbose false'
> node ./minigit.js add --verbose .
Running 'minigit add .' with '--verbose true'
> node ./minigit.js clone --depth 1 https://github.com/Effect-TS/cli.git
Running 'minigit clone' with the following options and arguments: '--depth 1, https://github.com/Effect-TS/cli.git'
In certain scenarios, you may want your subcommands to have access to Options
/ Args
passed to their parent commands.
Because Command
is also a subtype of Effect
, you can directly Effect.map
, Effect.flatMap
a parent command in a subcommand's handler to extract it's Config
.
For example, let's say that our minigit clone
subcommand needs access to the configuration parameters that can be passed to the parent minigit
command via minigit -c key=value
. We can accomplish this by adjusting our clone
Command
handler to Effect.flatMap
the parent minigit
:
const repository = Args.text({ name: "repository" })
const directory = Args.directory().pipe(Args.optional)
const depth = Options.integer("depth").pipe(Options.optional)
const minigitClone = Command.make("clone", { repository, directory, depth }, (subcommandConfig) =>
// By using `Effect.flatMap` on the parent command, we get access to it's parsed config
Effect.flatMap(minigit, (parentConfig) => {
const depth = Option.map(subcommandConfig.depth, (depth) => `--depth ${depth}`)
const repository = Option.some(subcommandConfig.repository)
const optionsAndArgs = ReadonlyArray.getSomes([depth, repository, subcommandConfig.directory])
const configs = Option.match(parentConfig.configs, {
onNone: () => "",
onSome: (map) => Array.from(map).map(([key, value]) => `${key}=${value}`).join(", ")
})
return Console.log(
"Running 'minigit clone' with the following options and arguments: " +
`'${ReadonlyArray.join(optionsAndArgs, ", ")}'\n` +
`and the following configuration parameters: ${configs}`
)
})
)
In addition, accessing a parent command in the handler of a subcommand will add the parent Command
to the environment of the subcommand.
We can directly observe this by inspecting the type of minigitClone
after accessing the parent command:
const minigitClone: Command.Command<
"clone",
// The parent `minigit` command has been added to the environment required by
// the subcommand's handler
Command.Command.Context<"minigit">,
never,
{
readonly repository: string;
readonly directory: Option.Option<string>;
readonly depth: Option.Option<number>;
}
>
The parent command will be "erased" from the subcommand's environment when using Command.withSubcommands
:
const command = minigit.pipe(Command.withSubcommands([minigitClone]))
// ^? Command<"minigit", never, ..., ...>
We can run the command with some configuration parameters to see the final result:
> node ./minigit.js -c key1=value1 clone --depth 1 https://github.com/Effect-TS/cli.git
Running 'minigit clone' with the following options and arguments: '--depth 1, https://github.com/Effect-TS/cli.git'
and the following configuration parameters: key1=value1
At this point, we've completed our tutorial!
We hope that you enjoyed learning a little bit about Effect CLI, but this tutorial has only scratched surface!
We encourage you to continue exploring Effect CLI and all the features it provides!
Happy Hacking!
The internal command-line argument parser operates under the following specifications:
By default, the Options
/ Args
of a command are only recognized before subcommands
# -v is an option for program
program -v subcommand
# -v is an option for subcommand
program subcommand -v
The Options
for a Command
are always parsed before positional Args
# valid
program --option arg
# invalid
program arg --option
Excess arguments after the command-line is fully processed results in a ValidationError
FAQs
A library for building command-line interfaces with Effect
The npm package @effect/cli receives a total of 20,455 weekly downloads. As such, @effect/cli popularity was classified as popular.
We found that @effect/cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
Security News
The Linux Foundation is warning open source developers that compliance with global sanctions is mandatory, highlighting legal risks and restrictions on contributions.
Security News
Maven Central now validates Sigstore signatures, making it easier for developers to verify the provenance of Java packages.