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.
Install command
npx @skill-hub/cli install alicoding-nextura-cli-design
Repository
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 repositoryBest 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
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
```
```