📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# cli-dev
|
||||
|
||||
Skills for building command-line tools with human-first UX and UNIX composability.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
/plugin install cli-dev@outfitter
|
||||
```
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| **cli-development-guidelines** | Design and review CLIs: arguments/flags, help text, stdout/stderr, exit codes, interactivity, configuration, errors, and distribution |
|
||||
|
||||
## Usage
|
||||
|
||||
The skill activates when you're designing, implementing, or reviewing a CLI tool. Trigger phrases include:
|
||||
|
||||
- `--help`, flags, subcommands, exit codes
|
||||
- stdout/stderr, piping, JSON output
|
||||
- color, prompts, config files, env vars
|
||||
- "works in CI", install/uninstall, telemetry
|
||||
|
||||
### What it produces
|
||||
|
||||
- **CLI contract**: commands, flags, IO behavior, exit codes, config/env, examples
|
||||
- **Help output**: example-first, scan-friendly
|
||||
- **Compliance audit**: via `scripts/cli_audit.py`
|
||||
|
||||
### Quick audit
|
||||
|
||||
```bash
|
||||
python cli-dev/skills/cli-development-guidelines/scripts/cli_audit.py -- <your-cli>
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
- Documentation: CC-BY-SA-4.0 (adapted from [clig.dev](https://clig.dev/))
|
||||
- Scripts: MIT
|
||||
@@ -0,0 +1 @@
|
||||
{"name": "cli-dev", "version": "1.1.0", "private": true}
|
||||
@@ -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 I’m 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 isn’t a TTY.
|
||||
- Support `NO_COLOR` and `--no-color`.
|
||||
- Consider `--json` and `--plain` for stable output.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Sketch the CLI contract first
|
||||
|
||||
- Start from the user’s jobs-to-be-done (what they’re 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 (don’t hand-roll).
|
||||
- Make “boundary-crossing” actions explicit:
|
||||
- Network calls
|
||||
- Writing files not explicitly provided
|
||||
- Mutating remote state
|
||||
- Avoid footguns:
|
||||
- Don’t accept secrets via flags or environment variables.
|
||||
- Don’t print stack traces by default.
|
||||
- Don’t 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 that’s 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`
|
||||
+103
@@ -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."
|
||||
+75
@@ -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
|
||||
+439
@@ -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)
|
||||
- 1–2 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>
|
||||
+511
@@ -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())
|
||||
+98
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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
|
||||
```
|
||||
+48
@@ -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>
|
||||
```
|
||||
Reference in New Issue
Block a user