Subcommands
Real CLIs are usually a tree: git commit, git push, docker run, npm install. In Effect you build that tree by defining each child as its own
Command, then attaching them to a parent with Command.withSubcommands. The
parent acts as a namespace and a place to declare flags shared by all children.
The example below builds a tasks tool with create and list subcommands.
The parent declares shared flags (--workspace, --verbose) that every
subcommand can read, and each subcommand reads them by yield*-ing the parent
command inside its handler.
import { NodeRuntime, NodeServices } from "@effect/platform-node"import { Console, Effect } from "effect"import { Argument, Command, Flag } from "effect/unstable/cli"
// Flags can be defined once and reused across commands.const workspace = Flag.string("workspace").pipe( Flag.withAlias("w"), Flag.withDescription("Workspace to operate on"), Flag.withDefault("personal"))
// The root command. `withSharedFlags` makes these flags available to the root// handler *and* to every descendant — and accepts them before or after a// subcommand name (npm-style: `tasks --workspace x list` or `tasks list -w x`).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"))
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 }) { // Read the parent's shared flags by yielding the parent command. `root` is // typed as `{ workspace: string; verbose: boolean }`. 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") ) }, Effect.fn(function*({ status }) { const root = yield* tasks yield* Console.log(`Listing ${status} tasks in ${root.workspace}`) })).pipe( Command.withDescription("List tasks"), // Give the command a shorter alternative name: `tasks ls`. Command.withAlias("ls"))
// Attach the children, then run the whole tree as one executable.tasks.pipe( Command.withSubcommands([create, list]), Command.run({ version: "1.0.0" }), Effect.provide(NodeServices.layer), NodeRuntime.runMain)A few invocations and what they do:
tasks --workspace team-a create "Fix bug" --priority high— runscreatewithworkspace: "team-a".tasks create "Fix bug" -w team-a— identical: shared flags are accepted after the subcommand name too.tasks ls --status done— runslistvia its alias.tasks --helpandtasks create --help— generated help for the tree and for a single subcommand.
How subcommands read parent input
Section titled “How subcommands read parent input”A Command is itself an Effect whose success value is the parent’s parsed
input. So inside a child handler, yield* parentCommand gives you back the
parent’s shared flags, fully typed. This is type-safe in both directions:
- Only flags declared with
withSharedFlagsare visible to children. Plain config on the parent stays local and is not inherited — a child cannot accidentally depend on it. - Effect tracks the dependency at the type level, so if a child yields a parent that was never wired up as its ancestor, the types won’t line up.
Composing larger trees
Section titled “Composing larger trees”withSubcommands takes an array, and the result is just another Command, so
you can nest arbitrarily deep — attach subcommands to a command that is itself a
subcommand of something else. Effect handles selection (only the first
non-flag token opens a subcommand), -- to stop option parsing, and friendly
“did you mean…?” suggestions for misspelled subcommands automatically.
Because every handler is an ordinary Effect, a subcommand can do everything any
Effect can: depend on services, acquire
scoped resources, fail with
typed errors, and read
configuration. Provide those dependencies on the final
Command.run Effect, or per-command with Command.provide.
Testing a command tree
Section titled “Testing a command tree”Command.runWith is the same as Command.run but takes an explicit argument
array instead of reading process.argv. That makes commands easy to exercise in
tests without spawning a process.
import { Effect } from "effect"import { Command } from "effect/unstable/cli"
const program = Effect.gen(function*() { const run = Command.runWith( tasks.pipe(Command.withSubcommands([create, list])), { version: "1.0.0" } )
// Drive the CLI with explicit argv-style arrays. yield* run(["--workspace", "team-a", "create", "Ship 4.0", "--priority", "high"]) yield* run(["list", "--status", "done"]) yield* run(["--help"])})Provide the platform services (NodeServices.layer, or a test-friendly Stdio
layer) when you run program, just as you would in production.