effect-cli
SKILL.md
Structure
Command.run(command, { name, version })
├── Command (parent)
│ ├── Options (named flags: --verbose, --depth 5)
│ ├── Args (positional: <repo> <path>)
│ └── Command.withSubcommands([...])
│ ├── Command (child — can yield* parent for its config)
│ └── Command (child)
Minimal App
import { Command, Options, Args } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Console, Effect } from "effect"
const greet = Command.make(
"greet",
{
name: Args.text({ name: "name" }),
loud: Options.boolean("loud").pipe(Options.withAlias("l"))
},
({ name, loud }) =>
Console.log(loud ? name.toUpperCase() + "!" : `Hello, ${name}!`)
)
const cli = Command.run(greet, { name: "Greeter", version: "1.0.0" })
Effect.suspend(() => cli(process.argv)).pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
Commands
// No config
const root = Command.make("app")
// With config (record of Options + Args, arbitrarily nested)
const cmd = Command.make("cmd", {
verbose: Options.boolean("verbose"),
file: Args.text({ name: "file" })
}, ({ verbose, file }) => Console.log(file))
// Attach handler later
const cmd2 = Command.make("cmd", { n: Args.integer({ name: "n" }) }).pipe(
Command.withHandler(({ n }) => Console.log(n))
)
// Description
const cmd3 = cmd.pipe(Command.withDescription("Does something useful"))
Subcommands
const git = Command.make("git", {
verbose: Options.boolean("verbose").pipe(Options.withAlias("v"))
})
const clone = Command.make("clone", { repo: Args.text({ name: "repo" }) },
({ repo }) =>
Effect.gen(function* () {
const { verbose } = yield* git // access parent's parsed config
yield* Console.log(`Cloning ${repo}, verbose=${verbose}`)
})
)
const add = Command.make("add", { path: Args.text({ name: "path" }) },
({ path }) => Console.log(`Adding ${path}`)
)
const command = git.pipe(Command.withSubcommands([clone, add]))
Key pattern: subcommand handlers yield* parentCommand to access the parent's parsed config. This works because Command extends Effect.
Providing Services
cmd.pipe(Command.provide(MyService.layer))
cmd.pipe(Command.provide((config) => MyService.layer(config.path))) // config-dependent
cmd.pipe(Command.provideEffect(MyService, (_config) => Effect.succeed(impl)))
cmd.pipe(Command.provideSync(MyService, impl))
cmd.pipe(Command.provideEffectDiscard((_config) => Effect.log("Starting...")))
// Transform handler
cmd.pipe(Command.transformHandler((effect, config) =>
Effect.provideService(effect, MyService, impl)
))
Options (Named Flags)
For detailed options reference see effect-cli-options.md.
| Constructor | CLI syntax | Type |
|---|---|---|
Options.boolean("verbose") |
--verbose |
boolean |
Options.text("name") |
--name foo |
string |
Options.integer("depth") |
--depth 5 |
number |
Options.float("amount") |
--amount 3.14 |
number |
Options.date("when") |
--when 2024-01-01 |
Date |
Options.redacted("token") |
--token abc |
Redacted |
Options.choice("fmt", ["json", "csv"]) |
--fmt json |
"json" | "csv" |
Options.file("config") |
--config ./f.json |
string |
Options.directory("out") |
--out ./dist |
string |
Options.fileText("input") |
--input ./f.txt |
[path, string] |
Options.fileSchema("cfg", S) |
--cfg ./f.json |
Schema.Type<S> |
Options.keyValueMap("c") |
-c k=v -c k2=v2 |
HashMap<string,string> |
Common Combinators
Options.text("name").pipe(
Options.withAlias("n"), // -n shorthand
Options.withDescription("Your name"),
Options.optional, // Option<string>
// or: Options.withDefault("world"), // string with default
)
// Repetition
Options.text("tag").pipe(Options.repeated) // Array<string>
Options.text("tag").pipe(Options.atLeast(1)) // NonEmptyArray<string>
// Schema validation
Options.text("balance").pipe(Options.withSchema(Schema.BigDecimal))
// Fallback to env var (via Effect Config)
Options.integer("port").pipe(Options.withFallbackConfig(Config.integer("PORT")))
// Fallback to interactive prompt
Options.text("name").pipe(
Options.withFallbackPrompt(Prompt.text({ message: "Enter name:" }))
)
Args (Positional Arguments)
For detailed args reference see effect-cli-args.md.
| Constructor | Type |
|---|---|
Args.text({ name: "repo" }) |
string |
Args.integer({ name: "n" }) |
number |
Args.float({ name: "x" }) |
number |
Args.boolean() |
boolean |
Args.date() |
Date |
Args.file() |
string |
Args.file({ exists: "yes" }) |
string (must exist) |
Args.directory() |
string |
Args.fileText() |
[path, string] |
Args.fileSchema(MySchema) |
Schema.Type<S> |
Common Combinators
Args.text({ name: "dir" }).pipe(
Args.optional, // Option<string>
// or: Args.withDefault("/"), // string with default
// or: Args.repeated, // Array<string>
// or: Args.atLeast(1), // NonEmptyArray<string>
)
Args.text({ name: "n" }).pipe(Args.withSchema(Schema.NumberFromString))
Args.text({ name: "r" }).pipe(Args.withFallbackConfig(Config.string("REPO")))
Args.text({ name: "r" }).pipe(Args.withDescription("The repository URL"))
Prompts
For detailed prompt reference see effect-cli-prompt.md.
import { Prompt } from "@effect/cli"
Prompt.text({ message: "Name:", default: "Alice" }) // string
Prompt.password({ message: "Password:" }) // Redacted
Prompt.integer({ message: "Age:", min: 0, max: 150 }) // number
Prompt.confirm({ message: "Sure?" }) // boolean
Prompt.toggle({ message: "Enable?", active: "on", inactive: "off" }) // boolean
Prompt.select({
message: "Pick env:",
choices: [
{ title: "Production", value: "prod" },
{ title: "Staging", value: "staging" },
{ title: "Dev", value: "dev" }
]
}) // string
Prompt.list({ message: "Tags:", delimiter: "," }) // Array<string>
// Combine prompts
Prompt.all({ name: namePrompt, age: agePrompt })
Prompt-Based Commands
const favorites = Command.prompt(
"favorites",
Prompt.all([
Prompt.select({ message: "Color?", choices: [...] }),
Prompt.confirm({ message: "Continue?" })
]),
([color, confirmed]) => Console.log(`Color: ${color}`)
)
Running
const cli = Command.run(command, {
name: "My App",
version: "1.0.0",
// summary: Span.text("Short summary"),
// footer: HelpDoc.p("Footer text"),
})
// cli: (args: ReadonlyArray<string>) => Effect<void, ...>
Effect.suspend(() => cli(process.argv)).pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
process.argv is automatically stripped of the node executable and script path.
Built-In Options (Automatic)
Every CLI app gets these for free:
| Flag | Effect |
|---|---|
-h, --help |
Print auto-generated help |
--version |
Print version |
--wizard |
Interactive wizard for all options/args |
--completions bash|fish|zsh |
Shell completion scripts |
--log-level debug|info|warn|error |
Set log level |
Config File Support
import { ConfigFile } from "@effect/cli"
Effect.suspend(() => cli(process.argv)).pipe(
Effect.provide(
Layer.mergeAll(
NodeContext.layer,
ConfigFile.layer("myapp") // reads myapp.json, myapp.yaml, etc.
)
),
NodeRuntime.runMain
)
Options using withFallbackConfig will read from the config file.
Config Precedence
CLI args > withFallbackConfig (env/config file) > withFallbackPrompt (interactive) > withDefault (static)
Additional Resources
Detailed Options reference Detailed Args reference Detailed Prompt reference
Weekly Installs
24
Repository
tstelzer/skillsFirst Seen
Feb 16, 2026
Security Audits
Installed on
cursor24
opencode23
codex23
claude-code21
github-copilot21
gemini-cli20