📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,55 @@
# CLI Development Guidelines
Design and review command-line interfaces with human-first UX and UNIX composability.
## What's included
- **SKILL.md** — Core methodology for CLI design
- **references/** — Deep-dive reference material and checklists
- **templates/** — JSON spec template, help text skeleton, error message patterns
- **scripts/cli_audit.py** — Automated CLI citizenship checker
## Attribution
This skill is adapted primarily from [Command Line Interface Guidelines](https://clig.dev/) (CC BY-SA 4.0):
- Authors: Aanand Prasad, Ben Firshman, Carl Tashian, Eva Parish
- Design: Mark Hurrell
- Repository: <https://github.com/cli-guidelines/cli-guidelines>
Additional sources:
- [POSIX Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)
- [GNU Coding Standards](https://www.gnu.org/prep/standards/)
- [Heroku CLI Style Guide](https://devcenter.heroku.com/articles/cli-style-guide)
- [12 Factor CLI Apps](https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46)
- [NO_COLOR convention](https://no-color.org/)
- [XDG Base Directory Spec](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
## License
**Documentation** (SKILL.md, references/, templates/*.md): [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
**Scripts** (scripts/): MIT License
```
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
@@ -0,0 +1,95 @@
---
name: cli-development-guidelines
description: This skill should be used when designing, implementing, or reviewing CLI tools, or when flags, subcommands, help text, exit codes, or `--cli-dev` are mentioned.
license: CC-BY-SA-4.0 (docs, adapted from clig.dev); MIT (scripts)
compatibility: Scripts use Python 3.10+ (scripts/cli_audit.py).
metadata:
version: "0.1.0"
upstream: "clig.dev + POSIX/GNU/Heroku/12-factor + Agent Skills spec"
---
# CLI Development Guidelines
## When to activate this skill
- You are *designing*, *implementing*, or *reviewing* a command-line tool.
- The user mentions (explicitly or implicitly): `--help`, flags, subcommands, exit codes, stdout/stderr, piping, JSON output, color, prompts, config files, env vars, “works in CI”, install/uninstall, telemetry.
## What this skill produces
- A *CLI contract* (what users can rely on): commands, flags, IO behavior, exit codes, config/env, examples, and safety behavior.
- Draft *help output* and docs structure (example-first).
- A *compliance audit* (when runnable) using `scripts/cli_audit.py`.
## Non-negotiable CLI citizenship
- Exit codes:
- `0` on success.
- Non-zero on failure (and ideally meaningful, documented codes).
- Streams:
- `stdout` is for primary output and machine-readable output.
- `stderr` is for errors, warnings, progress, and “what Im doing” messaging.
- Discoverability:
- `--help` (and usually `-h`) shows help and exits.
- `--version` prints version and exits.
- Interactivity:
- Prompts only when `stdin` is a TTY.
- Provide `--no-input` to force non-interactive behavior.
- Scripting friendliness:
- No ANSI color / spinners when output isnt a TTY.
- Support `NO_COLOR` and `--no-color`.
- Consider `--json` and `--plain` for stable output.
## Workflow
### Sketch the CLI contract first
- Start from the users jobs-to-be-done (what theyre trying to accomplish).
- Decide:
- Command shape: single command vs subcommands (`noun verb` is common).
- Inputs: args vs flags vs stdin vs prompts vs config/env.
- Outputs: human default, plus machine modes (`--json`, `--plain`, `--quiet`).
- Safety: confirmations, `--dry-run`, `--force`, secret handling.
Use:
- [CLI reference](references/REFERENCE.md)
- [CLI spec template](templates/cli-command-spec-template.json)
### Implement with safe defaults
- Use a CLI parsing library (dont hand-roll).
- Make “boundary-crossing” actions explicit:
- Network calls
- Writing files not explicitly provided
- Mutating remote state
- Avoid footguns:
- Dont accept secrets via flags or environment variables.
- Dont print stack traces by default.
- Dont assume TTY (detect it).
### Validate and iterate
- Run an automated sanity check (when possible):
- `python scripts/cli_audit.py -- <your-cli> [subcommand]`
- Fix in this order:
- Broken stdout/stderr separation
- Incorrect exit codes
- Help thats missing or undiscoverable
- Unsafe defaults (destructive ops, secrets, hidden network writes)
- Unscriptable output (no stable modes)
Use:
- [Checklist](references/CHECKLIST.md)
- `scripts/cli_audit.py`
## Reference library
- Core reference: [references/REFERENCE.md](references/REFERENCE.md)
- Quick audit checklist: [references/CHECKLIST.md](references/CHECKLIST.md)
- Evaluation prompts: [references/EVAL_PROMPTS.md](references/EVAL_PROMPTS.md)
## Templates and scripts
- CLI spec template: `templates/cli-command-spec-template.json`
- Help text template: `templates/help-text-template.md`
- Error message template: `templates/error-message-template.md`
- Audit a CLI: `scripts/cli_audit.py`
@@ -0,0 +1,103 @@
# CLI Development Checklist
Use this to *review a CLI design* or *gate a release*.
## Interface contract
- The CLI's interface is documented (commands, flags, exit codes, output modes).
- There is a stable scripting mode (`--json` and/or `--plain`) for anything that users might automate.
- Backwards-compatibility is treated as a release constraint.
## Basics
- Exit codes:
- `0` on success
- Non-zero on failure
- Usage/argument errors are consistently non-zero (often `2`)
- Streams:
- Primary output goes to `stdout`
- Errors, warnings, progress, and status messaging go to `stderr`
- `--help` prints help and exits successfully
- `--version` prints version and exits successfully
## Help and docs
- If invoked incorrectly (missing required args), prints a *concise* help block.
- Full help is scan-friendly:
- USAGE
- DESCRIPTION
- COMMANDS (if any)
- OPTIONS
- EXAMPLES (near the top)
- Includes:
- Web docs link
- Support/issue path
- Doesn't emit ANSI escape sequences when help is piped/captured.
## Output behavior
- Human-friendly defaults when writing to a TTY.
- Machine-friendly modes exist and are documented:
- `--json` (structured)
- `--plain` (one record per line / simple tabular)
- Color:
- Disabled when stream isn't a TTY
- Disabled when `NO_COLOR` is set
- Disabled when `TERM=dumb`
- Disabled with `--no-color`
- No animations/spinners/progress bars when output isn't a TTY.
- Large output uses a pager only when appropriate and respects `PAGER`.
## Arguments, flags, and subcommands
- Flags are preferred over positional args unless the command is truly "classic" (`cp src dst`).
- All flags have long forms.
- One-letter flags are reserved for truly common actions.
- Uses conventional flag names where applicable (`--json`, `--dry-run`, `--force`, etc.).
- Subcommands are unambiguous and avoid near-synonyms.
- Order dependence is avoided when feasible.
## Interactivity and safety
- Prompts only when `stdin` is a TTY.
- `--no-input` disables prompts.
- Dangerous operations:
- Support `--dry-run` (when helpful)
- Confirm interactively
- Support `--force`/`--confirm=...` for non-interactive usage
- Boundary-crossing actions (network, implicit file writes) are explicit and/or clearly documented.
## Configuration
- Precedence is clear and consistent:
- Flags > env > project config > user config > system config
- Uses XDG base dirs for user config/cache/data when relevant.
- `.env` is only used for simple context knobs and is not used for secrets.
## Secrets
- Secrets are not accepted via flags or environment variables.
- Secrets are accepted via:
- stdin (`--token-stdin`, `--password-stdin`)
- file (`--token-file`)
- OS keychain / secret manager (when appropriate)
## Robustness
- Validates user input early and clearly.
- Gives quick feedback (<~100ms) before long operations.
- Network operations have timeouts.
- Operations are idempotent or recoverable when possible.
- Ctrl-C exits quickly and predictably; long cleanup can be interrupted.
## Release and distribution
- Installation is clear and reversible.
- Uninstall instructions exist.
- Changelog notes behavior changes and deprecations.
- Deprecations warn before removal; replacements are documented.
## Quick automation
- Run: `python scripts/cli_audit.py -- <your-cli> [subcommand]`
- Treat FAILs as blockers; treat WARNs as "fix soon."
@@ -0,0 +1,75 @@
# Evaluation Prompts for This Skill
Use these to verify an agent is applying CLI best practices (not just "making something that runs").
## Scoring rubric
- *Pass*:
- Produces a clear CLI contract (commands, flags, IO, exit codes, examples).
- Explicitly addresses stdout/stderr, exit codes, help behavior, and interactivity.
- Includes stable scripting output modes (`--json`/`--plain`) when relevant.
- Avoids secret leaks (no secrets via flags/env).
- *Strong pass*:
- Uses the checklist and/or the audit script.
- Highlights trade-offs and backwards-compatibility risks.
- Provides ready-to-ship help output and error message patterns.
## Prompt: design a new CLI
- Task:
- "Design a CLI called `logship` that tails logs from multiple sources (local files and HTTP endpoints), filters by regex, and outputs either human-friendly colored logs or machine-readable JSON."
- Must include:
- Subcommands or flags decision (and rationale)
- `stdout` vs `stderr` behavior
- `--json` output definition (shape)
- Color behavior (`NO_COLOR`, `--no-color`, TTY detection)
- Timeouts for HTTP, progress/status messages
- Example-first help outline
- Exit codes
## Prompt: review a flawed CLI help output
- Task:
- "Here's the current `--help` output for `acmectl`. It's 200 lines of flags, no examples, and no description. Rewrite it to be discoverable."
- Must include:
- Concise default help vs full help structure
- Examples near the top
- Group common flags first
- Support path / docs link
## Prompt: fix stdout/stderr separation
- Task:
- "This command prints progress bars to stdout and the JSON result to stderr. Fix the output contract."
- Must include:
- Machine output on stdout
- Human/progress on stderr
- Behavior when piped/captured (no animations)
## Prompt: safe destructive action
- Task:
- "Add a `delete` command that can delete remote projects. Make it safe for humans but scriptable."
- Must include:
- Confirmation levels (moderate vs severe)
- `--dry-run`
- `--force` and/or `--confirm="exact-name"`
- `--no-input` behavior
## Prompt: secret handling
- Task:
- "Add auth to the CLI. It currently accepts `--token <secret>` and reads `MYAPP_TOKEN` env var. Fix the design."
- Must include:
- `--token-file` and/or `--token-stdin`
- Recommendation for OS keychain / secret manager
- Explain why flags/env are unsafe
## Prompt: run the audit script
- Task:
- "Run `scripts/cli_audit.py` against `./mycli` and address the FAIL/WARN items."
- Must include:
- Interpreting the audit output
- Fixing highest severity issues first
- Updating help text and/or flags accordingly
@@ -0,0 +1,439 @@
# CLI Development Guidelines Reference
## Scope and sources
This reference is a *condensed, operational* guide for building well-behaved CLI tools.
- Primary source (adapted heavily): *Command Line Interface Guidelines* (<https://clig.dev/>)
- License: CC BY-SA 4.0
- Authors: Aanand Prasad, Ben Firshman, Carl Tashian, Eva Parish
- Additional sources: POSIX utility conventions, GNU standards, Heroku CLI style guide, 12-factor CLI apps, XDG base directory spec, NO_COLOR convention.
## Table of contents
- [Design principles](#design-principles)
- [The basics: being a good CLI citizen](#the-basics-being-a-good-cli-citizen)
- [Help and documentation](#help-and-documentation)
- [Output, formatting, and modes](#output-formatting-and-modes)
- [Errors and diagnostics](#errors-and-diagnostics)
- [Arguments, flags, and subcommands](#arguments-flags-and-subcommands)
- [Interactivity and safety](#interactivity-and-safety)
- [Configuration and environment variables](#configuration-and-environment-variables)
- [Secrets and sensitive data](#secrets-and-sensitive-data)
- [Robustness: timeouts, retries, signals](#robustness-timeouts-retries-signals)
- [Future-proofing](#future-proofing)
- [Distribution and lifecycle](#distribution-and-lifecycle)
- [Analytics and telemetry](#analytics-and-telemetry)
- [Implementation notes](#implementation-notes)
- [Further reading](#further-reading)
## Design principles
### Human-first, but composable
- Optimize the default UX for humans:
- Clear, calm messages
- Example-first help
- Progress indicators for long operations
- Still be *composable* in UNIX pipelines:
- Clean `stdout` for data
- Meaningful exit codes
- No unexpected prompts in scripts
### Consistency is a power tool
- Prefer established CLI conventions when possible.
- Be consistent within your tool:
- Same option names mean the same thing everywhere.
- Output formats don't randomly change between subcommands.
### Say *just* enough
- Too little:
- Silent hangs
- No confirmation that anything happened
- Too much:
- Verbose debug spew in normal mode
- Walls of text hiding the one important line
### Discovery beats memorization
- `--help` should teach quickly.
- Suggest the "next command" in multi-step workflows.
### CLI as a conversation
- Users will iterate: run → error → fix → run.
- Respond like a helpful conversational partner:
- Point out what went wrong
- Suggest the simplest fix
- Make it easy to learn the correct syntax
## The basics: being a good CLI citizen
### Use a parsing library
- Don't hand-roll parsing, help formatting, or error rendering.
- A good parser will usually also give you:
- Help output
- Unknown-flag handling
- Sometimes: typo suggestions
### Streams: stdout vs stderr
- `stdout`
- The command's primary output
- Machine-readable output (piped into the next command)
- `stderr`
- Errors
- Warnings
- Progress / status messages
- Human "what's happening" narration
### Exit codes
- `0` means success.
- Non-zero means failure.
- Prefer a small set of stable, documented failure codes over "random integers."
- Consider reserving `2` for argument/usage errors.
- If you need a more granular taxonomy, consider the BSD `sysexits` family (e.g., EX_USAGE = 64), but be aware that many tools simply use `1`/`2` in practice.
## Help and documentation
### Required behaviors
- `--help` shows help and exits successfully.
- Ideally also support `-h` (and do not overload it with a different meaning).
- If your CLI has subcommands:
- `tool subcmd --help`
- `tool help subcmd` (optional but common in `git`-like tools)
### Concise help by default (when invocation is incomplete)
If the user runs a command with missing required args/flags, print a concise help block:
- What the tool does (one line)
- 12 common examples
- The most important flags (or a pointer to full help)
- "Run `--help` for full usage"
### Full help when asked
Full help should include:
- Usage line(s)
- Description
- Commands (if any)
- Options
- Examples (lead with examples; users will copy-paste them)
- Support path (issues / repo)
- Link to web docs (especially to a subcommand anchor if you have it)
### Formatting guidance
- Use scan-friendly formatting:
- Uppercased section headings
- Alignment for options
- Avoid ANSI escape sequences if help is piped (your output should not become "escape soup")
### If stdin is required but not provided
If your tool expects piped input and `stdin` is a TTY, don't hang.
- Print help or a clear message.
- Exit non-zero.
## Output, formatting, and modes
### Human-readable output is the default
A practical heuristic:
- If output is going to a TTY, it's probably a human.
- If output is being captured/piped, it's probably a program.
### Provide machine-readable output when it doesn't harm usability
Common patterns:
- `--json` outputs structured JSON (stable shape, versioned if needed).
- `--plain` outputs simple line/tabular output with one record per line.
- Encourage scripts to use `--json`/`--plain` rather than scraping the human UI.
### Keep success output brief, but not mysterious
- Printing nothing can feel like "it hung."
- Printing too much becomes noise.
- If you changed state, tell the user *what changed*.
### Color and symbols
- Use color with intention:
- Red for errors
- Yellow for warnings
- Highlight important parts only
- Disable color when:
- The relevant stream is not a TTY
- `NO_COLOR` is set (non-empty)
- `TERM=dumb`
- User passes `--no-color`
- Consider supporting `FORCE_COLOR` (some ecosystems use it), but don't let it break logs.
### Animations and progress
- Never animate when output is not a TTY.
- If something takes "long," show progress.
- If parallel work is happening, avoid interleaving chaos (multi-progress-bar libs help).
### Paging
- If output is long and you're on a TTY, consider a pager.
- Respect `PAGER` if set.
- A common `less` default is: `less -FIRX`
- Doesn't page if one screen
- Keeps formatting, doesn't clear screen on exit
## Errors and diagnostics
### Rewrite expected errors for humans
Don't dump raw stack traces for normal user errors.
- Say what failed
- Say why it might have failed (likely causes)
- Say what to do next (actionable fix)
### Keep signal-to-noise high
- Group repetitive errors under one explanation.
- Put the most important info at the end (recency bias in terminals is real).
### Suggest corrections carefully
- Typo suggestions are great when safe:
- "Unknown command `pss`. Did you mean `ps`?"
- Avoid "DWIM" behavior that silently changes meaning for destructive operations.
### Unexpected errors
When something truly unexpected happens:
- Provide a short human summary
- Offer a way to get debug details:
- `--debug` or `--verbose`
- Optional log file path
- Provide a bug report path and include reproducibility info
## Arguments, flags, and subcommands
### Prefer flags to positional args (usually)
- Flags are self-documenting and easier to extend without breaking compatibility.
- Exception: "classic" two-arg patterns (`cp <src> <dst>`) where brevity is worth it.
### Provide long forms for all flags
- If you have `-h`, also have `--help`.
- Long forms are friendlier in scripts and documentation.
### Reserve one-letter flags for truly common options
Short flags are a scarce resource. Spend them wisely.
### Standard flag names (use existing conventions)
Common conventions across CLI ecosystems:
- `-h`, `--help`
- `--version`
- `-v`, `--verbose` (but note ambiguity: sometimes `-v` is version)
- `-q`, `--quiet`
- `-d`, `--debug`
- `-f`, `--force`
- `-n`, `--dry-run`
- `--json`
- `--no-input`
- `--no-color`
- `-o`, `--output`
### Order independence (when feasible)
Users often add flags to the end of the previous command via ↑.
If possible, allow:
- `tool --flag subcmd`
- `tool subcmd --flag`
### Subcommand naming
- Avoid near-synonyms (`update` vs `upgrade`) unless the difference is extremely clear.
- For object/action CLIs, `noun verb` is common:
- `docker container create`
- Keep verbs consistent across objects:
- If you use `create`, also use `delete`/`list`/`get` consistently.
## Interactivity and safety
### Prompts only when stdin is a TTY
- If `stdin` is not a TTY:
- Fail with a clear message describing the required flag(s)
- Do not block waiting for input that will never arrive
### `--no-input` should disable prompts
- If required info is missing:
- Exit non-zero
- Tell the user how to provide it via flags or stdin
### Confirm dangerous operations
Different danger levels:
- Mild:
- Deleting an explicit file the user named
- Moderate:
- Bulk deletes, remote deletes, complex irreversible changes
- Severe:
- "Delete the whole app/account/project"
- Require explicit confirmation:
- Type the resource name, or
- `--confirm="exact-name"`
### Provide dry-run where it reduces fear
- `--dry-run` should describe intended changes without doing them.
## Configuration and environment variables
### Choose the right configuration surface
- Flags:
- High variability per invocation
- Environment variables:
- Varies by execution context (shell/session/CI)
- Project config file:
- Stable for a project and shareable in version control
- User config:
- Stable per machine/user
### Precedence (high → low)
A common, predictable precedence order:
- Flags
- Process environment
- Project config (`.env` / tool config in repo)
- User config
- System config
### XDG base directory spec
Prefer:
- Config: `$XDG_CONFIG_HOME` (default `~/.config`)
- Data: `$XDG_DATA_HOME` (default `~/.local/share`)
- Cache: `$XDG_CACHE_HOME` (default `~/.cache`)
### Environment variable naming
- Uppercase, numbers, underscores.
- Prefer tool-specific prefixes:
- `MYTOOL_FOO=1`
### `.env` is not a real config system
`.env` is useful for small "context knobs," but it's limited:
- Everything is a string
- Often not versioned
- Often abused for secrets
## Secrets and sensitive data
- Do *not* accept secrets via flags:
- Leaks into shell history and process listings (`ps`)
- Do *not* accept secrets via environment variables:
- Easy to leak into logs, `docker inspect`, systemd unit displays, etc.
Prefer:
- `--token-file path`
- `--password-stdin`
- OS keychains / secret managers
- Pipes and local IPC when appropriate
## Robustness: timeouts, retries, signals
### Responsive beats fast
- Aim to print *something* within ~100ms for operations that might take time:
- "Fetching…"
- "Computing…"
- "Connecting to …"
### Timeouts and retries
- Network requests should have timeouts.
- Consider retries for transient failures (with backoff).
- Make retries visible (don't silently hide minutes of retrying).
### Recoverability and idempotence
- If a command fails mid-way, a rerun should:
- Pick up where it left off, or
- Fail safely without corrupting state
### Ctrl-C behavior
- On SIGINT (Ctrl-C), stop quickly and say what happened.
- If cleanup is long:
- Allow a second Ctrl-C to force quit
- Don't hang forever in cleanup
## Future-proofing
- Treat the CLI as a public API:
- Commands, flags, output formats, config keys are all interfaces
- Prefer additive changes:
- New flags > changing behavior of old flags
- Deprecate explicitly:
- Warn when deprecated flags are used
- Tell the user the replacement
- Output for humans can evolve.
- Output for scripts should be stabilized via `--plain` or `--json`.
## Distribution and lifecycle
- Prefer a single binary distribution if reasonable.
- Make uninstall easy and documented.
- Provide version output (`--version`).
- Consider:
- Man pages
- Shell completions
- Web docs with deep links to subcommands
## Analytics and telemetry
- Don't "phone home" without consent.
- If you collect anything:
- Explain what, why, how anonymized, and retention period
- Make opting out easy
Consider alternatives:
- Instrument docs
- Measure downloads
- Talk to users
## Implementation notes
### Parser libraries (examples, not exhaustive)
- Go: Cobra, urfave/cli
- Rust: clap
- Python: argparse, Click, Typer
- Node: oclif, commander, yargs
- Java: picocli
- Kotlin: clikt
- Swift: swift-argument-parser
### Practical output design tip
When in doubt:
- Human UI = defaults when TTY
- Machine UI = explicit flags (`--json`, `--plain`)
- Debug UI = explicit flags (`--debug`, `--verbose`)
## Further reading
- CLI Guidelines (primary): <https://clig.dev/>
- POSIX Utility Conventions: <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html>
- GNU Coding Standards (Program Behavior, CLI conventions): <https://www.gnu.org/prep/standards/>
- Heroku CLI Style Guide: <https://devcenter.heroku.com/articles/cli-style-guide>
- 12 Factor CLI Apps: <https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46>
- NO_COLOR convention: <https://no-color.org/>
- XDG Base Directory Spec: <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>
@@ -0,0 +1,511 @@
#!/usr/bin/env python3
"""
cli_audit.py — a lightweight CLI "citizenship" checker.
Usage:
python scripts/cli_audit.py -- <command> [args...]
Examples:
python scripts/cli_audit.py -- ./mycmd
python scripts/cli_audit.py -- mycmd subcmd
What it checks (heuristically):
- --help works (exit 0) and looks like help
- invalid flag produces non-zero and error on stderr
- common conventions appear in help (e.g., --version, --json, --no-color)
- ANSI escape codes / animations in non-TTY output (captured output is non-TTY)
- NO_COLOR / TERM=dumb behavior (best-effort)
Notes:
- This script does NOT "prove" correctness; it flags likely UX/composability issues.
- Some checks are WARN (recommendations), not FAIL (hard requirements).
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from typing import Dict, List, Optional, Sequence, Tuple
ANSI_RE = re.compile(
r"""
\x1b # ESC
(?:
\[ [0-?]* [ -/]* [@-~] # CSI sequences
| \] .*? (?:\x07|\x1b\\) # OSC sequences
| [@-Z\\-_] # 2-character sequences
)
""",
re.VERBOSE | re.DOTALL,
)
@dataclass
class RunResult:
argv: List[str]
returncode: Optional[int]
stdout: str
stderr: str
timed_out: bool
@dataclass
class Finding:
level: str # PASS | WARN | FAIL
title: str
details: str = ""
def _decode(b: bytes) -> str:
return b.decode("utf-8", errors="replace")
def run_cmd(
argv: Sequence[str],
timeout_s: float,
env_overrides: Optional[Dict[str, str]] = None,
) -> RunResult:
env = os.environ.copy()
if env_overrides:
env.update(env_overrides)
try:
proc = subprocess.run(
list(argv),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
timeout=timeout_s,
check=False,
)
return RunResult(
argv=list(argv),
returncode=proc.returncode,
stdout=_decode(proc.stdout),
stderr=_decode(proc.stderr),
timed_out=False,
)
except FileNotFoundError:
return RunResult(
argv=list(argv),
returncode=None,
stdout="",
stderr="Command not found.",
timed_out=False,
)
except subprocess.TimeoutExpired as e:
return RunResult(
argv=list(argv),
returncode=None,
stdout=_decode(e.stdout or b""),
stderr=_decode(e.stderr or b""),
timed_out=True,
)
def has_ansi(s: str) -> bool:
return bool(ANSI_RE.search(s))
def has_carriage_returns(s: str) -> bool:
return "\r" in s
def looks_like_help(text: str) -> bool:
t = text.lower()
return any(k in t for k in ["usage:", "\nusage", "synopsis", "options", "commands"])
def find_flag_mentions(help_text: str) -> Dict[str, bool]:
t = help_text
flags = {
"--help": "--help" in t,
"-h": re.search(r"(^|\s)-h(\s|,|$)", t) is not None,
"--version": "--version" in t,
"--json": "--json" in t,
"--plain": "--plain" in t,
"--no-color": "--no-color" in t,
"NO_COLOR": "NO_COLOR" in t,
"--no-input": "--no-input" in t,
"--dry-run": "--dry-run" in t,
"--force": "--force" in t,
"--quiet": "--quiet" in t or re.search(r"(^|\s)-q(\s|,|$)", t) is not None,
"--verbose": "--verbose" in t or re.search(r"(^|\s)-v(\s|,|$)", t) is not None,
"--debug": "--debug" in t or re.search(r"(^|\s)-d(\s|,|$)", t) is not None,
}
return flags
def format_findings(findings: List[Finding]) -> str:
def icon(level: str) -> str:
return {"PASS": "[PASS]", "WARN": "[WARN]", "FAIL": "[FAIL]"}.get(
level, "[INFO]"
)
lines: List[str] = []
for f in findings:
lines.append(f"{icon(f.level)} {f.title}")
if f.details.strip():
for line in f.details.rstrip().splitlines():
lines.append(f" {line}")
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument(
"--timeout",
type=float,
default=10.0,
help="Per-invocation timeout in seconds (default: 10).",
)
parser.add_argument(
"--strict",
action="store_true",
help="Treat WARN as FAIL for exit status purposes.",
)
parser.add_argument(
"--print-output",
action="store_true",
help="Print captured stdout/stderr for each probe.",
)
parser.add_argument(
"cmd",
nargs=argparse.REMAINDER,
help="Command to audit (must be provided after --).",
)
args = parser.parse_args()
if not args.cmd:
print(
"Error: no command provided.\n\nUsage:\n python scripts/cli_audit.py -- <command> [args...]\n",
file=sys.stderr,
)
return 2
# If user forgot the -- separator, try to recover.
cmd = args.cmd
if cmd and cmd[0] == "--":
cmd = cmd[1:]
if not cmd:
print("Error: no command provided after --.", file=sys.stderr)
return 2
exe = cmd[0]
if shutil.which(exe) is None and not os.path.exists(exe):
print(f"Error: command not found: {exe}", file=sys.stderr)
return 127
findings: List[Finding] = []
# Probe: --help
help_res = run_cmd(cmd + ["--help"], timeout_s=args.timeout)
if help_res.timed_out:
findings.append(
Finding("FAIL", "--help timed out", "Help should return quickly.")
)
elif help_res.returncode is None:
findings.append(
Finding(
"FAIL",
"--help failed to execute",
help_res.stderr.strip() or "Unknown error.",
)
)
else:
if help_res.returncode != 0:
findings.append(
Finding(
"FAIL",
f"--help exit code was {help_res.returncode}",
"Help should exit 0.",
)
)
else:
findings.append(Finding("PASS", "--help exits with code 0"))
combined = (help_res.stdout + "\n" + help_res.stderr).strip()
if not combined:
findings.append(Finding("FAIL", "--help produced no output"))
else:
if looks_like_help(combined):
findings.append(
Finding(
"PASS",
"--help output looks like help (usage/options/commands detected)",
)
)
else:
findings.append(
Finding(
"WARN",
"--help output did not obviously look like help",
"Check formatting and content.",
)
)
if help_res.stdout.strip() and not help_res.stderr.strip():
findings.append(Finding("PASS", "Help printed to stdout"))
elif help_res.stderr.strip() and not help_res.stdout.strip():
findings.append(
Finding(
"WARN",
"Help printed to stderr",
"Common convention is help on stdout; stderr is typically for errors.",
)
)
else:
findings.append(
Finding(
"WARN",
"Help printed to both stdout and stderr",
"Prefer help on stdout; reserve stderr for errors/warnings.",
)
)
if args.print_output:
print("== PROBE: --help ==")
print("--- stdout ---")
print(help_res.stdout.rstrip())
print("--- stderr ---")
print(help_res.stderr.rstrip())
print()
# Probe: -h (recommended, not required)
h_res = run_cmd(cmd + ["-h"], timeout_s=args.timeout)
if h_res.timed_out:
findings.append(
Finding(
"WARN", "-h timed out", "If you support -h, it should return quickly."
)
)
elif h_res.returncode == 0 and (h_res.stdout.strip() or h_res.stderr.strip()):
findings.append(Finding("PASS", "-h works (exit 0)"))
else:
findings.append(
Finding(
"WARN",
"-h did not behave like help",
"If you intentionally use -h for something else, consider avoiding that.",
)
)
# Probe: invalid flag
bad_flag = "--definitely-not-a-real-flag-xyz"
bad_res = run_cmd(cmd + [bad_flag], timeout_s=args.timeout)
if bad_res.timed_out:
findings.append(
Finding(
"FAIL",
"Invalid-flag probe timed out",
"Invalid input should fail fast with guidance.",
)
)
elif bad_res.returncode is None:
findings.append(
Finding(
"FAIL",
"Invalid-flag probe failed to execute",
bad_res.stderr.strip() or "Unknown error.",
)
)
else:
if bad_res.returncode == 0:
findings.append(
Finding(
"FAIL",
"Unknown flag returned exit code 0",
"Unknown flags should be an error.",
)
)
else:
findings.append(
Finding("PASS", f"Unknown flag returns non-zero ({bad_res.returncode})")
)
if bad_res.stderr.strip():
findings.append(Finding("PASS", "Unknown-flag error printed to stderr"))
else:
findings.append(
Finding(
"WARN",
"Unknown-flag error not printed to stderr",
"Prefer errors on stderr.",
)
)
if "--help" in (bad_res.stdout + bad_res.stderr):
findings.append(Finding("PASS", "Unknown-flag error mentions --help"))
else:
findings.append(
Finding(
"WARN",
"Unknown-flag error does not mention --help",
"Consider adding a hint to discover help.",
)
)
noisy_markers = [
"Traceback (most recent call last)",
"panic:",
"stack trace",
"Stack trace",
]
if any(m in (bad_res.stdout + bad_res.stderr) for m in noisy_markers):
findings.append(
Finding(
"WARN",
"Error output includes a stack trace marker",
"Prefer stack traces only in --debug/--verbose mode.",
)
)
if args.print_output:
print("== PROBE: invalid flag ==")
print("--- stdout ---")
print(bad_res.stdout.rstrip())
print("--- stderr ---")
print(bad_res.stderr.rstrip())
print()
# Analyze help for common conventions
help_text = help_res.stdout + "\n" + help_res.stderr
flag_mentions = find_flag_mentions(help_text)
if flag_mentions.get("--version"):
findings.append(Finding("PASS", "Help mentions --version"))
else:
findings.append(
Finding(
"WARN",
"Help does not mention --version",
"Consider supporting --version for discoverability.",
)
)
if flag_mentions.get("--json"):
findings.append(Finding("PASS", "Help mentions --json"))
else:
findings.append(
Finding(
"WARN",
"Help does not mention --json",
"If scripts may consume output, consider a structured JSON mode.",
)
)
if flag_mentions.get("--plain"):
findings.append(Finding("PASS", "Help mentions --plain"))
else:
findings.append(
Finding(
"WARN",
"Help does not mention --plain",
"If human output is formatted, a stable plain mode helps scripting.",
)
)
if flag_mentions.get("--no-color") or flag_mentions.get("NO_COLOR"):
findings.append(
Finding("PASS", "Help mentions color controls (--no-color and/or NO_COLOR)")
)
else:
findings.append(
Finding(
"WARN",
"Help does not mention color controls",
"Consider supporting --no-color and NO_COLOR.",
)
)
if flag_mentions.get("--no-input"):
findings.append(Finding("PASS", "Help mentions --no-input"))
else:
findings.append(
Finding(
"WARN",
"Help does not mention --no-input",
"If you prompt, consider a non-interactive escape hatch.",
)
)
# ANSI / animation checks (captured output is non-TTY)
if has_ansi(help_res.stdout) or has_ansi(help_res.stderr):
findings.append(
Finding(
"WARN",
"ANSI escape sequences detected in --help output (captured/non-TTY)",
"Consider disabling color/formatting when output is not a TTY, or when NO_COLOR is set.",
)
)
else:
findings.append(
Finding(
"PASS", "No ANSI escape sequences detected in captured --help output"
)
)
if has_carriage_returns(help_res.stdout) or has_carriage_returns(help_res.stderr):
findings.append(
Finding(
"WARN",
"Carriage returns detected in --help output",
"This can indicate animations/progress behavior; ensure you don't animate when not a TTY.",
)
)
# NO_COLOR / TERM=dumb best-effort probes (only meaningful if the tool would emit ANSI)
no_color_res = run_cmd(
cmd + ["--help"], timeout_s=args.timeout, env_overrides={"NO_COLOR": "1"}
)
if has_ansi(no_color_res.stdout) or has_ansi(no_color_res.stderr):
findings.append(
Finding(
"WARN",
"ANSI still present with NO_COLOR=1",
"Consider honoring NO_COLOR to disable color output.",
)
)
else:
findings.append(
Finding("PASS", "NO_COLOR=1 produced no ANSI sequences (best-effort check)")
)
dumb_term_res = run_cmd(
cmd + ["--help"], timeout_s=args.timeout, env_overrides={"TERM": "dumb"}
)
if has_ansi(dumb_term_res.stdout) or has_ansi(dumb_term_res.stderr):
findings.append(
Finding(
"WARN",
"ANSI still present with TERM=dumb",
"Consider disabling ANSI when TERM=dumb.",
)
)
else:
findings.append(
Finding("PASS", "TERM=dumb produced no ANSI sequences (best-effort check)")
)
# Summary and exit status
fail = sum(1 for f in findings if f.level == "FAIL")
warn = sum(1 for f in findings if f.level == "WARN")
print(format_findings(findings))
print(f"Summary: {fail} FAIL, {warn} WARN")
if fail > 0:
return 1
if args.strict and warn > 0:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,98 @@
{
"name": "mycmd",
"summary": "One-line description of what this tool does.",
"description": "Longer description: what it does, what it does not do, and when to use it.",
"docsUrl": "https://example.com/docs/mycmd",
"issuesUrl": "https://github.com/example/mycmd/issues",
"outputModes": {
"default": "human",
"jsonFlag": "--json",
"plainFlag": "--plain",
"quietFlag": "--quiet",
"verboseFlag": "--verbose",
"debugFlag": "--debug",
"noColorFlag": "--no-color",
"noInputFlag": "--no-input",
"dryRunFlag": "--dry-run",
"forceFlag": "--force"
},
"streamsContract": {
"stdout": "Primary output and machine-readable output.",
"stderr": "Errors, warnings, progress, and status messages."
},
"exitCodes": [
{ "code": 0, "meaning": "Success." },
{ "code": 1, "meaning": "General failure." },
{ "code": 2, "meaning": "Usage / invalid arguments." }
],
"config": {
"precedenceHighToLow": [
"flags",
"env",
"projectConfig",
"userConfig",
"systemConfig"
],
"xdg": true,
"files": {
"projectConfig": "./.mycmd.toml",
"userConfig": "~/.config/mycmd/config.toml",
"cacheDir": "~/.cache/mycmd/"
},
"env": [
{ "name": "MYCMD_DEBUG", "purpose": "Enable verbose debug logging." },
{ "name": "NO_COLOR", "purpose": "Disable ANSI color output." }
]
},
"globalOptions": [
{ "flags": ["-h", "--help"], "description": "Show help and exit." },
{ "flags": ["--version"], "description": "Show version and exit." }
],
"commands": [
{
"name": "example",
"summary": "Describe what this command does.",
"usage": "mycmd example [options] <arg>",
"args": [
{
"name": "arg",
"required": true,
"description": "What this argument is."
}
],
"options": [
{
"flags": ["-o", "--output"],
"value": "PATH",
"description": "Write output to PATH (or '-' for stdout)."
}
],
"examples": [
{
"description": "Basic example.",
"command": "mycmd example ./input.txt"
}
]
}
],
"safety": {
"boundaryCrossingActions": [
"Network calls",
"Writing files not explicitly passed",
"Mutating remote state"
],
"destructiveActions": [
{
"action": "delete-project",
"dangerLevel": "severe",
"interactiveConfirmation": "type the project name",
"nonInteractiveConfirmation": "--confirm=\"project-name\"",
"supportsDryRun": true
}
],
"secrets": {
"neverAcceptVia": ["flags", "environment variables"],
"preferAcceptVia": ["stdin", "file", "OS keychain", "secret manager"]
}
}
}
@@ -0,0 +1,53 @@
# Error Message Template
## Goals
- *Human readable first*: describe the problem in plain language.
- *Actionable*: include the next step (flag, file path, permission change, docs link).
- *Low noise*: avoid stack traces in normal mode.
- *Correct stream*: errors go to `stderr`.
- *Correct exit code*: non-zero.
## Pattern
```text
Error: <what failed in plain language>
Cause: <most likely cause, if known> (optional)
Fix: <what the user should do next>
Hint: <related command / docs> (optional)
For more help: mycmd <subcmd> --help
Docs: <https://example.com/docs/...> (optional)
```
## Examples
### Missing required argument
```text
Error: missing required argument <path>
Fix: pass a path, or run: mycmd upload --help
```
### Permission error with actionable fix
```text
Error: can't write to /var/log/mycmd/output.txt
Fix: choose a writable location, or run: chmod u+w /var/log/mycmd/output.txt
```
### Unknown flag (with suggestion)
```text
Error: unknown option: --jon
Fix: did you mean --json ?
For more help: mycmd --help
```
### Unexpected error (debug path)
```text
Error: unexpected failure while reading config
Fix: re-run with --debug, and include the log in a bug report
Issues: https://github.com/example/mycmd/issues/new
```
@@ -0,0 +1,48 @@
# Help Text Template
Use this as a skeleton for `mycmd --help` (plain text, scan-friendly).
```text
mycmd — <one-line summary>
USAGE
mycmd <command> [options]
mycmd <command> --help
DESCRIPTION
<what this tool does, and when to use it>
<what it does NOT do (optional, but helpful for setting expectations)>
EXAMPLES
# <most common use case>
mycmd <command> <args>
# <machine-readable output>
mycmd <command> --json
COMMANDS
<command> <short summary>
<command> <short summary>
help [command] Show help for a command (optional)
OPTIONS
-h, --help Show help and exit
--version Show version and exit
--json Output structured JSON
--plain Output stable plain text (one record per line)
--no-color Disable ANSI color output
--no-input Disable prompts; fail if required input is missing
-q, --quiet Reduce non-essential output
-v, --verbose Increase output detail
-d, --debug Enable debug logging
-n, --dry-run Describe changes without applying them
-f, --force Skip confirmations / force the action
ENVIRONMENT
NO_COLOR Disable color output
MYCMD_DEBUG Enable debug logging (equivalent to --debug)
DOCUMENTATION
Docs: <https://example.com/docs/mycmd>
Issues: <https://github.com/example/mycmd/issues>
```