Skip to main content

cli-design

Unix-composable CLI design patterns. Use when building CLI tools, designing command trees, implementing output layers, or testing CLI behavior. Covers stream separation (stdout/stderr), format flags (--json/--plain), exit codes, TTY detection, composability, and error design. Language-agnostic principles; TypeScript implementation patterns in resources/. For API design (REST, HTTP), see api-design. Use when this capability is needed.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
tomevault-io/tomes
آخر نشاط في المصدر
٢٣ يوليو ٢٠٢٦ في ٢١:٤٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
cli-design
description
Unix-composable CLI design patterns. Use when building CLI tools, designing command trees, implementing output layers, or testing CLI behavior. Covers stream separation (stdout/stderr), format flags (--json/--plain), exit codes, TTY detection, composability, and error design. Language-agnostic principles; TypeScript implementation patterns in resources/. For API design (REST, HTTP), see api-design. Use when this capability is needed.
metadata
{"author":"citypaul"}
# CLI Design: Unix-Composable Command-Line Interfaces This skill covers **language-agnostic** CLI design principles. The rules about stream separation, exit codes, format flags, and composability apply regardless of implementation language. For API contract stability and Hyrum's Law, see the `api-design` skill. For config, env vars, and graceful shutdown, see the `twelve-factor` skill. **TypeScript implementation patterns** are in the `resources/` directory. Load them on demand when building a CLI in TypeScript: | Resource | Load when... | |----------|-------------| | `output-architecture.md` | Implementing Result types, entry point wiring, formatters, logger, JSON envelope schemas | | `testing-cli.md` | Writing Vitest tests for CLI behavior (streams, exit codes, pipes, contract tests) | | `stream-contracts.md` | Understanding Node.js buffering, NDJSON, signal handling, crash-only design | --- ## When to Use - Building any command-line tool (any language) - Designing command tree, flags, and I/O contracts - Implementing the output layer (format detection, stream routing) - Testing CLI behavior (stdout/stderr separation, exit codes) - Reviewing a CLI for Unix composability --- ## Core Principle **stdout is for DATA only — the product the user asked for.** **stderr is for EVERYTHING ELSE — diagnostics, progress, spinners, warnings, errors.** This separation is what makes `mycli --json | jq ...` work. One spinner character on stdout breaks every downstream pipe. > "Whatever software you're building, you can be absolutely certain that people will use it in ways you didn't anticipate. Your software will become a part in a larger system — your only choice is over whether it will be a well-behaved part." — clig.dev --- ## The Unix Stream Contract | Content | Stream | Why | |---------|--------|-----| | Primary output (data, results, JSON) | stdout | Pipeable, buffered for throughput | | Progress bars, spinners, status | stderr | Not data — must not corrupt pipes | | Warnings, errors, diagnostics | stderr | Visible to user even when stdout is piped | | Debug/verbose output | stderr | Diagnostic, never data | **Buffering behavior:** - **stdout**: line-buffered when connected to a TTY, block-buffered when piped (~2x faster than stderr) - **stderr**: unbuffered — every write is a syscall (immediate but expensive) - **Check each stream independently** — stdout being piped does not mean stderr is piped When stdout is piped, the user doesn't want your status messages in their data. All non-data output must go to stderr. For a deep dive on buffering behavior and performance implications, see `resources/stream-contracts.md`. --- ## Keep Handlers Pure The practical rule: **functions that do the work should return data, not write to stdout.** The CLI entry point handles all I/O. ``` Entry point (CLI main) Your logic (handlers) ───────────────────── ───────────────────── parse args (input) → structured result detect format (json/plain/human) no printing to stdout call handler no writing to stderr format the result no calling exit write to correct stream just returns data set exit code ``` This isn't an architecture mandate — it's just clean function design. The benefits are concrete: - **Testable without subprocess spawning** — call the handler, assert on the returned value - **Format flexibility for free** — same data renders as JSON, plain text, or coloured tables by swapping one function - **Reusable** — the same handler works from a CLI, MCP server, HTTP API, or programmatic import For simple CLIs where the "handler" is just calling a library, this separation already exists naturally — your library returns data, your CLI formats it. No extra layers needed. If your project uses hexagonal architecture, the mapping is direct: the CLI entry point is a **driving adapter**, and the handler is a **use case** that returns a result through a port. See the `hexagonal-architecture` skill — the patterns reinforce each other, but hex arch is not required to benefit from keeping handlers pure. For TypeScript implementation patterns (Result types, entry point wiring, formatters, logger interfaces), see `resources/output-architecture.md`. --- ## Format Flag Contract Three-tier output hierarchy: ### Default: Human-Readable - Colors, tables, formatted text - Progress bars and spinners on **stderr** - Output tailored for terminal width - May change between versions — this is **not** a contract ### `--plain`: Grep/Awk-Friendly - One record per line, no formatting, no colors - Stable between minor versions — this **is** a contract - Flat table rows, no borders, no grouped sections - Enables: `mycli list --plain | grep error | wc -l` > "Encourage your users to use `--plain` or `--json` in scripts to keep output stable." — clig.dev ### `--json`: Structured Data - stdout contains **ONLY** valid JSON — no spinners, no color, no progress - stderr continues normally — human diagnostics still visible - Errors are structured JSON too — not just success responses - Schema is versioned — breaking changes to JSON output are breaking changes to the CLI - `--json` implies non-interactive regardless of TTY **Consistent envelope:** ```json { "ok": true, "data": { ... } } { "ok": false, "error": { "code": "CONFIG_MISSING", "message": "...", "fix": "..." } } ``` ### NDJSON for Streaming For large datasets, use NDJSON (one JSON object per `\n`): - Each line is independently parseable - Include a `type` field per record for multiplexing events - Final line can be a summary record - Enables: `mycli run --format ndjson | while read -r line; do ...; done` For NDJSON specification details, see `resources/stream-contracts.md`. --- ## Exit Codes | Code | Meaning | When | |------|---------|------| | 0 | Success | Operation completed as expected | | 1 | Domain failure | Tool-specific failure (e.g. quality threshold not met) | | 2 | Invalid usage | Bad flags, missing required args, validation error | | 78 | Configuration error | Invalid config file, missing required config | | 75 | Temporary failure | Network timeout, service unavailable — retry may help | | 130 | SIGINT | User pressed Ctrl-C (128 + 2) | | 143 | SIGTERM | Process terminated (128 + 15) | **Rules:** - Non-zero exit code **MUST** have a stderr explanation - Document exit codes in `--help` - Never use codes above 125 for application errors (reserved for signals: 128 + signal number) - Exit code 75 (transient) is critical — it tells retry logic the failure may be temporary - Map non-zero codes to the most important failure modes for your tool --- ## TTY Detection Check priority order (first match wins): | Priority | Condition | Effect | |----------|-----------|--------| | 1 | `--format json` or `--json` flag | Non-interactive, no color, no animation | | 2 | `--no-color` flag | Disable color (output may still be interactive) | | 3 | `NO_COLOR` env (non-empty) | Disable color | | 4 | `FORCE_COLOR` env | Enable color regardless | | 5 | `TERM=dumb` | Disable color and animations | | 6 | `CI=true` | No interactive prompts | | 7 | stdout is not a TTY (`!isatty(stdout)`) | Plain output, no animations on stdout | | 8 | Default | Full interactive with colors | **Check stdout and stderr independently.** When stdout is piped but stderr is a TTY, you can still show spinners on stderr while keeping stdout clean for the pipe consumer. Optionally support `MYCLI_NO_COLOR` for app-specific color override. --- ## Input Design ### Flags Over Arguments - **1 positional arg**: acceptable (the "main thing") - **2 positional args**: suspicious — consider flags instead - **3+ positional args**: never acceptable Flags are self-documenting, order-independent, and future-proof. ```bash # Bad — which is source, which is destination? mycli copy myapp backup # Good — explicit mycli copy --from myapp --to backup ``` ### Standard Flags Always provide long forms. Short flags only for the most common operations. | Flag | Meaning | |------|---------| | `-h`, `--help` | Show help (this should only mean help) | | `--version` | Print version to stdout | | `-q`, `--quiet` | Suppress non-essential output | | `-v`, `--verbose` | More detail in human output | | `-d`, `--debug` | Diagnostic output to stderr | | `-f`, `--force` | Skip confirmation prompts | | `-n`, `--dry-run` | Show what would happen without doing it | | `--json` | Structured JSON output | | `--plain` | Stable, grep-friendly plain text | | `--no-color` | Disable color output | | `--no-input` | Disable all prompts/interactivity | | `-o`, `--output` | Output file | ### Prompts and Interactivity - **All prompts MUST be bypassable** via flags for scriptability - Confirmation → `--yes` or `--force` - Selection → `--type=value` - Text input → `--name=value` - Passwords → `--password-file=path` or stdin pipe - If stdin is not a TTY, never prompt — fail with a clear error or use defaults - **Secrets via files/stdin/env only** — never via flag values (they leak to `ps` output and shell history) ### Conventions - Support `--` to stop flag parsing: `mycli run -- --flag-for-child-process` - Support `-` for stdin/stdout file arguments: `curl ... | mycli process -` - Accept both `--flag=value` and `--flag value` - If stdin is expected but is an interactive terminal, display help immediately (don't hang like `cat`) --- ## Config Precedence Highest to lowest priority: 1. **Flags** — per-invocation overrides 2. **Environment variables** — `MYCLI_*` prefix, per-session 3. **Project config** — `.myclirc`, `mycli.config.ts`, or in `package.json` 4. **User config** — `~/.config/mycli/` (follow XDG spec) 5. **Defaults** — sensible built-in values **Rules:** - Follow the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) for config file locations - Env var naming: `MYCLI_*` prefix, uppercase letters + digits + underscores - Never accept secrets via flags — use env vars, files, or stdin - Read `.env` where appropriate, but don't use it as a substitute for proper config - If you modify configuration that belongs to another program, ask consent first --- ## Error Design Every error needs: 1. **Machine-readable code** — `UPPER_SNAKE_CASE` (e.g. `CONFIG_MISSING`, `AUTH_EXPIRED`) 2. **What went wrong** — context: which resource, operation, input 3. **How to fix it** — exact command or action the user should take 4. **Reference** — docs URL or `mycli help <topic>` (optional) ### Human Mode ``` Error: CONFIG_MISSING — Configuration file not found No configuration file found at ./mycli.config.ts or ~/.config/mycli/config.ts Fix: Run `mycli init` to create a default configuration file Docs: https://mycli.dev/docs/configuration ``` - Put the most important information **last** (the eye is drawn to the end) - Use red sparingly and intentionally - Suggest corrections for typos ("Did you mean 'deploy'?") - Group similar errors under one header — don't repeat 50 similar-looking lines - Write debug logs to a file, not the terminal (unless `--debug`) ### JSON Mode Errors are structured too — not just success responses: ```json { "ok": false, "error": { "code": "CONFIG_MISSING", "message": "No configuration file found at ./mycli.config.ts", "fix": "Run `mycli init` to create a default configuration file", "transient": false } } ``` The `transient` boolean tells retry logic whether the failure may be temporary. --- ## Composability Patterns Design for real-world pipes: ```bash # Filter structured output mycli list --json | jq '.data[] | select(.status == "failed")' # Stream results for large datasets mycli run --format ndjson | while read -r line; do echo "$line" | jq '.file'; done # Feed stdin cat previous-results.json | mycli report --format markdown # Combine with other tools mycli run --json | mycli diff --baseline previous.json # Silent mode for CI — only exit code matters mycli check --quiet || echo "Check failed!" # Chain: create outputs an identifier, next command uses it mycli create --json | jq -r '.data.id' | xargs mycli deploy --id # Column selection for efficiency mycli list --json --fields name,status,id | jq '.data[]' # Parallel processing mycli list --json --fields id | jq -r '.data[].id' | xargs -P4 mycli process --id ``` **Key patterns:** - Create commands output identifiers so subsequent commands can chain - List commands support `--fields` for column selection (reduces output size, critical for agent efficiency) - `--quiet` for CI scripts that only care about the exit code - NDJSON for streaming large datasets without buffering everything in memory - `--dry-run` with `--json` outputs planned changes as structured data --- ## Subcommand Design - **noun verb** pattern is most common: `mycli config set`, `mycli report generate` - Be consistent across all subcommands — same flag names for same things - No ambiguous pairs (`update` vs `upgrade` is confusing) - No catch-all subcommands (you can never add subcommands with conflicting names) - No arbitrary abbreviations — aliases must be explicit and stable - With no args: list subcommands (multi-command CLI) or show help (single-command CLI) ### Help - `mycli --help` — top-level help - `mycli help <subcommand>` — subcommand help - `mycli <subcommand> --help` — same as above - If run with missing required args, show concise help + 1-2 examples + "use --help for more" - **Examples are the most-read section** — lead with them - Include flag types, defaults, and allowed values for finite sets --- ## Output Stability Contract **Stdout is a public API.** Breaking changes to stdout format are breaking changes to the CLI. | Change | Impact | |--------|--------| | Adding new optional JSON fields | Safe (additive) | | Adding new subcommands | Safe | | Adding new flags with preserving defaults | Safe | | Removing or renaming flags | **Breaking** | | Removing or renaming JSON fields | **Breaking** | | Changing exit codes | **Breaking** | | Changing default behavior | **Breaking** |
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub