Skip to main content
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
)

Command metadata

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.

Build docs developers (and LLMs) love