fix(cli): allow global format flag after subcommands

Closes #6879\n\nISSUES CLOSED: #6879
This commit is contained in:
2026-04-10 18:54:27 +00:00
committed by drew
parent fc07583aee
commit dcbc1a4e33
3 changed files with 105 additions and 0 deletions
+5
View File
@@ -1061,6 +1061,11 @@ iteration` and data corruption under concurrent plan execution. All public
The `export` command gains `--output-format` and the `import` command gains `--format`
to select the output envelope format independently of the export/import file format.
- **CLI global format flag parsing** (#6879): Promotes global CLI options before
Typer parsing so `agents <command> --format json` and `-f` shorthand work from
the documented position. Adds regression coverage for version, info, and
diagnostics commands.
- **Robot Framework TDD Listener Guards** (#5436): Added three guard conditions to the
`tdd_expected_fail_listener` `end_test()` function to prevent blindly inverting ALL test
failures to passes, which was masking infrastructure errors and causing flaky CI behavior.
+19
View File
@@ -44,6 +44,25 @@ Feature: Global --format flag propagation to subcommands
Then the global format command should succeed
And the global format output should be valid JSON
# -----------------------------------------------------------------------
# Regression: global --format accepted after the subcommand name
# -----------------------------------------------------------------------
Scenario: Global --format flag works after the info subcommand name
When I invoke the CLI with global args "info --format json"
Then the global format command should succeed
And the global format output should be valid JSON
Scenario: Global --format flag works after the version subcommand name
When I invoke the CLI with global args "version --format json"
Then the global format command should succeed
And the global format output should be valid JSON
Scenario: Global --format flag works after the diagnostics subcommand name
When I invoke the CLI with global args "diagnostics --format json"
Then the global format command should succeed
And the global format output should be valid JSON
# -----------------------------------------------------------------------
# Subtask 3: version, info, diagnostics read format from global ctx.obj
# -----------------------------------------------------------------------
+81
View File
@@ -8,7 +8,9 @@ import sys
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any
import click
import typer
from typer.core import TyperGroup
from cleveragents import __version__
from cleveragents.cli.formatting import OutputFormat
@@ -21,6 +23,81 @@ _console: "Console | None" = None
_err_console: "Console | None" = None
_subcommands_registered = False
_GLOBAL_VALUE_OPTIONS = frozenset({"--format"})
_GLOBAL_VALUE_SHORT_OPTIONS = frozenset({"-f"})
_GLOBAL_VALUE_EQ_PREFIXES = ("--format=",)
_GLOBAL_FLAG_OPTIONS = frozenset({"--show-secrets", "--version", "-V"})
def _split_short_value_option(option: str) -> tuple[str, str] | None:
for short in _GLOBAL_VALUE_SHORT_OPTIONS:
if option.startswith(short) and len(option) > len(short):
return short, option[len(short) :]
return None
def _promote_global_options(args: list[str]) -> list[str]:
"""Move global root options before subcommands for Typer parsing."""
if not args:
return args
promoted: list[str] = []
remaining: list[str] = []
i = 0
length = len(args)
while i < length:
arg = args[i]
if arg == "--":
remaining.extend(args[i:])
break
if arg in _GLOBAL_FLAG_OPTIONS:
promoted.append(arg)
i += 1
continue
if arg in _GLOBAL_VALUE_OPTIONS:
if i + 1 < length:
promoted.extend([arg, args[i + 1]])
i += 2
else:
remaining.append(arg)
i += 1
continue
eq_prefix = next(
(prefix for prefix in _GLOBAL_VALUE_EQ_PREFIXES if arg.startswith(prefix)),
None,
)
if eq_prefix is not None and len(arg) > len(eq_prefix):
promoted.extend([eq_prefix.rstrip("="), arg[len(eq_prefix) :]])
i += 1
continue
split_short = _split_short_value_option(arg)
if split_short is not None:
short_opt, value = split_short
promoted.extend([short_opt, value])
i += 1
continue
remaining.append(arg)
i += 1
return promoted + remaining
class CleverAgentsTyperGroup(TyperGroup):
"""Typer group that promotes global options before subcommand parsing."""
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
promoted_args = _promote_global_options(list(args))
return super().parse_args(ctx, promoted_args)
__all__ = [
"_print_basic_help",
"app",
@@ -69,6 +146,7 @@ app: Any = typer.Typer(
pretty_exceptions_enable=True,
pretty_exceptions_show_locals=False,
rich_markup_mode="rich",
cls=CleverAgentsTyperGroup,
)
@@ -740,6 +818,9 @@ def main(args: list[str] | None = None) -> int:
if not args:
args = ["--help"]
# Global CLI options are promoted by CleverAgentsTyperGroup.parse_args,
# so we avoid reprocessing here to prevent double handling.
# Fast paths for lightweight flags to avoid heavy imports when using the
# real Typer app. If the app is monkeypatched (e.g., tests), fall back to
# normal execution so patched call paths are exercised.