Back to skills
SkillHub ClubDesign ProductFull StackDesigner

cli-design

CLI design patterns using Typer. Covers command structure, arguments, options, configuration, and output formatting. Use this when building command-line tools.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C2.5
Composite score
2.5
Best-practice grade
B84.0

Install command

npx @skill-hub/cli install alicoding-nextura-cli-design

Repository

alicoding/nextura

Skill path: .claude/skills/cli-design

CLI design patterns using Typer. Covers command structure, arguments, options, configuration, and output formatting. Use this when building command-line tools.

Open repository

Best for

Primary workflow: Design Product.

Technical facets: Full Stack, Designer.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: alicoding.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install cli-design into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/alicoding/nextura before adding cli-design to shared team environments
  • Use cli-design for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: cli-design
scope: generic
description: >
  CLI design patterns using Typer. Covers command structure, arguments,
  options, configuration, and output formatting. Use this when building
  command-line tools.
version: 2.0.0
triggers:
  - cli design
  - command line
  - typer
  - cli tool
  - terminal interface
---

# CLI Design

You are building command-line tools that developers will use daily. A good CLI
is intuitive, well-documented, and follows conventions users expect.

**Use Typer for Python CLIs.**

---

## Command Structure

```
app [GLOBAL OPTIONS] COMMAND [COMMAND OPTIONS] [ARGS]

# Examples
myapp --verbose users list --format json
myapp config set key value
myapp deploy --env production
```

### Naming Conventions

```
# Commands: verb or noun (depends on context)
myapp list      # list resources
myapp users     # manage users (sub-app)

# Sub-commands: verb
myapp users list
myapp users create
myapp users delete

# Consistent patterns
myapp <resource> list
myapp <resource> get <id>
myapp <resource> create
myapp <resource> update <id>
myapp <resource> delete <id>
```

**For detailed Typer patterns:** See [reference/typer-patterns.md](reference/typer-patterns.md)

---

## Arguments vs Options

| Type | When | Example |
|------|------|---------|
| Arguments | Required, positional | `myapp process input.txt` |
| Options | Named, usually optional | `myapp list --format json` |

### Short vs Long Options

```
# Common flags get short options
-v, --verbose
-h, --help
-q, --quiet
-f, --format
-o, --output

# Less common = long only
--dry-run
--include-hidden
--max-retries
```

---

## Configuration Priority

```
1. Command-line arguments (highest)
2. Environment variables
3. Config file (local)
4. Config file (global)
5. Defaults in code (lowest)
```

---

## Output and Exit Codes

### Output Formats

```python
@app.command()
def list_items(
    format: str = typer.Option("table", "--format", "-f"),
    quiet: bool = typer.Option(False, "--quiet", "-q"),
):
    if quiet:
        # Machine-readable: exit code only
        raise typer.Exit(0 if success else 1)
    # Human-readable with colors
    ...
```

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments |
| 130 | Interrupted (Ctrl+C) |

---

## STOP GATES

### STOP GATE 1: Help Text Complete

**Check:** Does every command have help text?
**Pass:** All commands and options documented
**Fail:** STOP. Add help text.

### STOP GATE 2: Config Externalized

**Check:** Are all configurable values externalized?
**Pass:** No hardcoded URLs, keys, paths
**Fail:** STOP. Move to config/env.

### STOP GATE 3: Exit Codes Correct

**Check:** Do commands return appropriate exit codes?
**Pass:** 0 for success, non-zero for errors
**Fail:** STOP. Fix exit codes.

### STOP GATE 4: Output Formats

**Check:** Is output machine-parseable when needed?
**Pass:** JSON/quiet modes available
**Fail:** STOP. Add machine-readable output.

### STOP GATE 5: Tested

**Check:** Are commands tested with CliRunner?
**Pass:** Test coverage for all commands
**Fail:** STOP. Add CLI tests.

---

## Quick Reference Checklist

```
[ ] All commands have --help
[ ] Consistent naming (verb-noun)
[ ] Short options for common flags (-v, -q, -f)
[ ] Config from env vars supported
[ ] JSON output available
[ ] Proper exit codes
[ ] Progress for long operations
[ ] Colors for human output
[ ] Error messages to stderr
```

---

## Anti-Patterns

| If you're doing... | STOP. Do this instead... |
|--------------------|--------------------------|
| Hardcoded config values | Use env vars / config file |
| No help text | Add docstrings and help= |
| Print to stdout for errors | Use stderr (err=True) |
| Exit code 0 on error | Return non-zero |
| No quiet mode | Add --quiet for scripts |
| No JSON output | Add --format json |
| No tests | Use CliRunner tests |

---

## Reference Files

- [reference/typer-patterns.md](reference/typer-patterns.md) - Typer code patterns


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### reference/typer-patterns.md

```markdown
# Typer Patterns

Detailed patterns for building CLIs with Typer.

---

## App Structure

```python
import typer

app = typer.Typer(help="My CLI application")

# Sub-command groups
users_app = typer.Typer(help="Manage users")
config_app = typer.Typer(help="Manage configuration")

app.add_typer(users_app, name="users")
app.add_typer(config_app, name="config")

@users_app.command("list")
def list_users():
    """List all users."""
    ...

@users_app.command("create")
def create_user(name: str):
    """Create a new user."""
    ...

if __name__ == "__main__":
    app()
```

---

## Arguments vs Options

```python
# ARGUMENTS: Required, positional
@app.command()
def process(
    file: Path,                    # Required argument
    output: Path = Path("out"),    # Optional argument with default
):
    """Process FILE and write to OUTPUT."""
    ...

# Usage: myapp process input.txt output.txt

# OPTIONS: Named, usually optional
@app.command()
def process(
    file: Path,
    verbose: bool = typer.Option(False, "--verbose", "-v"),
    format: str = typer.Option("json", "--format", "-f"),
):
    """Process FILE with options."""
    ...

# Usage: myapp process input.txt --verbose --format yaml
```

---

## Option Patterns

```python
# Boolean flags
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output")
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress output")

# Value options
format: str = typer.Option("json", "--format", "-f", help="Output format")
count: int = typer.Option(10, "--count", "-n", help="Number of items")

# Required options (unusual but valid)
api_key: str = typer.Option(..., "--api-key", envvar="API_KEY", help="API key")

# Multiple values
tags: list[str] = typer.Option([], "--tag", "-t", help="Tags (can repeat)")

# Choices
level: str = typer.Option("info", "--level", help="Log level",
    click_type=click.Choice(["debug", "info", "warn", "error"]))
```

---

## Configuration Pattern

```python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    api_url: str = "https://api.example.com"
    api_key: str | None = None
    verbose: bool = False

    class Config:
        env_prefix = "MYAPP_"
        env_file = ".env"

settings = Settings()

@app.command()
def call_api(
    verbose: bool = typer.Option(settings.verbose, "--verbose", "-v"),
):
    # CLI option overrides config
    ...
```

---

## Config Commands

```python
config_app = typer.Typer(help="Manage configuration")

@config_app.command("show")
def config_show():
    """Show current configuration."""
    config = load_config()
    typer.echo(yaml.dump(config))

@config_app.command("set")
def config_set(key: str, value: str):
    """Set a configuration value."""
    config = load_config()
    config[key] = value
    save_config(config)
    typer.echo(f"Set {key}={value}")

@config_app.command("get")
def config_get(key: str):
    """Get a configuration value."""
    config = load_config()
    if key in config:
        typer.echo(config[key])
    else:
        raise typer.Exit(1)
```

---

## Output Formats

```python
from enum import Enum

class OutputFormat(str, Enum):
    json = "json"
    yaml = "yaml"
    table = "table"
    text = "text"

@app.command()
def list_items(
    format: OutputFormat = typer.Option(OutputFormat.table, "--format", "-f"),
):
    items = get_items()

    match format:
        case OutputFormat.json:
            typer.echo(json.dumps([i.dict() for i in items], indent=2))
        case OutputFormat.yaml:
            typer.echo(yaml.dump([i.dict() for i in items]))
        case OutputFormat.table:
            print_table(items)
        case OutputFormat.text:
            for item in items:
                typer.echo(f"{item.id}: {item.name}")
```

---

## Progress and Colors

```python
from rich.progress import Progress, SpinnerColumn, TextColumn

# For determinate progress
with typer.progressbar(items, label="Processing") as progress:
    for item in progress:
        process(item)

# For indeterminate progress (using rich)
with Progress(
    SpinnerColumn(),
    TextColumn("[progress.description]{task.description}"),
) as progress:
    task = progress.add_task("Loading...", total=None)
    result = long_running_operation()
    progress.update(task, completed=True)

# Color output
typer.echo("Normal message")
typer.secho("Success!", fg=typer.colors.GREEN)
typer.secho("Warning!", fg=typer.colors.YELLOW)
typer.secho("Error!", fg=typer.colors.RED, err=True)
```

---

## Testing with CliRunner

```python
from typer.testing import CliRunner

runner = CliRunner()

def test_list_users():
    result = runner.invoke(app, ["users", "list"])
    assert result.exit_code == 0
    assert "alice" in result.stdout

def test_create_user():
    result = runner.invoke(app, ["users", "create", "bob"])
    assert result.exit_code == 0
    assert "Created user: bob" in result.stdout

def test_confirm_delete():
    result = runner.invoke(app, ["delete", "item-1"], input="y\n")
    assert result.exit_code == 0
    assert "Deleted" in result.stdout
```

```

cli-design | SkillHub