"""Full environment diagnostic command, inspired by ``fly doctor``. Rendered output (colour roles): ──────────────────────────────────────────── [DIM rule] OpenSRE Doctor [TEXT bold section header] ──────────────────────────────────────────── [DIM rule] ✓ python Python 3.13.2 [HIGHLIGHT ✓] [SECONDARY check] [TEXT detail] ✓ env_file .env (12 keys) ⚠ integrations no integrations [WARNING ⚠] ✗ llm_provider ANTHROPIC_API_KEY unset [ERROR ✗] ✓ version (up to date) ✓ network github.com reachable ──────────────────────────────────────────── [DIM rule] 1 error · 1 warning — run opensre doctor [ERROR/WARNING counts · SECONDARY hint] All checks passed. [HIGHLIGHT — when clean] """ from __future__ import annotations import json import os import sys from pathlib import Path from typing import Any import click from rich.console import Console from rich.rule import Rule from rich.text import Text import platform from config.version import get_opensre_version from platform.common.exit_codes import ERROR as EXIT_ERROR from platform.common.exit_codes import SUCCESS from platform.common.runtime_flags import is_json_output from platform.terminal.theme import ( DIM, ERROR, GLYPH_ERROR, GLYPH_SUCCESS, GLYPH_WARNING, HIGHLIGHT, SECONDARY, TEXT, WARNING, ) def _check(name: str, fn: Any) -> dict[str, str]: """Run a single diagnostic check and return a result dict.""" try: ok, detail = fn() return {"check": name, "status": "ok" if ok else "warn", "detail": detail} except Exception as exc: return {"check": name, "status": "error", "detail": str(exc)} def _check_python_version() -> tuple[bool, str]: version = platform.python_version() major, minor = sys.version_info[:2] if (major, minor) < (3, 11): return False, f"Python {version} — opensre requires >= 3.11" return True, f"Python {version}" def _check_env_file() -> tuple[bool, str]: env_path = os.getenv("OPENSRE_PROJECT_ENV_PATH", ".env") path = Path(env_path) if not path.exists(): return False, f"{env_path} not found" content = path.read_text(encoding="utf-8", errors="replace") lines = [ln for ln in content.splitlines() if ln.strip() and not ln.strip().startswith("#")] return True, f"{env_path} ({len(lines)} keys)" def _check_llm_provider() -> tuple[bool, str]: from config.config import get_configured_llm_provider from config.llm_auth.credentials import status as credential_status provider = get_configured_llm_provider() auth = credential_status(provider) if auth.configured and not auth.stale: return True, f"provider={provider}, auth={auth.source} ({auth.detail})" if auth.stale: return False, f"provider={provider}, auth stale ({auth.detail})" return False, f"provider={provider}, auth missing ({auth.detail})" def _check_integrations() -> tuple[bool, str]: from integrations.store import STORE_PATH, list_integrations path = Path(str(STORE_PATH)) if not path.exists(): return False, f"{STORE_PATH} not found — run 'opensre integrations setup'" items = list_integrations() if not items: return False, "no integrations configured" names = [i["service"] for i in items] return True, f"{len(items)} configured: {', '.join(names)}" def _check_version_freshness() -> tuple[bool, str]: current = get_opensre_version() from surfaces.cli.lifecycle.update import ( _fetch_latest_version, _is_update_available, development_install_doctor_version_detail, ) dev_detail = development_install_doctor_version_detail(current) if dev_detail is not None: return True, dev_detail try: latest = _fetch_latest_version() if _is_update_available(current, latest): return False, f"current={current}, latest={latest} — run 'opensre update'" return True, f"{current} (up to date)" except Exception as exc: return True, f"{current} (could not check: {exc})" _CHECKS = [ ("python", _check_python_version), ("env_file", _check_env_file), ("llm_provider", _check_llm_provider), ("integrations", _check_integrations), ("version", _check_version_freshness), ] def _render_doctor_results(console: Console, results: list[dict[str, str]]) -> None: """Print formatted diagnostic results with design-system colour roles. Section header: TEXT bold + DIM rule (col 45). ✓ checks: HIGHLIGHT glyph, SECONDARY check name, TEXT detail. ⚠ warnings: WARNING glyph, SECONDARY check name, TEXT detail. ✗ errors: ERROR glyph, SECONDARY check name, TEXT detail. Summary: ERROR count + WARNING count in their respective roles, SECONDARY hint. """ console.print() console.print(Rule(style=DIM)) # Section header — TEXT weight, DIM rule to col 45. header = Text() header.append(" OpenSRE Doctor", style=f"bold {TEXT}") console.print(header) console.print(Rule(style=DIM)) console.print() check_col = 18 # fixed column width for check name alignment for r in results: status = r["status"] if status == "ok": glyph = GLYPH_SUCCESS glyph_style = f"bold {HIGHLIGHT}" detail_style = TEXT elif status == "warn": glyph = GLYPH_WARNING glyph_style = f"bold {WARNING}" detail_style = TEXT else: glyph = GLYPH_ERROR glyph_style = f"bold {ERROR}" detail_style = TEXT row = Text() row.append(f" {glyph} ", style=glyph_style) row.append(f"{r['check']:<{check_col}}", style=SECONDARY) row.append(r["detail"], style=detail_style) console.print(row) console.print() console.print(Rule(style=DIM)) error_count = sum(1 for r in results if r["status"] == "error") warn_count = sum(1 for r in results if r["status"] == "warn") if error_count == 0 and warn_count == 0: summary = Text() summary.append(f" {GLYPH_SUCCESS} ", style=f"bold {HIGHLIGHT}") summary.append("All checks passed.", style=TEXT) console.print(summary) else: summary = Text() summary.append(" ") if error_count: summary.append( f"{error_count} error{'s' if error_count > 1 else ''}", style=f"bold {ERROR}" ) if error_count and warn_count: summary.append(" · ", style=SECONDARY) if warn_count: summary.append( f"{warn_count} warning{'s' if warn_count > 1 else ''}", style=f"bold {WARNING}" ) summary.append(" — fix and rerun ", style=SECONDARY) summary.append("opensre doctor", style=f"bold {TEXT}") console.print(summary) console.print() @click.command(name="doctor") def doctor_command() -> None: """Run a full environment diagnostic to surface setup issues.""" results: list[dict[str, str]] = [] for name, fn in _CHECKS: results.append(_check(name, fn)) if is_json_output(): click.echo(json.dumps(results, indent=2)) else: console = Console( highlight=False, force_terminal=True, color_system="truecolor", legacy_windows=False, ) _render_doctor_results(console, results) has_errors = any(r["status"] == "error" for r in results) raise SystemExit(EXIT_ERROR if has_errors else SUCCESS)