Add or extend a LibreYOLO CLI command
The CLI is Typer with one custom layer: KeyValueCommand
(libreyolo/cli/parsing.py) rewrites YOLO-style key=value tokens into
--key value before Click parses, so every command accepts both grammars
with full Typer type-checking, help, and completion intact. Understand that
layer before touching anything; most CLI bugs live at its edges.
The map
libreyolo/cli/__init__.py entrypoint(): registers everything. Special
commands (version checks models formats cfg info metadata) come from
commands/special.py; core modes (predict train val export ui monitor label doctor) are one module each under commands/; profile is a
Typer sub-app (command group) - use that pattern for any new
multi-subcommand tool.
commands/command_utils.py, output.py, errors.py: shared option
handling, machine-readable output, and error rendering. Reuse; don't
reimplement --json printing or error exits per command.
aliases.py: mode-aware translation of user-facing shorthand to internal
config names (e.g. train mosaic to mosaic_prob, val conf to
conf_thres). The single source of truth for name translation; never
inline a rename in a command function.
config.py: CLI-side config resolution.
Adding a flag to an existing command
- Add it as a typed Typer parameter in the command function, named per the
ecosystem convention (
libreyolo-api-conventions; check the standard's
documented name first).
- If users know it by a different name than the internal config field, add
the mapping to the right
*_ALIASES dict in aliases.py.
- Bool flags need a decision: negatable (
--flag/--no-flag) or
one-way (--json, --quiet)? key=false on a one-way flag must drop
the flag, not emit a nonexistent --no-<flag>; the negatable set in
rewrite_known_bool_flags encodes this and getting it wrong is a known
bug class (see the comments in cli/__init__.py around the logging
flags). Register new bool flags accordingly.
- Mirror it in the Python API in the same PR (CLI and Python land
together), and thread it through, verifying the value actually reaches
the code that uses it; an accepted-and-ignored flag is a blocking review
finding.
- Defaults come from the loaded model/family where they are family-shaped
(imgsz, thresholds): read them off the model, don't hardcode
(REVIEW.md: "CLI defaults are family-derived").
Adding a new command
- New module
libreyolo/cli/commands/<name>.py exposing <name>_cmd
(or a <name>_app Typer sub-app for command groups).
- Register in
entrypoint() with cls=KeyValueCommand (or
app.add_typer for groups). Without KeyValueCommand the command
silently loses key=value support, which users will file as a bug.
- Support the output contract:
--json (machine-readable to stdout,
nothing else on stdout), --quiet, and correct exit codes via the
shared error helpers. Agents and scripts consume these; they are part of
why the CLI is self-describing.
- Keep imports inside the command function or module lazy where heavy
(torch etc.):
libreyolo --help must stay fast, and the entrypoint's
deferred imports exist for that reason.
- Docs: the command shows up in
libreyolo --help automatically; add it
to the website docs command reference when user-facing.
Testing (the part that actually gates)
CLI tests are unit tests (tests/unit/cli/ for parsing/aliases/output/
errors, tests/unit/cli/commands/test_<cmd>.py per command) and run in the
PR gate, so they must be hermetic: mock the model layer, use tmp_path,
never download. Cover, at minimum:
- both grammars for the new surface (
key=value and --key value),
- bool-flag behavior including
key=false on one-way flags,
- alias translation if you touched
aliases.py,
--json output shape (assert keys, not prose),
- the failure path (bad input produces the intended error, not a
traceback).
--help-json output comes from the declared Typer parameters, so it stays
correct if you declared the flag properly and wrong if you hand-parsed
argv; that is the test for whether you wired it right.
Traps
- Hand-parsing
sys.argv in a command. Everything goes through Typer
parameters + KeyValueCommand, or grammar support fractures.
- Renaming inside the command function instead of
aliases.py.
- Printing progress to stdout (breaks
--json consumers); human chatter
goes to stderr / logging, respecting --quiet.
- A new command that imports torch at module top (slows every
libreyolo
invocation, including --help).
- Adding a
track / task-prefixed verb without checking
_strip_task_prefix in cli/__init__.py for how task-prefixed argv is
normalized.
Related
skills/libreyolo-api-conventions/: what to name it and whether it
should exist on both surfaces (yes).
skills/libreyolo-run-unit-tests/: keeping the new tests PR-gate-safe.
tests/unit/cli/: the exemplars to copy.
1---2name: libreyolo-add-cli-command3description: Add or extend a LibreYOLO CLI command or flag correctly: the Typer + KeyValueCommand architecture, both argument grammars (key=value and --key value), the --json/--quiet/--help-json contract, mode-aware aliases, family-derived defaults, and the unit tests that gate CLI behavior. Use when someone asks to add a CLI command, add a flag to predict/train/val/export, expose a Python feature on the CLI, or when a CLI parsing bug is reported (bool flags, aliases, key=value quirks). API naming decisions belong to libreyolo-api-conventions; this is the implementation side.4---56# Add or extend a LibreYOLO CLI command78The CLI is Typer with one custom layer: `KeyValueCommand`9(`libreyolo/cli/parsing.py`) rewrites YOLO-style `key=value` tokens into10`--key value` before Click parses, so every command accepts both grammars11with full Typer type-checking, help, and completion intact. Understand that12layer before touching anything; most CLI bugs live at its edges.1314## The map1516- `libreyolo/cli/__init__.py` `entrypoint()`: registers everything. Special17 commands (`version checks models formats cfg info metadata`) come from18 `commands/special.py`; core modes (`predict train val export ui monitor19 label doctor`) are one module each under `commands/`; `profile` is a20 Typer sub-app (command group) - use that pattern for any new21 multi-subcommand tool.22- `commands/command_utils.py`, `output.py`, `errors.py`: shared option23 handling, machine-readable output, and error rendering. Reuse; don't24 reimplement `--json` printing or error exits per command.25- `aliases.py`: mode-aware translation of user-facing shorthand to internal26 config names (e.g. train `mosaic` to `mosaic_prob`, val `conf` to27 `conf_thres`). The single source of truth for name translation; never28 inline a rename in a command function.29- `config.py`: CLI-side config resolution.3031## Adding a flag to an existing command32331. Add it as a typed Typer parameter in the command function, named per the34 ecosystem convention (`libreyolo-api-conventions`; check the standard's35 documented name first).362. If users know it by a different name than the internal config field, add37 the mapping to the right `*_ALIASES` dict in `aliases.py`.383. **Bool flags need a decision**: negatable (`--flag/--no-flag`) or39 one-way (`--json`, `--quiet`)? `key=false` on a one-way flag must drop40 the flag, not emit a nonexistent `--no-<flag>`; the `negatable` set in41 `rewrite_known_bool_flags` encodes this and getting it wrong is a known42 bug class (see the comments in `cli/__init__.py` around the logging43 flags). Register new bool flags accordingly.444. Mirror it in the Python API in the same PR (CLI and Python land45 together), and thread it through, verifying the value actually reaches46 the code that uses it; an accepted-and-ignored flag is a blocking review47 finding.485. Defaults come from the loaded model/family where they are family-shaped49 (imgsz, thresholds): read them off the model, don't hardcode50 (REVIEW.md: "CLI defaults are family-derived").5152## Adding a new command53541. New module `libreyolo/cli/commands/<name>.py` exposing `<name>_cmd`55 (or a `<name>_app` Typer sub-app for command groups).562. Register in `entrypoint()` with `cls=KeyValueCommand` (or57 `app.add_typer` for groups). Without `KeyValueCommand` the command58 silently loses `key=value` support, which users will file as a bug.593. Support the output contract: `--json` (machine-readable to stdout,60 nothing else on stdout), `--quiet`, and correct exit codes via the61 shared error helpers. Agents and scripts consume these; they are part of62 why the CLI is self-describing.634. Keep imports inside the command function or module lazy where heavy64 (torch etc.): `libreyolo --help` must stay fast, and the entrypoint's65 deferred imports exist for that reason.665. Docs: the command shows up in `libreyolo --help` automatically; add it67 to the website docs command reference when user-facing.6869## Testing (the part that actually gates)7071CLI tests are unit tests (`tests/unit/cli/` for parsing/aliases/output/72errors, `tests/unit/cli/commands/test_<cmd>.py` per command) and run in the73PR gate, so they must be hermetic: mock the model layer, use `tmp_path`,74never download. Cover, at minimum:7576- both grammars for the new surface (`key=value` and `--key value`),77- bool-flag behavior including `key=false` on one-way flags,78- alias translation if you touched `aliases.py`,79- `--json` output shape (assert keys, not prose),80- the failure path (bad input produces the intended error, not a81 traceback).8283`--help-json` output comes from the declared Typer parameters, so it stays84correct if you declared the flag properly and wrong if you hand-parsed85argv; that is the test for whether you wired it right.8687## Traps8889- Hand-parsing `sys.argv` in a command. Everything goes through Typer90 parameters + `KeyValueCommand`, or grammar support fractures.91- Renaming inside the command function instead of `aliases.py`.92- Printing progress to stdout (breaks `--json` consumers); human chatter93 goes to stderr / logging, respecting `--quiet`.94- A new command that imports torch at module top (slows every `libreyolo`95 invocation, including `--help`).96- Adding a `track` / task-prefixed verb without checking97 `_strip_task_prefix` in `cli/__init__.py` for how task-prefixed argv is98 normalized.99100## Related101102- `skills/libreyolo-api-conventions/`: what to name it and whether it103 should exist on both surfaces (yes).104- `skills/libreyolo-run-unit-tests/`: keeping the new tests PR-gate-safe.105- `tests/unit/cli/`: the exemplars to copy.