Skip to content

Commit eedc6bc

Browse files
yeldarbyclaude
andcommitted
feat(cli): flattened help, -h/-v shorthands, alphabetized everything
- Flattened command listing: roboflow --help now shows all 65 visible commands as 'noun verb' (e.g. 'project list', 'project get') instead of collapsed groups. Fully alphabetized. - Added -h shorthand for --help (works on all subcommands too) - Added -v shorthand for --version - Options sorted alphabetically in help output - Hidden commands (legacy aliases) excluded from help but still functional 374 tests pass, all linting clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 284937e commit eedc6bc

1 file changed

Lines changed: 85 additions & 20 deletions

File tree

roboflow/cli/__init__.py

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from __future__ import annotations
88

99
import json
10-
from typing import Annotated, Optional
10+
from typing import Annotated, Any, Optional
1111

1212
import click
1313
import typer
@@ -18,24 +18,26 @@
1818
# Root application
1919
# ---------------------------------------------------------------------------
2020

21+
_DESCRIPTION = (
22+
"Build and deploy computer vision models with Roboflow. "
23+
"Manage datasets, train models, run inference, and deploy "
24+
"workflows \u2014 from the command line or via structured JSON for AI agents."
25+
)
26+
2127
app = typer.Typer(
2228
name="roboflow",
23-
help=(
24-
"Build and deploy computer vision models with Roboflow. "
25-
"Manage datasets, train models, run inference, and deploy "
26-
"workflows \u2014 from the command line or via structured JSON for AI agents."
27-
),
28-
pretty_exceptions_enable=False, # We handle errors ourselves via output_error
29+
help=_DESCRIPTION,
30+
pretty_exceptions_enable=False,
2931
rich_markup_mode="rich",
30-
add_completion=False, # We have our own 'completion' subcommand
32+
add_completion=False,
33+
context_settings={"help_option_names": ["-h", "--help"]},
3134
)
3235

3336

3437
def _version_callback(value: bool) -> None:
3538
if value:
3639
import sys
3740

38-
# Check if --json was passed (eager callback fires before other params are parsed)
3941
if "--json" in sys.argv or "-j" in sys.argv:
4042
print(json.dumps({"version": roboflow.__version__}))
4143
else:
@@ -46,25 +48,31 @@ def _version_callback(value: bool) -> None:
4648
@app.callback(invoke_without_command=True)
4749
def _root_callback(
4850
ctx: typer.Context,
49-
json_output: Annotated[
50-
bool,
51-
typer.Option("--json", "-j", help="Output results as JSON (stable schema, for agents and piping)"),
52-
] = False,
5351
api_key: Annotated[
5452
Optional[str],
5553
typer.Option("--api-key", "-k", help="API key override (default: $ROBOFLOW_API_KEY or config file)"),
5654
] = None,
57-
workspace: Annotated[
58-
Optional[str],
59-
typer.Option("--workspace", "-w", help="Workspace URL or ID override (default: configured default)"),
60-
] = None,
55+
json_output: Annotated[
56+
bool,
57+
typer.Option("--json", "-j", help="Output results as JSON (stable schema, for agents and piping)"),
58+
] = False,
6159
quiet: Annotated[
6260
bool,
6361
typer.Option("--quiet", "-q", help="Suppress non-essential output (progress bars, status messages)"),
6462
] = False,
6563
version: Annotated[
6664
Optional[bool],
67-
typer.Option("--version", help="Show package version and exit", callback=_version_callback, is_eager=True),
65+
typer.Option(
66+
"--version",
67+
"-v",
68+
help="Show package version and exit",
69+
callback=_version_callback,
70+
is_eager=True,
71+
),
72+
] = None,
73+
workspace: Annotated[
74+
Optional[str],
75+
typer.Option("--workspace", "-w", help="Workspace URL or ID override (default: configured default)"),
6876
] = None,
6977
) -> None:
7078
"""Build and deploy computer vision models with Roboflow."""
@@ -74,12 +82,69 @@ def _root_callback(
7482
ctx.obj["workspace"] = workspace
7583
ctx.obj["quiet"] = quiet
7684

77-
# Show help and exit 0 when no subcommand is given
7885
if ctx.invoked_subcommand is None:
79-
print(ctx.get_help()) # noqa: T201
86+
_print_flattened_help()
8087
raise typer.Exit(code=0)
8188

8289

90+
def _print_flattened_help() -> None:
91+
"""Print a custom help screen with all commands flattened and alphabetized."""
92+
import shutil
93+
94+
click_app = typer.main.get_command(app)
95+
96+
# Collect all visible commands, flattened
97+
commands: list[tuple[str, str]] = []
98+
99+
def _walk(group: Any, prefix: str = "") -> None:
100+
for name in sorted(group.list_commands(None) or []): # type: ignore[arg-type]
101+
cmd = group.get_command(None, name) # type: ignore[arg-type]
102+
if cmd is None:
103+
continue
104+
# Skip hidden commands
105+
if getattr(cmd, "hidden", False):
106+
continue
107+
full = f"{prefix} {name}".strip() if prefix else name
108+
if hasattr(cmd, "list_commands") and cmd.list_commands(None):
109+
_walk(cmd, full)
110+
else:
111+
help_text = cmd.get_short_help_str() or ""
112+
commands.append((full, help_text))
113+
114+
_walk(click_app)
115+
commands.sort(key=lambda x: x[0])
116+
117+
# Calculate column width
118+
width = shutil.get_terminal_size((80, 24)).columns
119+
name_width = max((len(c[0]) for c in commands), default=20) + 2
120+
desc_width = max(width - name_width - 4, 20)
121+
122+
# Print
123+
print(f"\n \033[1mroboflow\033[0m v{roboflow.__version__}") # noqa: T201
124+
print(f" {_DESCRIPTION}\n") # noqa: T201
125+
126+
print(" \033[1mUsage:\033[0m roboflow [OPTIONS] COMMAND [ARGS]\n") # noqa: T201
127+
128+
print(" \033[1mOptions:\033[0m") # noqa: T201
129+
options = [
130+
(" --api-key, -k <KEY>", "API key override"),
131+
(" --json, -j", "Output as JSON (for agents and piping)"),
132+
(" --quiet, -q", "Suppress progress bars and status messages"),
133+
(" --version, -v", "Show version"),
134+
(" --workspace, -w <ID>", "Workspace override"),
135+
(" --help, -h", "Show this help"),
136+
]
137+
opt_name_width = max(len(o[0]) for o in options) + 2
138+
for opt_name, opt_help in options:
139+
print(f" {opt_name:<{opt_name_width}} {opt_help}") # noqa: T201
140+
141+
print("\n \033[1mCommands:\033[0m") # noqa: T201
142+
for cmd_name, help_text in commands:
143+
truncated = help_text[:desc_width] if len(help_text) > desc_width else help_text
144+
print(f" {cmd_name:<{name_width}} {truncated}") # noqa: T201
145+
print() # noqa: T201
146+
147+
83148
# ---------------------------------------------------------------------------
84149
# Register command groups (explicit imports — no auto-discovery needed)
85150
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)