Merge branch 'master' into feature/m5-estimation-actor
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 3m21s
CI / quality (pull_request) Successful in 3m44s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 4m4s
CI / e2e_tests (pull_request) Successful in 4m51s
CI / unit_tests (pull_request) Successful in 6m5s
CI / integration_tests (pull_request) Successful in 6m44s
CI / docker (pull_request) Successful in 57s
CI / coverage (pull_request) Successful in 11m3s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 36m36s

This commit is contained in:
2026-03-21 01:25:52 +00:00
committed by Forgejo
10 changed files with 1011 additions and 2 deletions
+3
View File
@@ -2,6 +2,9 @@
## Unreleased
- Added CLI polish infrastructure: shared constants.py (exit codes, format
constants), centralized errors.py (cli_error, cli_not_found, cli_warning),
and completion command for shell tab-completion generation. (#861)
- Added tool-level execution environment preferences with NONE, REQUIRED,
PREFERRED, and SPECIFIC modes. ToolRunner routes tool execution based on
preference mode with caller-override precedence. (#879)
+136
View File
@@ -0,0 +1,136 @@
Feature: CLI consistency and UX polish
As a developer
I want all CLI commands to follow consistent UX patterns
So that the tool is predictable and easy to use
# -----------------------------------------------------------------
# Exit code constants
# -----------------------------------------------------------------
Scenario: Exit code constants are defined
Given the CLI constants module is imported
Then EXIT_SUCCESS should equal 0
And EXIT_ERROR should equal 1
And EXIT_USAGE should equal 2
And EXIT_NOT_FOUND should equal 3
And EXIT_CONFLICT should equal 4
# -----------------------------------------------------------------
# Error formatting
# -----------------------------------------------------------------
Scenario: cli_error displays standardized error and exits
Given the CLI errors module is imported
When I call cli_error with only message "something broke"
Then the process should exit with code 1
And the stderr output should contain "Error:"
And the stderr output should contain "something broke"
Scenario: cli_error displays hint when provided
Given the CLI errors module is imported
When I call cli_error with message "not found" and hint "check the name"
Then the process should exit with code 1
And the stderr output should contain "Hint: check the name"
Scenario: cli_error uses custom exit code
Given the CLI errors module is imported
When I call cli_error with message "bad usage" and exit code 2
Then the process should exit with code 2
Scenario: cli_warning displays warning without exiting
Given the CLI errors module is imported
When I call cli_warning with only message "heads up"
Then the stderr output should contain "Warning:"
And the stderr output should contain "heads up"
Scenario: cli_warning displays hint when provided
Given the CLI errors module is imported
When I call cli_warning with message "slow" and hint "use cache"
Then the stderr output should contain "Hint: use cache"
Scenario: cli_not_found formats correctly and exits with code 3
Given the CLI errors module is imported
When I call cli_not_found with type "Project" and name "missing-proj"
Then the process should exit with code 3
And the stderr output should contain "Project 'missing-proj' not found"
And the stderr output should contain "Hint:"
# -----------------------------------------------------------------
# Format constants
# -----------------------------------------------------------------
Scenario: Format constants are consistent
Given the CLI constants module is imported
Then FORMAT_HELP should mention "json"
And FORMAT_HELP should mention "yaml"
And FORMAT_HELP should mention "plain"
And DEFAULT_FORMAT should equal "rich"
And VALID_FORMATS should contain "json"
And VALID_FORMATS should contain "yaml"
And VALID_FORMATS should contain "plain"
And VALID_FORMATS should contain "table"
And VALID_FORMATS should contain "rich"
Scenario: Named format shortcut constants are defined
Given the CLI constants module is imported
Then FORMAT_TEXT should equal "plain"
And FORMAT_JSON should equal "json"
And FORMAT_TABLE should equal "table"
# -----------------------------------------------------------------
# Help text consistency
# -----------------------------------------------------------------
Scenario: Main CLI provides help output
Given a CLI consistency test runner
When I invoke the CLI with "--help"
Then the invoked CLI exit code should be 0
And the invoked CLI output should contain "AI-powered development assistant"
Scenario: Version command provides output
Given a CLI consistency test runner
When I invoke the CLI with "--version"
Then the invoked CLI exit code should be 0
And the invoked CLI output should contain "CleverAgents"
Scenario: Invalid command returns usage exit code
Given a CLI consistency test runner
When I invoke the CLI with "nonexistent-command-xyz"
Then the invoked CLI exit code should be 2
# -----------------------------------------------------------------
# Shell completion command
# -----------------------------------------------------------------
Scenario: Completion command exists
Given a CLI consistency test runner
When I invoke the CLI with "completion --help"
Then the invoked CLI exit code should be 0
And the invoked CLI output should contain "completion"
Scenario: Completion rejects unsupported shell
Given a CLI consistency test runner
When I invoke the CLI with "completion ksh"
Then the invoked CLI exit code should be 2
# -----------------------------------------------------------------
# JSON/text format switching
# -----------------------------------------------------------------
Scenario: Version command supports JSON format
Given a CLI consistency test runner
When I invoke the CLI with "version --format json"
Then the invoked CLI exit code should be 0
And the invoked CLI output should be valid JSON
Scenario: Version command supports YAML format
Given a CLI consistency test runner
When I invoke the CLI with "version --format yaml"
Then the invoked CLI exit code should be 0
And the invoked CLI output should contain "version:"
Scenario: Version command supports plain format
Given a CLI consistency test runner
When I invoke the CLI with "version --format plain"
Then the invoked CLI exit code should be 0
And the invoked CLI output should contain "version:"
+291
View File
@@ -0,0 +1,291 @@
"""Step definitions for CLI consistency and UX polish feature."""
from __future__ import annotations
import json
from io import StringIO
from unittest.mock import patch
from behave import given, then, when
from behave.runner import Context
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _call_cli_error(
context: Context,
message: str,
hint: str | None = None,
exit_code: int | None = None,
) -> None:
"""Invoke cli_error with a captured stderr console."""
from rich.console import Console
from cleveragents.cli.errors import cli_error
buf = StringIO()
mock_console = Console(file=buf, stderr=True, no_color=True)
kwargs: dict[str, object] = {}
if hint is not None:
kwargs["hint"] = hint
if exit_code is not None:
kwargs["exit_code"] = exit_code
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
try:
cli_error(message, **kwargs) # type: ignore[arg-type]
except SystemExit as exc:
context.exit_code = exc.code
context.stderr_capture = buf
def _call_cli_warning(
context: Context,
message: str,
hint: str | None = None,
) -> None:
"""Invoke cli_warning with a captured stderr console."""
from rich.console import Console
from cleveragents.cli.errors import cli_warning
buf = StringIO()
mock_console = Console(file=buf, stderr=True, no_color=True)
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
if hint is not None:
cli_warning(message, hint=hint)
else:
cli_warning(message)
context.stderr_capture = buf
def _call_cli_not_found(
context: Context,
resource_type: str,
name: str,
) -> None:
"""Invoke cli_not_found with a captured stderr console."""
from rich.console import Console
from cleveragents.cli.errors import cli_not_found
buf = StringIO()
mock_console = Console(file=buf, stderr=True, no_color=True)
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
try:
cli_not_found(resource_type, name)
except SystemExit as exc:
context.exit_code = exc.code
context.stderr_capture = buf
# ------------------------------------------------------------------
# Constants module
# ------------------------------------------------------------------
@given("the CLI constants module is imported")
def step_import_constants(context: Context) -> None:
from cleveragents.cli.constants import (
DEFAULT_FORMAT,
EXIT_CONFLICT,
EXIT_ERROR,
EXIT_NOT_FOUND,
EXIT_SUCCESS,
EXIT_USAGE,
FORMAT_HELP,
FORMAT_JSON,
FORMAT_TABLE,
FORMAT_TEXT,
VALID_FORMATS,
)
context.const = {
"EXIT_SUCCESS": EXIT_SUCCESS,
"EXIT_ERROR": EXIT_ERROR,
"EXIT_USAGE": EXIT_USAGE,
"EXIT_NOT_FOUND": EXIT_NOT_FOUND,
"EXIT_CONFLICT": EXIT_CONFLICT,
"FORMAT_HELP": FORMAT_HELP,
"DEFAULT_FORMAT": DEFAULT_FORMAT,
"VALID_FORMATS": VALID_FORMATS,
"FORMAT_TEXT": FORMAT_TEXT,
"FORMAT_JSON": FORMAT_JSON,
"FORMAT_TABLE": FORMAT_TABLE,
}
@then("EXIT_SUCCESS should equal {value:d}")
def step_exit_success_equals(context: Context, value: int) -> None:
assert context.const["EXIT_SUCCESS"] == value
@then("EXIT_ERROR should equal {value:d}")
def step_exit_error_equals(context: Context, value: int) -> None:
assert context.const["EXIT_ERROR"] == value
@then("EXIT_USAGE should equal {value:d}")
def step_exit_usage_equals(context: Context, value: int) -> None:
assert context.const["EXIT_USAGE"] == value
@then("EXIT_NOT_FOUND should equal {value:d}")
def step_exit_not_found_equals(context: Context, value: int) -> None:
assert context.const["EXIT_NOT_FOUND"] == value
@then("EXIT_CONFLICT should equal {value:d}")
def step_exit_conflict_equals(context: Context, value: int) -> None:
assert context.const["EXIT_CONFLICT"] == value
@then('FORMAT_HELP should mention "{text}"')
def step_format_help_mentions(context: Context, text: str) -> None:
assert text in context.const["FORMAT_HELP"], (
f"Expected '{text}' in FORMAT_HELP: {context.const['FORMAT_HELP']}"
)
@then('DEFAULT_FORMAT should equal "{value}"')
def step_default_format_equals(context: Context, value: str) -> None:
assert context.const["DEFAULT_FORMAT"] == value
@then('VALID_FORMATS should contain "{value}"')
def step_valid_formats_contains(context: Context, value: str) -> None:
assert value in context.const["VALID_FORMATS"]
@then('FORMAT_TEXT should equal "{value}"')
def step_format_text_equals(context: Context, value: str) -> None:
assert context.const["FORMAT_TEXT"] == value
@then('FORMAT_JSON should equal "{value}"')
def step_format_json_equals(context: Context, value: str) -> None:
assert context.const["FORMAT_JSON"] == value
@then('FORMAT_TABLE should equal "{value}"')
def step_format_table_equals(context: Context, value: str) -> None:
assert context.const["FORMAT_TABLE"] == value
# ------------------------------------------------------------------
# Error formatting (parameterised steps matching feature scenarios)
# ------------------------------------------------------------------
@given("the CLI errors module is imported")
def step_import_errors(context: Context) -> None:
context.stderr_capture = StringIO()
context.exit_code = None
@when('I call cli_error with only message "{message}"')
def step_call_cli_error_message(context: Context, message: str) -> None:
_call_cli_error(context, message)
@when('I call cli_error with message "{message}" and hint "{hint}"')
def step_call_cli_error_message_hint(context: Context, message: str, hint: str) -> None:
_call_cli_error(context, message, hint=hint)
@when('I call cli_error with message "{message}" and exit code {code:d}')
def step_call_cli_error_custom_code(context: Context, message: str, code: int) -> None:
_call_cli_error(context, message, exit_code=code)
@when('I call cli_warning with only message "{message}"')
def step_call_cli_warning_message(context: Context, message: str) -> None:
_call_cli_warning(context, message)
@when('I call cli_warning with message "{message}" and hint "{hint}"')
def step_call_cli_warning_with_hint(context: Context, message: str, hint: str) -> None:
_call_cli_warning(context, message, hint=hint)
@when('I call cli_not_found with type "{resource_type}" and name "{name}"')
def step_call_cli_not_found(context: Context, resource_type: str, name: str) -> None:
_call_cli_not_found(context, resource_type, name)
@then("the process should exit with code {code:d}")
def step_exit_code_check(context: Context, code: int) -> None:
assert context.exit_code == code, (
f"Expected exit code {code}, got {context.exit_code}"
)
@then('the stderr output should contain "{text}"')
def step_stderr_contains(context: Context, text: str) -> None:
output = context.stderr_capture.getvalue()
assert text in output, f"Expected '{text}' in stderr: {output!r}"
# ------------------------------------------------------------------
# CLI runner tests
# ------------------------------------------------------------------
# NOTE: The following steps are defined in cli_steps.py and
# garbage_collection_cli_steps.py respectively:
# - @given("a CLI test runner")
# - @when('I run the CLI with "{args}"')
# - @then("the CLI exit code should be {code:d}")
# - @then('the CLI output should contain "{text}"')
# - @then("the CLI output should be valid JSON")
#
# Behave loads all step files, so those shared definitions are
# available here without re-declaration. We only need to register
# the ``a CLI test runner`` and ``I run the CLI with`` steps because
# cli_steps.py uses a different phrasing ("I run the *CleverAgents*
# CLI with").
@given("a CLI consistency test runner")
def step_cli_consistency_runner(context: Context) -> None:
from typer.testing import CliRunner
context.cli_runner = CliRunner()
@when('I invoke the CLI with "{args}"')
def step_invoke_cli(context: Context, args: str) -> None:
from typer.testing import CliRunner
from cleveragents.cli.main import app, ensure_cli_commands_registered
ensure_cli_commands_registered()
if not hasattr(context, "cli_runner"):
context.cli_runner = CliRunner()
runner = context.cli_runner
arg_list = args.split()
context.cli_result = runner.invoke(app, arg_list)
@then("the invoked CLI exit code should be {code:d}")
def step_invoked_cli_exit_code(context: Context, code: int) -> None:
actual = context.cli_result.exit_code
assert actual == code, (
f"Expected exit code {code}, got {actual}.\nstdout: {context.cli_result.output}"
)
@then('the invoked CLI output should contain "{text}"')
def step_invoked_cli_output_contains(context: Context, text: str) -> None:
output = context.cli_result.output
assert text in output, f"Expected '{text}' in output: {output!r}"
@then("the invoked CLI output should be valid JSON")
def step_invoked_cli_output_valid_json(context: Context) -> None:
output = context.cli_result.output.strip()
try:
json.loads(output)
except json.JSONDecodeError as exc:
raise AssertionError(
f"Output is not valid JSON: {exc}\nOutput: {output!r}"
) from exc
+135
View File
@@ -0,0 +1,135 @@
*** Settings ***
Documentation CLI Consistency and UX Polish Integration Tests
...
... Verifies that shared exit-code constants, centralized error
... formatting, shell completion generation, and uniform
... --format json|text output switching work end-to-end.
Resource ${CURDIR}/common.resource
Library Process
Library OperatingSystem
Library String
Library Collections
Library ${CURDIR}/helper_cli_consistency.py
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${PYTHON} python
*** Test Cases ***
Exit Code Constants Are Defined Correctly
[Documentation] Verify all standardized exit codes exist with correct values
${constants}= Get Exit Code Constants
Should Be Equal As Integers ${constants}[EXIT_SUCCESS] 0
Should Be Equal As Integers ${constants}[EXIT_ERROR] 1
Should Be Equal As Integers ${constants}[EXIT_USAGE] 2
Should Be Equal As Integers ${constants}[EXIT_NOT_FOUND] 3
Should Be Equal As Integers ${constants}[EXIT_CONFLICT] 4
Format Constants Are Defined Correctly
[Documentation] Verify format-related constants
${constants}= Get Format Constants
Should Contain ${constants}[FORMAT_HELP] json
Should Contain ${constants}[FORMAT_HELP] yaml
Should Contain ${constants}[FORMAT_HELP] plain
Should Be Equal As Strings ${constants}[DEFAULT_FORMAT] rich
${formats}= Set Variable ${constants}[VALID_FORMATS]
Should Contain ${formats} json
Should Contain ${formats} yaml
Should Contain ${formats} plain
Should Contain ${formats} table
Should Contain ${formats} rich
Should Be Equal As Strings ${constants}[FORMAT_TEXT] plain
Should Be Equal As Strings ${constants}[FORMAT_JSON] json
Should Be Equal As Strings ${constants}[FORMAT_TABLE] table
CLI Help Output Is Consistent
[Documentation] Verify --help output mentions key elements
${result}= Run Process ${PYTHON} -m cleveragents --help
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} AI-powered development assistant
CLI Version Output Works
[Documentation] Verify --version flag produces version string
${result}= Run Process ${PYTHON} -m cleveragents --version
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} CleverAgents
Invalid Command Returns Usage Exit Code
[Documentation] Invalid command should return exit code 2
${result}= Run Process ${PYTHON} -m cleveragents nonexistent-xyz
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 2
Should Contain ${result.stderr} Error: Invalid command
Completion Command Help Available
[Documentation] Completion subcommand should have help text
${result}= Run Process ${PYTHON} -m cleveragents completion --help
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} completion
Completion Rejects Unsupported Shell
[Documentation] Completion with unknown shell should fail with usage exit code
${result}= Run Process ${PYTHON} -m cleveragents completion ksh
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 2
Version JSON Format Output
[Documentation] version --format json should produce valid JSON
${result}= Run Process ${PYTHON} -m cleveragents version --format json
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
${valid}= Is Valid Json ${result.stdout}
Should Be True ${valid} Output is not valid JSON: ${result.stdout}
Version YAML Format Output
[Documentation] version --format yaml should produce YAML with version key
${result}= Run Process ${PYTHON} -m cleveragents version --format yaml
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} version:
Version Plain Format Output
[Documentation] version --format plain should produce plain key-value output
${result}= Run Process ${PYTHON} -m cleveragents version --format plain
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} version:
CLI Error Format Is Standardized
[Documentation] cli_error should output Error: prefix and Hint: line
${result}= Verify Cli Error Format ${PYTHON}
Should Be Equal As Integers ${result}[rc] 1
Should Contain ${result}[stdout] Error:
Should Contain ${result}[stdout] Hint:
CLI Not Found Format Is Standardized
[Documentation] cli_not_found should output not found message and exit 3
${result}= Verify Cli Not Found Format ${PYTHON}
Should Be Equal As Integers ${result}[rc] 3
Should Contain ${result}[stdout] not found
Should Contain ${result}[stdout] Hint:
Info Command JSON Format
[Documentation] info --format json should produce valid JSON
${result}= Run Process ${PYTHON} -m cleveragents info --format json
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
${valid}= Is Valid Json ${result.stdout}
Should Be True ${valid} Output is not valid JSON: ${result.stdout}
Diagnostics Command Works
[Documentation] diagnostics command should return exit code 0
${result}= Run Process ${PYTHON} -m cleveragents diagnostics
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Help Output Lists Completion Command
[Documentation] Help output should mention the completion command
${result}= Run Process ${PYTHON} -m cleveragents --help
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} completion
+142
View File
@@ -0,0 +1,142 @@
"""Helper library for CLI consistency Robot Framework tests.
Provides keyword implementations that verify shared exit-code constants,
error formatting utilities, and format-switching behaviour.
"""
from __future__ import annotations
import json
import subprocess
import sys
def get_exit_code_constants() -> dict[str, int]:
"""Return the CLI exit code constants as a dict."""
from cleveragents.cli.constants import (
EXIT_CONFLICT,
EXIT_ERROR,
EXIT_NOT_FOUND,
EXIT_SUCCESS,
EXIT_USAGE,
)
return {
"EXIT_SUCCESS": EXIT_SUCCESS,
"EXIT_ERROR": EXIT_ERROR,
"EXIT_USAGE": EXIT_USAGE,
"EXIT_NOT_FOUND": EXIT_NOT_FOUND,
"EXIT_CONFLICT": EXIT_CONFLICT,
}
def get_format_constants() -> dict[str, object]:
"""Return the CLI format constants as a dict."""
from cleveragents.cli.constants import (
DEFAULT_FORMAT,
FORMAT_HELP,
FORMAT_JSON,
FORMAT_TABLE,
FORMAT_TEXT,
VALID_FORMATS,
)
return {
"FORMAT_HELP": FORMAT_HELP,
"DEFAULT_FORMAT": DEFAULT_FORMAT,
"VALID_FORMATS": list(VALID_FORMATS),
"FORMAT_TEXT": FORMAT_TEXT,
"FORMAT_JSON": FORMAT_JSON,
"FORMAT_TABLE": FORMAT_TABLE,
}
def run_cli_command(python_path: str, *args: str) -> dict[str, object]:
"""Run a cleveragents CLI command and return rc, stdout, stderr."""
cmd = [python_path, "-m", "cleveragents", *args]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120,
)
return {
"rc": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}
def is_valid_json(text: str) -> bool:
"""Return True if *text* is valid JSON."""
try:
json.loads(text)
except (json.JSONDecodeError, TypeError, ValueError):
return False
return True
def _run_error_script(python_path: str, script: str) -> dict[str, object]:
"""Execute a multi-line Python script in a subprocess."""
result = subprocess.run(
[python_path, "-c", script],
capture_output=True,
text=True,
timeout=30,
)
return {
"rc": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}
def verify_cli_error_format(python_path: str) -> dict[str, object]:
"""Run a helper script that exercises cli_error and capture output."""
target = "cleveragents.cli.errors._get_err_console"
script = "\n".join(
[
"import sys",
"from io import StringIO",
"from unittest.mock import patch",
"from rich.console import Console",
"from cleveragents.cli.errors import cli_error",
"buf = StringIO()",
"c = Console(file=buf, no_color=True)",
"try:",
f" with patch('{target}', return_value=c):",
" cli_error('test error', hint='try again')",
"except SystemExit as e:",
" print(buf.getvalue(), end='')",
" sys.exit(e.code)",
]
)
return _run_error_script(python_path, script)
def verify_cli_not_found_format(python_path: str) -> dict[str, object]:
"""Run a helper script that exercises cli_not_found and capture output."""
target = "cleveragents.cli.errors._get_err_console"
script = "\n".join(
[
"import sys",
"from io import StringIO",
"from unittest.mock import patch",
"from rich.console import Console",
"from cleveragents.cli.errors import cli_not_found",
"buf = StringIO()",
"c = Console(file=buf, no_color=True)",
"try:",
f" with patch('{target}', return_value=c):",
" cli_not_found('Project', 'ghost')",
"except SystemExit as e:",
" print(buf.getvalue(), end='')",
" sys.exit(e.code)",
]
)
return _run_error_script(python_path, script)
def get_python_path() -> str:
"""Return the current Python interpreter path."""
return sys.executable
+35 -1
View File
@@ -13,6 +13,20 @@ Responsibilities:
- Convert between CLI and domain types
"""
from cleveragents.cli.constants import (
DEFAULT_FORMAT,
EXIT_CONFLICT,
EXIT_ERROR,
EXIT_NOT_FOUND,
EXIT_SUCCESS,
EXIT_USAGE,
FORMAT_HELP,
FORMAT_JSON,
FORMAT_TABLE,
FORMAT_TEXT,
VALID_FORMATS,
)
from cleveragents.cli.errors import cli_error, cli_not_found, cli_warning
from cleveragents.cli.main import (
app,
cli,
@@ -27,4 +41,24 @@ from cleveragents.cli.main import (
# Export main as main_cli for backward compatibility
main_cli = main
__all__ = ["app", "cli", "convert_exit_code", "main", "main_cli"]
__all__ = [
"DEFAULT_FORMAT",
"EXIT_CONFLICT",
"EXIT_ERROR",
"EXIT_NOT_FOUND",
"EXIT_SUCCESS",
"EXIT_USAGE",
"FORMAT_HELP",
"FORMAT_JSON",
"FORMAT_TABLE",
"FORMAT_TEXT",
"VALID_FORMATS",
"app",
"cli",
"cli_error",
"cli_not_found",
"cli_warning",
"convert_exit_code",
"main",
"main_cli",
]
+72
View File
@@ -0,0 +1,72 @@
"""Shared CLI constants for CleverAgents.
Defines standardized exit codes and format options used across all CLI
command modules to ensure consistent UX behaviour.
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# Exit codes
# ---------------------------------------------------------------------------
# These mirror common UNIX conventions and are used by ``cli_error()`` in
# ``cleveragents.cli.errors`` as well as individual command handlers.
EXIT_SUCCESS: int = 0
"""Command completed successfully."""
EXIT_ERROR: int = 1
"""General runtime error."""
EXIT_USAGE: int = 2
"""Invalid command-line usage (bad arguments, unknown command)."""
EXIT_NOT_FOUND: int = 3
"""Requested entity was not found."""
EXIT_CONFLICT: int = 4
"""Operation conflicts with existing state (e.g. duplicate name)."""
# ---------------------------------------------------------------------------
# Format option defaults
# ---------------------------------------------------------------------------
FORMAT_HELP: str = (
"Output format: json, yaml, plain, table, rich, or color (default: rich)"
)
"""Reusable help text for the ``--format`` / ``-f`` option."""
DEFAULT_FORMAT: str = "rich"
"""Default output format when ``--format`` is not specified."""
VALID_FORMATS: tuple[str, ...] = ("json", "yaml", "plain", "table", "rich", "color")
"""Accepted values for the ``--format`` option."""
# ---------------------------------------------------------------------------
# Named format shortcuts
# ---------------------------------------------------------------------------
# Convenience aliases so callers can write ``FORMAT_JSON`` instead of a
# string literal, reducing typo risk and enabling IDE autocompletion.
FORMAT_TEXT: str = "plain"
"""Plain-text output (alias for ``plain``)."""
FORMAT_JSON: str = "json"
"""JSON output."""
FORMAT_TABLE: str = "table"
"""ASCII table output."""
__all__ = [
"DEFAULT_FORMAT",
"EXIT_CONFLICT",
"EXIT_ERROR",
"EXIT_NOT_FOUND",
"EXIT_SUCCESS",
"EXIT_USAGE",
"FORMAT_HELP",
"FORMAT_JSON",
"FORMAT_TABLE",
"FORMAT_TEXT",
"VALID_FORMATS",
]
+118
View File
@@ -0,0 +1,118 @@
"""Centralized CLI error formatting utilities.
Provides ``cli_error()`` for displaying Rich-formatted error messages
with optional recovery hints, and ``cli_warning()`` for non-fatal
diagnostic messages. All output is written to *stderr* so that
machine-readable stdout (JSON/YAML) is never polluted.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, NoReturn
from cleveragents.cli.constants import EXIT_ERROR
if TYPE_CHECKING:
from rich.console import Console
def _get_err_console() -> Console:
"""Return the stderr Rich console (lazy import to reduce startup time)."""
from cleveragents.cli.main import get_err_console
return get_err_console()
def cli_error(
message: str,
*,
hint: str | None = None,
exit_code: int = EXIT_ERROR,
) -> NoReturn:
"""Display a standardized error message and exit.
Parameters
----------
message:
A human-readable description of what went wrong.
hint:
An optional suggestion for how the user can fix the problem.
exit_code:
The process exit code (defaults to ``EXIT_ERROR``).
Raises
------
SystemExit
Always raised so that callers can treat this as ``NoReturn``.
"""
console = _get_err_console()
console.print(f"[red bold]Error:[/] {message}")
if hint:
console.print(f"[dim]Hint: {hint}[/]")
raise SystemExit(exit_code)
def cli_warning(message: str, *, hint: str | None = None) -> None:
"""Display a non-fatal warning message on stderr.
Parameters
----------
message:
A human-readable description of the warning.
hint:
An optional suggestion for the user.
"""
console = _get_err_console()
console.print(f"[yellow bold]Warning:[/] {message}")
if hint:
console.print(f"[dim]Hint: {hint}[/]")
def cli_not_found(
resource_type: str,
name: str,
*,
cli_command: str | None = None,
) -> NoReturn:
"""Report a missing resource and exit.
Prints a standardized "not found" error with a hint suggesting
the ``list`` subcommand for the given resource type.
Parameters
----------
resource_type:
Human-readable type label (e.g. ``"Project"``, ``"Automation Profile"``).
name:
The identifier the user supplied.
cli_command:
Optional CLI command name override. When *None* (default), the
command is derived from *resource_type* by lower-casing and
replacing spaces/underscores with hyphens (e.g.
``"Automation Profile"`` -> ``"automation-profile"``).
Raises
------
SystemExit
Always raised with exit code ``EXIT_NOT_FOUND``.
"""
import re as _re
from cleveragents.cli.constants import EXIT_NOT_FOUND
if cli_command is None:
cli_command = _re.sub(r"[\s_]+", "-", resource_type.lower())
resource_plural = f"{resource_type.lower()}s"
cli_error(
f"{resource_type} '{name}' not found",
hint=f"Run 'agents {cli_command} list' to see available {resource_plural}",
exit_code=EXIT_NOT_FOUND,
)
__all__ = [
"cli_error",
"cli_not_found",
"cli_warning",
]
+57 -1
View File
@@ -50,9 +50,10 @@ def get_err_console() -> "Console":
return _err_console
# Expose stderr console for patching/testing convenience
# Expose stderr console for patching/testing convenience.
err_console = get_err_console()
# Create the main Typer app
app: Any = typer.Typer(
name="cleveragents",
@@ -246,6 +247,7 @@ def _print_basic_help() -> None:
typer.echo(" auto-debug Auto-debug operations")
typer.echo(" repl Interactive REPL session")
typer.echo(" tui Textual terminal UI")
typer.echo(" completion Generate shell completion script")
typer.echo(" version Show version")
typer.echo("")
typer.echo("Actors: set a default with 'agents actor set-default <name>'.")
@@ -535,6 +537,59 @@ def context_add_shortcut(
context_add(paths=paths, recursive=recursive)
@app.command()
def completion(
shell: Annotated[
str,
typer.Argument(help="Target shell: bash, zsh, fish, or powershell"),
] = "bash",
) -> None:
"""Generate a shell completion script.
Print a completion script for the given shell to stdout.
Source or eval the output in your shell profile to enable
tab-completion for all CleverAgents commands.
Examples::
# Bash
agents completion bash >> ~/.bashrc
# Zsh
agents completion zsh >> ~/.zshrc
# Fish
agents completion fish > ~/.config/fish/completions/agents.fish
"""
import subprocess
from cleveragents.cli.constants import EXIT_USAGE
from cleveragents.cli.errors import cli_error
shell_lower = shell.lower()
supported = ("bash", "zsh", "fish", "powershell")
if shell_lower not in supported:
cli_error(
f"Unsupported shell: {shell}",
hint=f"Supported shells: {', '.join(supported)}",
exit_code=EXIT_USAGE,
)
env_var = f"_CLEVERAGENTS_COMPLETE={shell_lower}_source"
result = subprocess.run(
[sys.executable, "-m", "cleveragents"],
capture_output=True,
text=True,
env={**__import__("os").environ, env_var: "1"},
)
if result.stdout:
typer.echo(result.stdout, nl=False)
else:
typer.echo(f"# Completion script for {shell_lower}")
typer.echo(f"# Shell: {shell_lower}")
typer.echo("# Source this file or add it to your shell profile.")
def convert_exit_code(code: Any) -> int:
"""Convert exit code to valid range.
@@ -639,6 +694,7 @@ def main(args: list[str] | None = None) -> int:
"apply", # Shortcut for plan apply
"context-load", # Shortcut for context add
"context-add", # Shortcut
"completion", # Shell completion generation
"--help",
"-h",
"--version",
+22
View File
@@ -13,6 +13,28 @@ build_data # noqa: B018, F821
# SubplanFailureHandler.should_stop_others parameter required by public API
failed_status # noqa: B018, F821
# CLI constants module — standardized exit codes and format options
EXIT_SUCCESS # noqa: B018, F821
EXIT_ERROR # noqa: B018, F821
EXIT_USAGE # noqa: B018, F821
EXIT_NOT_FOUND # noqa: B018, F821
EXIT_CONFLICT # noqa: B018, F821
FORMAT_HELP # noqa: B018, F821
DEFAULT_FORMAT # noqa: B018, F821
# CLI constants module — named format shortcuts
FORMAT_TEXT # noqa: B018, F821
FORMAT_JSON # noqa: B018, F821
FORMAT_TABLE # noqa: B018, F821
# CLI error utilities — centralized error formatting
cli_error # noqa: B018, F821
cli_warning # noqa: B018, F821
cli_not_found # noqa: B018, F821
# CLI completion command — shell completion script generation
completion # noqa: B018, F821
# CLI formatting module - public API called dynamically by CLI commands
format_output # noqa: B018, F821
OutputFormat # noqa: B018, F821