The effect/unstable/cli module provides a type-safe framework for building command-line applications. Define commands with arguments and flags, compose them into hierarchical structures, and let Effect handle parsing and validation.
Basic command structure
Commands are built using Command.make and can include typed arguments, flags, and handlers.
import { Console, Effect } from "effect"
import { Argument, Command, Flag } from "effect/unstable/cli"
const greet = Command.make(
"greet",
{
name: Argument.string("name").pipe(
Argument.withDescription("Name to greet")
),
loud: Flag.boolean("loud").pipe(
Flag.withDescription("Use uppercase")
)
},
Effect.fn(function*({ name, loud }) {
const message = `Hello, ${name}!`
yield* Console.log(loud ? message.toUpperCase() : message)
})
)
Flags and arguments
Flags
Flags are optional parameters that modify command behavior. They support various types and can be reused across commands.
// Boolean flags
const verbose = Flag.boolean("verbose").pipe(
Flag.withAlias("v"),
Flag.withDescription("Print diagnostic output")
)
// String flags with defaults
const workspace = Flag.string("workspace").pipe(
Flag.withAlias("w"),
Flag.withDescription("Workspace to operate on"),
Flag.withDefault("personal")
)
// Choice flags for enum-like values
const priority = Flag.choice("priority", ["low", "normal", "high"]).pipe(
Flag.withDescription("Task priority"),
Flag.withDefault("normal")
)
Arguments
Arguments are positional parameters that must be provided by the user.
// Single argument
const title = Argument.string("title").pipe(
Argument.withDescription("Task title")
)
// Variadic arguments (accept multiple values)
const files = Argument.string("files").pipe(
Argument.variadic(),
Argument.withDescription("Files to process")
)
Shared flags and subcommands
Create a root command with shared flags that are available to all subcommands.
import { NodeRuntime, NodeServices } from "@effect/platform-node"
import { Console, Effect } from "effect"
import { Argument, Command, Flag } from "effect/unstable/cli"
// Define reusable flags
const workspace = Flag.string("workspace").pipe(
Flag.withAlias("w"),
Flag.withDescription("Workspace to operate on"),
Flag.withDefault("personal")
)
// Root command with shared flags
const tasks = Command.make("tasks").pipe(
Command.withSharedFlags({
workspace,
verbose: Flag.boolean("verbose").pipe(
Flag.withAlias("v"),
Flag.withDescription("Print diagnostic output")
)
}),
Command.withDescription("Track and manage tasks")
)
// Subcommand that accesses parent flags
const create = Command.make(
"create",
{
title: Argument.string("title").pipe(
Argument.withDescription("Task title")
),
priority: Flag.choice("priority", ["low", "normal", "high"]).pipe(
Flag.withDescription("Priority for the new task"),
Flag.withDefault("normal")
)
},
Effect.fn(function*({ title, priority }) {
// Access parent command input by yielding the parent
const root = yield* tasks
if (root.verbose) {
yield* Console.log(`workspace=${root.workspace} action=create`)
}
yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
})
).pipe(
Command.withDescription("Create a task"),
Command.withExamples([{
command: "tasks create \"Ship 4.0\" --priority high",
description: "Create a high-priority task"
}])
)
const list = Command.make(
"list",
{
status: Flag.choice("status", ["open", "done", "all"]).pipe(
Flag.withDescription("Filter tasks by status"),
Flag.withDefault("open")
),
json: Flag.boolean("json").pipe(
Flag.withDescription("Print machine-readable output")
)
},
Effect.fn(function*({ status, json }) {
const root = yield* tasks
const items = [
{ title: "Ship 4.0", status: "open" },
{ title: "Update onboarding guide", status: "done" }
] as const
const filtered = status === "all"
? items
: items.filter((item) => item.status === status)
if (root.verbose) {
yield* Console.log(`workspace=${root.workspace} action=list`)
}
if (json) {
yield* Console.log(JSON.stringify({
workspace: root.workspace,
status,
items: filtered
}, null, 2))
return
}
yield* Console.log(`Listing ${status} tasks in ${root.workspace}`)
for (const item of filtered) {
yield* Console.log(`- ${item.title}`)
}
})
).pipe(
Command.withDescription("List tasks"),
Command.withAlias("ls")
)
// Compose and run the CLI
tasks.pipe(
Command.withSubcommands([create, list]),
Command.run({ version: "1.0.0" }),
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)
Enhance your CLI with descriptions, examples, and aliases.
const command = Command.make("deploy", config, handler).pipe(
Command.withDescription("Deploy your application"),
Command.withAlias("d"),
Command.withExamples([
{
command: "deploy --env production",
description: "Deploy to production"
},
{
command: "deploy --env staging --force",
description: "Force deploy to staging"
}
])
)
Running commands
Use Command.run to execute your command with process arguments. The CLI automatically generates help text and handles errors.
import { NodeRuntime, NodeServices } from "@effect/platform-node"
command.pipe(
Command.run({
version: "1.0.0"
}),
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)
The CLI framework provides:
- Automatic
--help and --version flags
- Input validation with helpful error messages
- Type-safe access to arguments and flags
- Hierarchical command structures with shared configuration
The CLI module is in the unstable namespace. The API may change in future versions.