feat(cli): enhance version/info/diagnostics with --format and --check

Add structured data builders and Rich renderers for version, info, and
diagnostics commands. Support --format rich/plain/json/yaml output parity
and --check flag for diagnostics (exits non-zero on errors).

Includes Behave scenarios, Robot smoke tests, ASV benchmarks, and updates
existing tests to match the new Rich panel output format.
This commit is contained in:
khyari hamza
2026-02-17 13:13:13 +00:00
parent 1c2fe1b5f8
commit 02c75fb4bb
9 changed files with 1141 additions and 68 deletions
+95
View File
@@ -0,0 +1,95 @@
"""ASV benchmarks for enhanced CLI core system commands.
Measures in-process execution time for version, info, and diagnostics
commands across all supported output formats (rich, plain, json, yaml).
"""
from __future__ import annotations
import sys
from pathlib import Path
try:
from cleveragents.cli.commands.system import (
build_diagnostics_data,
build_info_data,
build_version_data,
)
from cleveragents.cli.formatting import format_output
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cleveragents.cli.commands.system import (
build_diagnostics_data,
build_info_data,
build_version_data,
)
from cleveragents.cli.formatting import format_output
class VersionSuite:
"""Benchmark version data assembly and formatting."""
def time_build_version_data(self) -> None:
"""Measure build_version_data() latency."""
build_version_data()
def time_version_json(self) -> None:
"""Measure version + JSON format."""
data = build_version_data()
format_output(data, "json")
def time_version_yaml(self) -> None:
"""Measure version + YAML format."""
data = build_version_data()
format_output(data, "yaml")
def time_version_plain(self) -> None:
"""Measure version + plain format."""
data = build_version_data()
format_output(data, "plain")
class InfoSuite:
"""Benchmark info data assembly and formatting."""
def time_build_info_data(self) -> None:
"""Measure build_info_data() latency."""
build_info_data()
def time_info_json(self) -> None:
"""Measure info + JSON format."""
data = build_info_data()
format_output(data, "json")
def time_info_yaml(self) -> None:
"""Measure info + YAML format."""
data = build_info_data()
format_output(data, "yaml")
def time_info_plain(self) -> None:
"""Measure info + plain format."""
data = build_info_data()
format_output(data, "plain")
class DiagnosticsSuite:
"""Benchmark diagnostics data assembly and formatting."""
def time_build_diagnostics_data(self) -> None:
"""Measure build_diagnostics_data() latency."""
build_diagnostics_data()
def time_diagnostics_json(self) -> None:
"""Measure diagnostics + JSON format."""
data = build_diagnostics_data()
format_output(data, "json")
def time_diagnostics_yaml(self) -> None:
"""Measure diagnostics + YAML format."""
data = build_diagnostics_data()
format_output(data, "yaml")
def time_diagnostics_plain(self) -> None:
"""Measure diagnostics + plain format."""
data = build_diagnostics_data()
format_output(data, "plain")
+4 -6
View File
@@ -10,15 +10,13 @@ Feature: CleverAgents CLI metadata
Scenario: Display diagnostics
When I run the CleverAgents CLI with "diagnostics"
Then the output should contain "CleverAgents Diagnostics"
And the output should contain "Version:"
And the output should contain "Python:"
And the output should contain "Platform:"
Then the output should contain "Checks"
And the output should contain "Summary"
Scenario: Display info
When I run the CleverAgents CLI with "info"
Then the output should contain "CleverAgents Information"
And the output should contain "Version: 1.0.0"
Then the output should contain "Environment"
And the output should contain "Runtime"
Scenario Outline: Display help for command groups
When I run the CleverAgents CLI with "<command> --help"
+124
View File
@@ -0,0 +1,124 @@
Feature: Core system commands (version, info, diagnostics)
As a user of CleverAgents
I want version, info, and diagnostics commands
So that I can inspect the system state and troubleshoot issues
# -- version command --
Scenario: Version command shows version string
When I run the system version command
Then the system version output should contain "1.0.0"
And the system version output should contain "CleverAgents"
Scenario: Version command with plain format
When I run the system version command with format "plain"
Then the system version output should contain "version: 1.0.0"
Scenario: Version command with json format
When I run the system version command with format "json"
Then the system version json output should have key "version" with value "1.0.0"
And the system version json output should have key "schema" with value "v3"
And the system version json output should have key "channel" with value "stable"
Scenario: Version command with yaml format
When I run the system version command with format "yaml"
Then the system version output should contain "version: 1.0.0"
And the system version output should contain "schema: v3"
Scenario: Version command includes python version
When I run the system version command with format "json"
Then the system version json output should have key "python"
Scenario: Version command includes build metadata
When I run the system version command with format "json"
Then the system version json output should have key "build_date"
And the system version json output should have key "commit"
And the system version json output should have key "platform"
Scenario: Version command includes dependencies
When I run the system version command with format "json"
Then the system version json output should have nested key "dependencies"
# -- info command --
Scenario: Info command shows environment details
When I run the system info command
Then the system info output should contain "Environment"
And the system info output should contain "Runtime"
Scenario: Info command with json format
When I run the system info command with format "json"
Then the system info json output should have key "version" with value "1.0.0"
And the system info json output should have key "data_dir"
And the system info json output should have key "database"
And the system info json output should have key "server_mode" with value "disabled"
Scenario: Info command with plain format
When I run the system info command with format "plain"
Then the system info output should contain "version: 1.0.0"
And the system info output should contain "server_mode: disabled"
Scenario: Info command shows provider count
When I run the system info command with format "json"
Then the system info json output should have key "providers_configured"
Scenario: Info command shows automation level
When I run the system info command with format "json"
Then the system info json output should have key "automation"
Scenario: Info command shows storage info
When I run the system info command with format "json"
Then the system info json output should have nested key "storage"
# -- diagnostics command --
Scenario: Diagnostics command runs checks
When I run the system diagnostics command
Then the system diagnostics output should contain "Checks"
And the system diagnostics output should contain "Summary"
Scenario: Diagnostics command with json format
When I run the system diagnostics command with format "json"
Then the system diagnostics json output should have key "checks"
And the system diagnostics json output should have key "summary"
And the system diagnostics json output should have key "recommendations"
Scenario: Diagnostics json summary has counts
When I run the system diagnostics command with format "json"
Then the system diagnostics json summary should have key "total"
And the system diagnostics json summary should have key "warnings"
And the system diagnostics json summary should have key "errors"
And the system diagnostics json summary should have key "duration_s"
Scenario: Diagnostics checks include config file
When I run the system diagnostics command with format "json"
Then the system diagnostics checks should include "Config file"
Scenario: Diagnostics checks include database
When I run the system diagnostics command with format "json"
Then the system diagnostics checks should include "Database"
Scenario: Diagnostics checks include git
When I run the system diagnostics command with format "json"
Then the system diagnostics checks should include "Git"
Scenario: Diagnostics checks include disk space
When I run the system diagnostics command with format "json"
Then the system diagnostics checks should include "Disk space"
Scenario: Diagnostics checks include file permissions
When I run the system diagnostics command with format "json"
Then the system diagnostics checks should include "File permissions"
Scenario: Diagnostics --check exits zero when no errors
When I run the system diagnostics command with check flag
Then the system diagnostics exit code should be 0
Scenario: Diagnostics with plain format
When I run the system diagnostics command with format "plain"
Then the system diagnostics output should contain "checks:"
And the system diagnostics output should contain "summary:"
Scenario: Diagnostics recommendations are a list
When I run the system diagnostics command with format "json"
Then the system diagnostics json recommendations should be a list
+237
View File
@@ -0,0 +1,237 @@
"""Step definitions for core system commands (version, info, diagnostics).
Covers features/cli_core.feature — the enhanced CLI0.core commands that
support ``--format`` (rich/plain/json/yaml) and ``--check`` for diagnostics.
"""
from __future__ import annotations
from typing import Any
from behave import then, when
from behave.runner import Context
from cleveragents.cli.commands.system import (
build_diagnostics_data,
build_info_data,
build_version_data,
)
from cleveragents.cli.formatting import format_output
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _format_data(data: dict[str, Any], fmt: str) -> str:
"""Format *data* and return the rendered string."""
if fmt == "rich":
from io import StringIO
from unittest.mock import patch
from rich.console import Console
buf = StringIO()
console = Console(file=buf, width=200, no_color=True)
with patch("cleveragents.cli.main.get_console", return_value=console):
if data.get("checks") is not None:
from cleveragents.cli.commands.system import render_diagnostics_rich
render_diagnostics_rich(data)
elif "data_dir" in data:
from cleveragents.cli.commands.system import render_info_rich
render_info_rich(data)
else:
from cleveragents.cli.commands.system import render_version_rich
render_version_rich(data)
return buf.getvalue()
return format_output(data, fmt)
# ---------------------------------------------------------------------------
# VERSION — When steps
# ---------------------------------------------------------------------------
@when("I run the system version command")
def step_run_system_version(context: Context) -> None:
"""Run version with default (rich) format."""
data = build_version_data()
context.sys_version_data = data
context.sys_version_output = _format_data(data, "rich")
@when('I run the system version command with format "{fmt}"')
def step_run_system_version_fmt(context: Context, fmt: str) -> None:
"""Run version with a specific output format."""
data = build_version_data()
context.sys_version_data = data
context.sys_version_output = _format_data(data, fmt)
# ---------------------------------------------------------------------------
# VERSION — Then steps
# ---------------------------------------------------------------------------
@then('the system version output should contain "{text}"')
def step_system_version_output_contains(context: Context, text: str) -> None:
output = context.sys_version_output
assert text in output, f"Expected '{text}' in version output:\n{output}"
@then('the system version json output should have key "{key}" with value "{value}"')
def step_system_version_json_key_value(context: Context, key: str, value: str) -> None:
data = context.sys_version_data
assert key in data, f"Key '{key}' not in version data: {list(data.keys())}"
assert str(data[key]) == value, f"Expected {key}={value!r}, got {data[key]!r}"
@then('the system version json output should have key "{key}"')
def step_system_version_json_key(context: Context, key: str) -> None:
data = context.sys_version_data
assert key in data, f"Key '{key}' not in version data: {list(data.keys())}"
@then('the system version json output should have nested key "{key}"')
def step_system_version_json_nested_key(context: Context, key: str) -> None:
data = context.sys_version_data
assert key in data, f"Key '{key}' not in version data: {list(data.keys())}"
assert isinstance(data[key], dict), (
f"Expected '{key}' to be a dict, got {type(data[key])}"
)
# ---------------------------------------------------------------------------
# INFO — When steps
# ---------------------------------------------------------------------------
@when("I run the system info command")
def step_run_system_info(context: Context) -> None:
"""Run info with default (rich) format."""
data = build_info_data()
context.sys_info_data = data
context.sys_info_output = _format_data(data, "rich")
@when('I run the system info command with format "{fmt}"')
def step_run_system_info_fmt(context: Context, fmt: str) -> None:
"""Run info with a specific output format."""
data = build_info_data()
context.sys_info_data = data
context.sys_info_output = _format_data(data, fmt)
# ---------------------------------------------------------------------------
# INFO — Then steps
# ---------------------------------------------------------------------------
@then('the system info output should contain "{text}"')
def step_system_info_output_contains(context: Context, text: str) -> None:
output = context.sys_info_output
assert text in output, f"Expected '{text}' in info output:\n{output}"
@then('the system info json output should have key "{key}" with value "{value}"')
def step_system_info_json_key_value(context: Context, key: str, value: str) -> None:
data = context.sys_info_data
assert key in data, f"Key '{key}' not in info data: {list(data.keys())}"
assert str(data[key]) == value, f"Expected {key}={value!r}, got {data[key]!r}"
@then('the system info json output should have key "{key}"')
def step_system_info_json_key(context: Context, key: str) -> None:
data = context.sys_info_data
assert key in data, f"Key '{key}' not in info data: {list(data.keys())}"
@then('the system info json output should have nested key "{key}"')
def step_system_info_json_nested_key(context: Context, key: str) -> None:
data = context.sys_info_data
assert key in data, f"Key '{key}' not in info data: {list(data.keys())}"
assert isinstance(data[key], dict), (
f"Expected '{key}' to be a dict, got {type(data[key])}"
)
# ---------------------------------------------------------------------------
# DIAGNOSTICS — When steps
# ---------------------------------------------------------------------------
@when("I run the system diagnostics command")
def step_run_system_diagnostics(context: Context) -> None:
"""Run diagnostics with default (rich) format."""
data = build_diagnostics_data()
context.sys_diag_data = data
context.sys_diag_output = _format_data(data, "rich")
@when('I run the system diagnostics command with format "{fmt}"')
def step_run_system_diagnostics_fmt(context: Context, fmt: str) -> None:
"""Run diagnostics with a specific output format."""
data = build_diagnostics_data()
context.sys_diag_data = data
context.sys_diag_output = _format_data(data, fmt)
@when("I run the system diagnostics command with check flag")
def step_run_system_diagnostics_check(context: Context) -> None:
"""Run diagnostics with --check flag."""
data = build_diagnostics_data()
context.sys_diag_data = data
context.sys_diag_output = _format_data(data, "rich")
# Compute exit code: 1 if errors, 0 otherwise
context.sys_diag_exit_code = 1 if data["has_errors"] else 0
# ---------------------------------------------------------------------------
# DIAGNOSTICS — Then steps
# ---------------------------------------------------------------------------
@then('the system diagnostics output should contain "{text}"')
def step_system_diag_output_contains(context: Context, text: str) -> None:
output = context.sys_diag_output
assert text in output, f"Expected '{text}' in diagnostics output:\n{output}"
@then('the system diagnostics json output should have key "{key}"')
def step_system_diag_json_key(context: Context, key: str) -> None:
data = context.sys_diag_data
assert key in data, f"Key '{key}' not in diagnostics data: {list(data.keys())}"
@then('the system diagnostics json summary should have key "{key}"')
def step_system_diag_json_summary_key(context: Context, key: str) -> None:
summary = context.sys_diag_data.get("summary", {})
assert key in summary, (
f"Key '{key}' not in diagnostics summary: {list(summary.keys())}"
)
@then('the system diagnostics checks should include "{name}"')
def step_system_diag_checks_include(context: Context, name: str) -> None:
checks = context.sys_diag_data.get("checks", [])
names = [c["name"] for c in checks]
assert name in names, f"Check '{name}' not in diagnostics checks: {names}"
@then("the system diagnostics exit code should be {code:d}")
def step_system_diag_exit_code(context: Context, code: int) -> None:
actual = getattr(context, "sys_diag_exit_code", 0)
assert actual == code, f"Expected exit code {code}, got {actual}"
@then("the system diagnostics json recommendations should be a list")
def step_system_diag_recommendations_list(context: Context) -> None:
recs = context.sys_diag_data.get("recommendations")
assert isinstance(recs, list), (
f"Expected recommendations to be a list, got {type(recs)}"
)
+2 -6
View File
@@ -184,15 +184,11 @@ def step_test_cli_commands(context):
# Test info
result = runner.invoke(app, ["info"])
context.results.append(
(result.exit_code, "CleverAgents Information" in result.stdout)
)
context.results.append((result.exit_code, "Environment" in result.stdout))
# Test diagnostics
result = runner.invoke(app, ["diagnostics"])
context.results.append(
(result.exit_code, "CleverAgents Diagnostics" in result.stdout)
)
context.results.append((result.exit_code, "Checks" in result.stdout))
# Test init - use temp directory to avoid conflicts
import os
+4 -6
View File
@@ -46,17 +46,15 @@ CLI Diagnostics Command
[Documentation] Test diagnostics command functionality
${result}= Run Process ${PYTHON} -m cleveragents diagnostics
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} CleverAgents Diagnostics
Should Contain ${result.stdout} Version:
Should Contain ${result.stdout} Python:
Should Contain ${result.stdout} Platform:
Should Contain ${result.stdout} Checks
Should Contain ${result.stdout} Summary
CLI Info Command
[Documentation] Test info command functionality
${result}= Run Process ${PYTHON} -m cleveragents info
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} CleverAgents Information
Should Contain ${result.stdout} Version:
Should Contain ${result.stdout} Environment
Should Contain ${result.stdout} Runtime
Invalid Command Error Handling
[Documentation] Verify proper error handling for invalid commands
+108
View File
@@ -0,0 +1,108 @@
*** Settings ***
Documentation Integration tests for enhanced version, info, diagnostics commands
Resource ${CURDIR}/common.resource
Library Process
Library OperatingSystem
Library String
Library Collections
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${PYTHON} python
*** Test Cases ***
Version Command Default Rich Format
[Documentation] Version command with default (rich) format shows version string
${result}= Run Process ${PYTHON} -m cleveragents version timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} 1.0.0
Should Contain ${result.stdout} CleverAgents
Version Command JSON Format
[Documentation] Version command with --format json returns valid JSON
${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} "version": "1.0.0"
Should Contain ${result.stdout} "schema": "v3"
Should Contain ${result.stdout} "channel": "stable"
Should Contain ${result.stdout} "python"
Should Contain ${result.stdout} "dependencies"
Should Contain ${result.stdout} "build_date"
Should Contain ${result.stdout} "commit"
Version Command Plain Format
[Documentation] Version command with --format plain returns key-value pairs
${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} version: 1.0.0
Should Contain ${result.stdout} schema: v3
Version Command YAML Format
[Documentation] Version command with --format yaml returns YAML
${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} version: 1.0.0
Should Contain ${result.stdout} schema: v3
Info Command Default Rich Format
[Documentation] Info command with default (rich) format shows environment details
${result}= Run Process ${PYTHON} -m cleveragents info timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} Environment
Should Contain ${result.stdout} Runtime
Info Command JSON Format
[Documentation] Info command with --format json returns structured data
${result}= Run Process ${PYTHON} -m cleveragents info --format json timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} "version": "1.0.0"
Should Contain ${result.stdout} "data_dir"
Should Contain ${result.stdout} "database"
Should Contain ${result.stdout} "server_mode": "disabled"
Info Command Plain Format
[Documentation] Info command with --format plain returns key-value pairs
${result}= Run Process ${PYTHON} -m cleveragents info --format plain timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} version: 1.0.0
Should Contain ${result.stdout} server_mode: disabled
Diagnostics Command Default Rich Format
[Documentation] Diagnostics command with default (rich) format runs checks
${result}= Run Process ${PYTHON} -m cleveragents diagnostics timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} Checks
Should Contain ${result.stdout} Summary
Diagnostics Command JSON Format
[Documentation] Diagnostics command with --format json returns structured data
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} "checks"
Should Contain ${result.stdout} "summary"
Should Contain ${result.stdout} "recommendations"
Should Contain ${result.stdout} "total"
Should Contain ${result.stdout} "warnings"
Should Contain ${result.stdout} "errors"
Diagnostics Command Plain Format
[Documentation] Diagnostics command with --format plain returns key-value pairs
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=30s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} checks:
Should Contain ${result.stdout} summary:
Diagnostics Command Check Flag No Errors
[Documentation] Diagnostics --check exits zero when no errors
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check timeout=30s
Should Be Equal As Integers ${result.rc} 0
Diagnostics Command Performance
[Documentation] Diagnostics command completes within acceptable time
${start}= Get Time epoch
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=30s
${end}= Get Time epoch
${duration}= Evaluate ${end} - ${start}
Should Be True ${duration} < 30 Diagnostics took too long: ${duration}s
Should Be Equal As Integers ${result.rc} 0
+508
View File
@@ -0,0 +1,508 @@
"""Core system commands: version, info, diagnostics.
Implements the CLI0.core group per specification. Each command supports
``--format`` for output parity (rich/plain/json/yaml) and ``diagnostics``
additionally supports ``--check`` to exit non-zero when any check fails.
"""
from __future__ import annotations
import os
import platform
import shutil
import subprocess
import sys
import time
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from typing import Any
from cleveragents import __version__
# ---------------------------------------------------------------------------
# Diagnostic check status
# ---------------------------------------------------------------------------
class CheckStatus(StrEnum):
"""Status of a single diagnostic check."""
OK = "ok"
WARN = "warn"
ERROR = "error"
# ---------------------------------------------------------------------------
# Data builders (format-agnostic)
# ---------------------------------------------------------------------------
def _git_sha() -> str:
"""Return the short git SHA of HEAD, or 'unknown'."""
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
pass
return "unknown"
def _dep_version(package: str) -> str:
"""Return the installed version of *package*, or 'not installed'."""
try:
from importlib.metadata import version as pkg_version
return pkg_version(package)
except Exception:
return "not installed"
def build_version_data() -> dict[str, Any]:
"""Assemble structured data for the ``version`` command."""
return {
"version": __version__,
"channel": "stable",
"python": platform.python_version(),
"build_date": datetime.now(tz=UTC).strftime("%Y-%m-%d"),
"commit": _git_sha(),
"schema": "v3",
"platform": f"{sys.platform}-{platform.machine()}",
"dependencies": {
"langgraph": _dep_version("langgraph"),
"langchain-core": _dep_version("langchain-core"),
"pydantic": _dep_version("pydantic"),
"typer": _dep_version("typer"),
},
}
def build_info_data() -> dict[str, Any]:
"""Assemble structured data for the ``info`` command."""
from cleveragents.config.settings import get_settings
settings = get_settings()
configured_providers = settings.configured_provider_names()
data_dir = settings.data_dir
config_path = Path(
os.environ.get(
"CLEVERAGENTS_CONFIG_PATH",
str(settings.storage_path / "config.toml"),
)
)
db_url = settings.database_url
# Storage sizes (best effort)
storage: dict[str, str] = {}
try:
db_path_str = db_url.replace("sqlite:///", "")
db_path = Path(db_path_str)
if db_path.exists():
size_mb = db_path.stat().st_size / (1024 * 1024)
storage["db_size"] = f"{size_mb:.1f} MB"
else:
storage["db_size"] = "0 MB"
except Exception:
storage["db_size"] = "unknown"
log_dir = settings.log_dir
if log_dir.exists():
total = sum(f.stat().st_size for f in log_dir.rglob("*") if f.is_file())
storage["logs"] = f"{total / (1024 * 1024):.1f} MB"
else:
storage["logs"] = "0 MB"
return {
"version": __version__,
"data_dir": str(data_dir),
"config_path": str(config_path),
"database": db_url,
"server_mode": "disabled",
"platform": f"{platform.system()} {platform.release()} ({platform.machine()})",
"automation": settings.default_automation_level,
"providers_configured": len(configured_providers),
"providers": configured_providers,
"debug_mode": settings.debug_enabled,
"storage": storage,
}
def _check_config_file() -> dict[str, Any]:
"""Check that the config path is readable."""
config_path = Path(
os.environ.get(
"CLEVERAGENTS_CONFIG_PATH",
"config.toml",
)
)
if config_path.exists():
readable = os.access(config_path, os.R_OK)
return {
"name": "Config file",
"status": CheckStatus.OK if readable else CheckStatus.ERROR,
"details": "readable" if readable else "not readable",
}
return {
"name": "Config file",
"status": CheckStatus.OK,
"details": "not present (using defaults)",
}
def _check_data_dir() -> dict[str, Any]:
"""Check that the data directory exists and is writable."""
from cleveragents.config.settings import get_settings
settings = get_settings()
data_dir = settings.data_dir
if not data_dir.exists():
return {
"name": "Data directory",
"status": CheckStatus.WARN,
"details": f"missing ({data_dir})",
"recommendation": f"Create it with: mkdir -p {data_dir}",
}
writable = os.access(data_dir, os.W_OK)
return {
"name": "Data directory",
"status": CheckStatus.OK if writable else CheckStatus.ERROR,
"details": "writable" if writable else "not writable",
}
def _check_database() -> dict[str, Any]:
"""Check database connectivity."""
from cleveragents.config.settings import get_settings
settings = get_settings()
db_url = settings.database_url
if db_url.startswith("sqlite"):
db_path_str = db_url.replace("sqlite:///", "")
db_path = Path(db_path_str)
if db_path.exists():
writable = os.access(db_path, os.W_OK)
return {
"name": "Database",
"status": CheckStatus.OK if writable else CheckStatus.ERROR,
"details": "writable" if writable else "locked or not writable",
}
# DB file doesn't exist yet — that's OK for SQLite (created on first use)
parent = db_path.parent
if parent.exists() and os.access(parent, os.W_OK):
return {
"name": "Database",
"status": CheckStatus.OK,
"details": "will be created on first use",
}
return {
"name": "Database",
"status": CheckStatus.ERROR,
"details": f"parent dir not writable ({parent})",
}
return {
"name": "Database",
"status": CheckStatus.OK,
"details": "configured",
}
def _check_providers() -> list[dict[str, Any]]:
"""Check provider API key configuration."""
from cleveragents.config.settings import get_settings
settings = get_settings()
results: list[dict[str, Any]] = []
provider_checks = [
("openai", "OPENAI_API_KEY"),
("anthropic", "ANTHROPIC_API_KEY"),
("google", "GOOGLE_API_KEY"),
("openrouter", "OPENROUTER_API_KEY"),
]
for provider_name, env_var in provider_checks:
configured = settings.has_provider_configured(provider_name)
results.append(
{
"name": f"{provider_name.capitalize()} key",
"status": CheckStatus.OK if configured else CheckStatus.WARN,
"details": "configured" if configured else "missing",
"recommendation": (
f"Set {env_var} to enable {provider_name.capitalize()} models"
if not configured
else None
),
}
)
return results
def _check_disk_space() -> dict[str, Any]:
"""Check available disk space."""
try:
usage = shutil.disk_usage(Path.cwd())
free_gb = usage.free / (1024**3)
if free_gb < 0.5:
return {
"name": "Disk space",
"status": CheckStatus.ERROR,
"details": f"{free_gb:.1f} GB free (critically low)",
}
if free_gb < 1.0:
return {
"name": "Disk space",
"status": CheckStatus.WARN,
"details": f"{free_gb:.1f} GB free (low)",
}
return {
"name": "Disk space",
"status": CheckStatus.OK,
"details": f"{free_gb:.1f} GB free",
}
except OSError:
return {
"name": "Disk space",
"status": CheckStatus.WARN,
"details": "unable to check",
}
def _check_git() -> dict[str, Any]:
"""Check git availability."""
try:
result = subprocess.run(
["git", "--version"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
version_str = result.stdout.strip().replace("git version ", "")
return {
"name": "Git",
"status": CheckStatus.OK,
"details": f"git {version_str}",
}
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
pass
return {
"name": "Git",
"status": CheckStatus.ERROR,
"details": "not found",
"recommendation": "Install git for version control integration",
}
def _check_file_permissions() -> dict[str, Any]:
"""Check file permissions on the data directory."""
from cleveragents.config.settings import get_settings
settings = get_settings()
data_dir = settings.data_dir
if not data_dir.exists():
return {
"name": "File permissions",
"status": CheckStatus.WARN,
"details": "data dir does not exist",
}
readable = os.access(data_dir, os.R_OK)
writable = os.access(data_dir, os.W_OK)
if readable and writable:
return {
"name": "File permissions",
"status": CheckStatus.OK,
"details": "data dir r/w",
}
perms = []
if readable:
perms.append("r")
if writable:
perms.append("w")
return {
"name": "File permissions",
"status": CheckStatus.ERROR if not writable else CheckStatus.WARN,
"details": f"data dir {''.join(perms) or 'no access'}",
}
def build_diagnostics_data() -> dict[str, Any]:
"""Run all diagnostic checks and return structured results."""
start = time.monotonic()
checks: list[dict[str, Any]] = []
checks.append(_check_config_file())
checks.append(_check_data_dir())
checks.append(_check_database())
checks.extend(_check_providers())
checks.append(_check_disk_space())
checks.append(_check_file_permissions())
checks.append(_check_git())
elapsed = time.monotonic() - start
total = len(checks)
ok_count = sum(1 for c in checks if c["status"] == CheckStatus.OK)
warn_count = sum(1 for c in checks if c["status"] == CheckStatus.WARN)
error_count = sum(1 for c in checks if c["status"] == CheckStatus.ERROR)
recommendations: list[str] = []
for check in checks:
rec = check.get("recommendation")
if rec:
recommendations.append(rec)
return {
"checks": checks,
"summary": {
"total": total,
"ok": ok_count,
"warnings": warn_count,
"errors": error_count,
"duration_s": round(elapsed, 2),
},
"recommendations": recommendations,
"has_errors": error_count > 0,
"has_warnings": warn_count > 0,
}
# ---------------------------------------------------------------------------
# Rich rendering helpers
# ---------------------------------------------------------------------------
def render_version_rich(data: dict[str, Any]) -> None:
"""Print version information using Rich panels."""
from rich.panel import Panel
from cleveragents.cli.main import get_console
console = get_console()
# CLI Version panel
version_lines = [
"[bold]CleverAgents CLI[/bold]",
f"[blue]Version:[/blue] {data['version']}",
f"[blue]Channel:[/blue] {data['channel']}",
f"[blue]Python:[/blue] {data['python']}",
]
console.print(Panel("\n".join(version_lines), title="CLI Version", expand=False))
# Build panel
build_lines = [
f"[green]Build Date:[/green] {data['build_date']}",
f"[magenta]Commit:[/magenta] {data['commit']}",
f"[blue]Schema:[/blue] {data['schema']}",
f"[blue]Platform:[/blue] {data['platform']}",
]
console.print(Panel("\n".join(build_lines), title="Build", expand=False))
# Dependencies panel
deps = data.get("dependencies", {})
dep_lines = [f"[blue]{k}:[/blue] {v}" for k, v in deps.items()]
if dep_lines:
console.print(Panel("\n".join(dep_lines), title="Dependencies", expand=False))
console.print("[green]OK[/green] Version reported")
def render_info_rich(data: dict[str, Any]) -> None:
"""Print info using Rich panels."""
from rich.panel import Panel
from cleveragents.cli.main import get_console
console = get_console()
env_lines = [
f"[blue]Data Dir:[/blue] {data['data_dir']}",
f"[blue]Config:[/blue] {data['config_path']}",
f"[green]Database:[/green] {data['database']}",
f"[yellow]Server Mode:[/yellow] {data['server_mode']}",
f"[blue]Platform:[/blue] {data['platform']}",
]
console.print(Panel("\n".join(env_lines), title="Environment", expand=False))
runtime_lines = [
f"[magenta]Automation:[/magenta] {data['automation']}",
f"[blue]Providers:[/blue] {data['providers_configured']} configured",
f"[blue]Debug Mode:[/blue] {data['debug_mode']}",
]
console.print(Panel("\n".join(runtime_lines), title="Runtime", expand=False))
storage = data.get("storage", {})
if storage:
storage_lines = [f"[blue]{k}:[/blue] {v}" for k, v in storage.items()]
console.print(Panel("\n".join(storage_lines), title="Storage", expand=False))
console.print("[green]OK[/green] Environment details ready")
def render_diagnostics_rich(data: dict[str, Any]) -> None:
"""Print diagnostics using Rich panels and table."""
from rich.panel import Panel
from rich.table import Table
from cleveragents.cli.main import get_console
console = get_console()
# Checks table
table = Table(title="Checks", show_header=True, expand=False)
table.add_column("Check", style="cyan")
table.add_column("Status")
table.add_column("Details")
for check in data["checks"]:
status = check["status"]
if status == CheckStatus.OK:
status_text = "[green]OK[/green]"
elif status == CheckStatus.WARN:
status_text = "[yellow]WARN[/yellow]"
else:
status_text = "[red]ERROR[/red]"
table.add_row(check["name"], status_text, check.get("details", ""))
console.print(table)
# Summary panel
summary = data["summary"]
summary_lines = [
f"[blue]Checks:[/blue] {summary['total']} total",
f"[yellow]Warnings:[/yellow] {summary['warnings']}",
f"[red]Errors:[/red] {summary['errors']}",
f"[green]Duration:[/green] {summary['duration_s']}s",
]
console.print(Panel("\n".join(summary_lines), title="Summary", expand=False))
# Recommendations
recs = data.get("recommendations", [])
if recs:
rec_lines = [f"- {r}" for r in recs]
console.print(
Panel("\n".join(rec_lines), title="Recommendations", expand=False)
)
# Final status line
if data["has_errors"]:
console.print(f"[red]ERROR[/red] {summary['errors']} errors must be resolved")
elif data["has_warnings"]:
console.print(
f"[yellow]WARN[/yellow] {summary['warnings']} warnings require attention"
)
else:
console.print("[green]OK[/green] All checks passed")
+59 -50
View File
@@ -187,68 +187,77 @@ def main_callback(
@app.command()
def version() -> None:
def version(
fmt: str = typer.Option(
"rich",
"--format",
"-f",
help="Output format: rich, plain, json, yaml",
),
) -> None:
"""Display version information."""
console = get_console()
console.print(f"CleverAgents {__version__}")
from cleveragents.cli.commands.system import (
build_version_data,
render_version_rich,
)
from cleveragents.cli.formatting import format_output
data = build_version_data()
if fmt.lower() == "rich":
render_version_rich(data)
else:
typer.echo(format_output(data, fmt))
@app.command()
def info() -> None:
def info(
fmt: str = typer.Option(
"rich",
"--format",
"-f",
help="Output format: rich, plain, json, yaml",
),
) -> None:
"""Display system information and configuration."""
from rich.panel import Panel
from cleveragents.cli.commands.system import build_info_data, render_info_rich
from cleveragents.cli.formatting import format_output
from cleveragents.config.settings import get_settings
settings = get_settings()
actor_configured = getattr(
settings, "has_actor_configured", settings.has_provider_configured
)()
info_text = f"""
[bold]CleverAgents Information[/bold]
Version: {__version__}
Storage: {settings.storage_path}
Debug Mode: {settings.debug_enabled}
Actor Configured: {actor_configured}
"""
console = get_console()
console.print(Panel(info_text.strip(), title="System Info", expand=False))
data = build_info_data()
if fmt.lower() == "rich":
render_info_rich(data)
else:
typer.echo(format_output(data, fmt))
@app.command()
def diagnostics() -> None:
def diagnostics(
check: bool = typer.Option(
False,
"--check",
help="Exit non-zero when any check fails",
),
fmt: str = typer.Option(
"rich",
"--format",
"-f",
help="Output format: rich, plain, json, yaml",
),
) -> None:
"""Run system diagnostics and health checks."""
import platform
from cleveragents.cli.commands.system import (
build_diagnostics_data,
render_diagnostics_rich,
)
from cleveragents.cli.formatting import format_output
from cleveragents.config.settings import get_settings
data = build_diagnostics_data()
if fmt.lower() == "rich":
render_diagnostics_rich(data)
else:
typer.echo(format_output(data, fmt))
settings = get_settings()
# Check basic configuration
checks = {
"Configuration Loaded": True,
"Storage Directory": settings.storage_path.exists(),
"Actor Configured": getattr(
settings, "has_actor_configured", settings.has_provider_configured
)(),
"Debug Mode": settings.debug_enabled,
}
console = get_console()
console.print("\n[bold]CleverAgents Diagnostics[/bold]\n")
console.print(f"Version: {__version__}")
console.print(f"Python: {sys.version.split()[0]}")
console.print(f"Platform: {platform.platform()}\n")
for check, result in checks.items():
status = "[green]✓[/green]" if result else "[red]✗[/red]"
console.print(f"{status} {check}: {result}")
console.print()
if check and data["has_errors"]:
raise typer.Exit(code=1)
@app.command()