Claude-skill-registry cli-core

Core patterns for Effect CLI - Command.make, Args, Options, subcommands, and program structure. Foundation skill for TMNL CLI framework.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cli-core" ~/.claude/skills/majiayu000-claude-skill-registry-cli-core && rm -rf "$T"
manifest: skills/data/cli-core/SKILL.md
source content

CLI Core Patterns

Foundation patterns for building CLIs with

@effect/cli
. Part of the TMNL CLI Framework.

Quick Start

#!/usr/bin/env bun
import { Args, Command, Options } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Console, Effect, pipe } from "effect"

// Define command
const greet = Command.make(
  "greet",
  { name: Args.text({ name: "name" }) },
  ({ name }) => Console.log(`Hello, ${name}!`)
)

// Run
pipe(
  Command.run(greet, { name: "myapp", version: "1.0.0" }),
  (cli) => cli(process.argv),
  Effect.provide(NodeContext.layer),
  NodeRuntime.runMain
)

Command Definition

Basic Command

const myCommand = Command.make(
  "command-name",           // Command name (used in help)
  { /* config object */ },  // Args and Options
  (config) => Effect.gen(function* () {
    // Handler receives parsed config
    yield* Console.log(`Got: ${config.someArg}`)
  })
)

Config Object Structure

The second parameter defines what the command accepts:

{
  // Positional arguments
  target: Args.text({ name: "target" }),

  // Named options
  verbose: Options.boolean("verbose").pipe(Options.withAlias("v")),

  // Optional values
  count: Options.integer("count").pipe(Options.optional),
}

Arguments (Args)

Positional parameters passed after the command.

Text Argument

const target = Args.text({ name: "target" })
// Usage: mycli <target>

Integer Argument

const count = Args.integer({ name: "count" })
// Usage: mycli <count>

Optional Argument

const maybeFile = Args.text({ name: "file" }).pipe(Args.optional)
// Usage: mycli [file]
// Returns: Option<string>

Repeated Arguments

const files = Args.text({ name: "files" }).pipe(Args.repeated)
// Usage: mycli file1.txt file2.txt file3.txt
// Returns: Chunk<string>

With Description

const target = Args.text({ name: "target" }).pipe(
  Args.withDescription("The target file or directory")
)

With Default

const format = Args.text({ name: "format" }).pipe(
  Args.withDefault("json")
)

Options

Named flags and parameters.

Boolean Flag

const verbose = Options.boolean("verbose").pipe(
  Options.withAlias("v"),
  Options.withDefault(false)
)
// Usage: --verbose or -v

Text Option

const output = Options.text("output").pipe(
  Options.withAlias("o"),
  Options.optional
)
// Usage: --output file.txt or -o file.txt
// Returns: Option<string>

Integer Option

const limit = Options.integer("limit").pipe(
  Options.withAlias("n"),
  Options.withDefault(10)
)
// Usage: --limit 20 or -n 20

Choice Option (Enum)

const FORMATS = ["json", "yaml", "toml"] as const

const format = Options.choice("format", FORMATS).pipe(
  Options.withAlias("f"),
  Options.withDefault("json" as const)
)
// Usage: --format yaml or -f yaml

Optional vs Required

// Required (error if missing)
const required = Options.text("api-key")

// Optional (returns Option<string>)
const optional = Options.text("api-key").pipe(Options.optional)

// Optional with default (returns string)
const withDefault = Options.text("api-key").pipe(
  Options.withDefault("default-key")
)

Subcommands

Compose commands into hierarchies.

Basic Subcommands

const add = Command.make("add", { file: Args.text({ name: "file" }) },
  ({ file }) => Console.log(`Adding ${file}`)
)

const remove = Command.make("remove", { file: Args.text({ name: "file" }) },
  ({ file }) => Console.log(`Removing ${file}`)
)

const main = Command.make("git", {}, () =>
  Console.log("Usage: git <add|remove> <file>")
).pipe(
  Command.withSubcommands([add, remove])
)

// Usage: git add file.txt
// Usage: git remove file.txt

Nested Subcommands

const dbMigrate = Command.make("migrate", {}, () => Console.log("Migrating..."))
const dbSeed = Command.make("seed", {}, () => Console.log("Seeding..."))

const db = Command.make("db", {}, () => Console.log("Usage: app db <migrate|seed>"))
  .pipe(Command.withSubcommands([dbMigrate, dbSeed]))

const main = Command.make("app", {}, () => Console.log("Usage: app <db>"))
  .pipe(Command.withSubcommands([db]))

// Usage: app db migrate

Program Structure

Standard CLI Template

#!/usr/bin/env bun
import { Args, Command, Options } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Console, Effect, Layer, pipe } from "effect"

// =============================================================================
// OPTIONS & ARGS (define reusable pieces)
// =============================================================================

const verboseOption = Options.boolean("verbose").pipe(
  Options.withAlias("v"),
  Options.withDefault(false)
)

const formatOption = Options.choice("format", ["json", "text"] as const).pipe(
  Options.withAlias("f"),
  Options.withDefault("text" as const)
)

// =============================================================================
// COMMANDS
// =============================================================================

const listCommand = Command.make(
  "list",
  { verbose: verboseOption, format: formatOption },
  ({ verbose, format }) =>
    Effect.gen(function* () {
      yield* Console.log(`Listing (verbose=${verbose}, format=${format})`)
    })
)

const addCommand = Command.make(
  "add",
  { name: Args.text({ name: "name" }) },
  ({ name }) =>
    Effect.gen(function* () {
      yield* Console.log(`Adding: ${name}`)
    })
)

// =============================================================================
// MAIN COMMAND
// =============================================================================

const mainCommand = Command.make("mycli", {}, () =>
  Console.log(`
mycli - My CLI Tool

COMMANDS:
  list    List items
  add     Add an item

OPTIONS:
  --help, -h     Show help
  --version, -V  Show version
`)
).pipe(
  Command.withSubcommands([listCommand, addCommand])
)

// =============================================================================
// RUN
// =============================================================================

const cli = Command.run(mainCommand, {
  name: "mycli",
  version: "1.0.0",
})

pipe(
  Effect.sync(() => process.argv),
  Effect.flatMap(cli),
  Effect.provide(NodeContext.layer),
  NodeRuntime.runMain
)

With Custom Layers

// Define your service layers
const AppLayer = Layer.mergeAll(
  NodeContext.layer,
  DatabaseLayer,
  ConfigLayer
)

pipe(
  program,
  Effect.catchAll(handleError),
  Effect.provide(AppLayer),
  NodeRuntime.runMain
)

Handler Patterns

Effectful Handler

const myCommand = Command.make("cmd", { id: Args.text({ name: "id" }) },
  ({ id }) =>
    Effect.gen(function* () {
      const service = yield* MyService
      const result = yield* service.findById(id)
      yield* Console.log(JSON.stringify(result, null, 2))
    })
)

With Error Handling

const myCommand = Command.make("cmd", { id: Args.text({ name: "id" }) },
  ({ id }) =>
    Effect.gen(function* () {
      const result = yield* findById(id)
      yield* Console.log(result)
    }).pipe(
      Effect.catchTag("NotFoundError", (e) =>
        Console.error(`Not found: ${e.id}`)
      )
    )
)

Returning Exit Code

const myCommand = Command.make("cmd", {},
  () =>
    Effect.gen(function* () {
      const success = yield* doSomething()
      if (!success) {
        yield* Effect.fail(new Error("Operation failed"))
      }
    })
)

Help Text

Automatic Help

@effect/cli
generates help automatically from:

  • Command names
  • Arg/Option names
  • Descriptions via
    .withDescription()
const cmd = Command.make("greet",
  {
    name: Args.text({ name: "name" }).pipe(
      Args.withDescription("Name of person to greet")
    ),
    loud: Options.boolean("loud").pipe(
      Options.withAlias("l"),
      Options.withDescription("Greet loudly with exclamation")
    ),
  },
  handler
)

Custom Main Help

const main = Command.make("mycli", {}, () =>
  Console.log(`
mycli v1.0.0 - Description here

USAGE:
  mycli <command> [options]

COMMANDS:
  list      List all items
  add       Add new item
  remove    Remove item

GLOBAL OPTIONS:
  --help, -h      Show help
  --version, -V   Show version

EXAMPLES:
  mycli list --format json
  mycli add "new item"
`)
)

Anti-Patterns

DON'T: Sync handlers for async work

// WRONG
Command.make("cmd", {}, () => {
  const result = fetchSync() // Blocking!
  return Console.log(result)
})

// CORRECT
Command.make("cmd", {}, () =>
  Effect.gen(function* () {
    const result = yield* fetchEffect()
    yield* Console.log(result)
  })
)

DON'T: Forget to provide layers

// WRONG - Will fail with missing service
pipe(program, NodeRuntime.runMain)

// CORRECT
pipe(program, Effect.provide(AppLayer), NodeRuntime.runMain)

DON'T: Use console.log directly

// WRONG
Command.make("cmd", {}, () => {
  console.log("Hello") // Not Effect-native
  return Effect.void
})

// CORRECT
Command.make("cmd", {}, () => Console.log("Hello"))

Related Skills

SkillPurpose
cli/persistence
SQLite storage patterns
cli/messaging
Agent-guiding output
cli/services
Effect.Service for CLI
cli/config
Configuration patterns

Quick Reference

PatternImportExample
Command
@effect/cli
Command.make("name", {}, handler)
Text arg
@effect/cli
Args.text({ name: "x" })
Bool option
@effect/cli
Options.boolean("x")
Choice option
@effect/cli
Options.choice("x", [...])
Subcommands
@effect/cli
Command.withSubcommands([...])
Run
@effect/cli
Command.run(cmd, { name, version })
Runtime
@effect/platform-node
NodeRuntime.runMain