refactor: unify agent history and harden runtime diagnostics
Preserve user guidance through history compression, stabilize Flash index targets, and retire withdrawn checks without applying stale verdicts. Improve runtime diagnostics, tool responses, and history rendering. Simplify documentation and comments. Validation: 2076 tests passed in the full suite; 381 affected tests and 26 frontend tests passed after cleanup. Ruff and the quality ratchet passed.
This commit is contained in:
1 parent
e983180b5b
commit
5d92939d91
127 files changed
+13738
-1988
No files matched your search
@@ -12,321 +12,155 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""System, environment, and device diagnostics (artemis doctor)."""
|
||||
"""System, environment, and device diagnostics (artemis doctor).
|
||||
|
||||
The command renders the same readiness probes the web console's device wizard
|
||||
and the ``mobile_diagnose`` MCP tool run (:mod:`artemis.core.diagnostics`), so
|
||||
the terminal, the browser and the IDE agree on one verdict. Only two rows are
|
||||
CLI-specific and not engine probes: Node.js/npm and the Showcase UI build.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import platform as sys_platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
import typer
|
||||
|
||||
from artemis.config import (
|
||||
parse_llm_config,
|
||||
settings,
|
||||
)
|
||||
from artemis.config.paths import ROOT_DIR
|
||||
from artemis.core.diagnostics.adb_keys import heal_adb_keys, inspect_adb_keys
|
||||
from artemis.core.diagnostics.engine import readiness_engine
|
||||
from artemis.core.diagnostics.readiness import (
|
||||
Verdict,
|
||||
adb_keys_corrupted,
|
||||
base_verdict,
|
||||
collect_readiness,
|
||||
sort_by_fix_order,
|
||||
)
|
||||
from artemis.core.diagnostics.schema import ProbeResult, ProbeStatus
|
||||
from artemis.platform import OSType, platform
|
||||
from artemis.toolchain import toolchain
|
||||
from artemis.utils.ocr_api import is_ocr_configured
|
||||
from artemis.runtime.device_lock import DeviceExecutionLock
|
||||
|
||||
|
||||
def doctor_command(
|
||||
fix: bool = typer.Option(
|
||||
False,
|
||||
"--fix",
|
||||
"-f",
|
||||
help="Automatically repair fixable environment issues (such as corrupted ADB authentication keys).",
|
||||
),
|
||||
) -> None:
|
||||
"""Run diagnostics to inspect system dependencies, device connectivity, and configuration."""
|
||||
console = Console()
|
||||
console.print()
|
||||
os_name = sys_platform.system()
|
||||
arch_name = sys_platform.machine()
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold cyan]☕ Artemis System & Environment Doctor[/bold cyan]\n"
|
||||
f"[dim]Platform: {os_name} ({arch_name}) | PAL: {platform.os_type.value}[/dim]",
|
||||
expand=False,
|
||||
@dataclass(frozen=True)
|
||||
class ExtraRow:
|
||||
"""A CLI-only diagnostic row that is not backed by an engine probe."""
|
||||
|
||||
key: str
|
||||
title: str
|
||||
status: str # "pass" | "missing"
|
||||
status_markup: str
|
||||
summary: str
|
||||
detail: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FixOutcome:
|
||||
"""Result of one ``--fix`` repair step."""
|
||||
|
||||
fix: str
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _collect() -> list[ProbeResult]:
|
||||
report, host = await collect_readiness()
|
||||
return sort_by_fix_order([*report.probes, host])
|
||||
|
||||
|
||||
async def _apply_fixes(results: list[ProbeResult]) -> list[FixOutcome]:
|
||||
"""Repair corrupted ADB keys and sweep stale device locks left by dead runners."""
|
||||
outcomes: list[FixOutcome] = []
|
||||
|
||||
if adb_keys_corrupted(results):
|
||||
result = await readiness_engine.heal_adb_keys()
|
||||
outcomes.append(
|
||||
FixOutcome(
|
||||
fix="heal_adb_keys",
|
||||
success=bool(result.get("success")),
|
||||
message=str(result.get("message") or "")
|
||||
or ("Corrupted RSA keys regenerated." if result.get("success") else "Heal failed."),
|
||||
)
|
||||
)
|
||||
else:
|
||||
outcomes.append(
|
||||
FixOutcome(
|
||||
fix="heal_adb_keys",
|
||||
success=True,
|
||||
message="ADB authentication keys are healthy; nothing to repair.",
|
||||
)
|
||||
)
|
||||
|
||||
removed = await asyncio.to_thread(DeviceExecutionLock.cleanup_stale_locks)
|
||||
outcomes.append(
|
||||
FixOutcome(
|
||||
fix="cleanup_stale_locks",
|
||||
success=True,
|
||||
message=(
|
||||
f"Removed {removed} stale device lock(s)/queue ticket(s)."
|
||||
if removed
|
||||
else "No stale device locks or queue tickets found."
|
||||
),
|
||||
)
|
||||
)
|
||||
return outcomes
|
||||
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
table.add_column("Component", style="dim", width=22)
|
||||
table.add_column("Status", width=12)
|
||||
table.add_column("Details & Recommendations")
|
||||
|
||||
all_passed = True
|
||||
async def _diagnose(fix: bool) -> tuple[list[ProbeResult], list[FixOutcome]]:
|
||||
results = await _collect()
|
||||
fixes: list[FixOutcome] = []
|
||||
if fix:
|
||||
fixes = await _apply_fixes(results)
|
||||
results = await _collect()
|
||||
return results, fixes
|
||||
|
||||
# 1. Python Version
|
||||
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
if sys.version_info >= (3, 12):
|
||||
table.add_row(
|
||||
"Python Runtime",
|
||||
"[bold green]✔ OK[/bold green]",
|
||||
f"Python {py_ver} ({arch_name}, 64-bit)",
|
||||
)
|
||||
else:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"Python Runtime",
|
||||
"[bold red]✖ Warning[/bold red]",
|
||||
f"Python {py_ver} detected. Python >= 3.12 is strongly recommended.",
|
||||
)
|
||||
|
||||
# 2. ADB Installation & Server
|
||||
adb_path = toolchain.resolve("adb")
|
||||
if adb_path:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[adb_path, "version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
adb_info = res.stdout.splitlines()[0] if res.stdout else "Installed"
|
||||
table.add_row(
|
||||
"Android ADB", "[bold green]✔ OK[/bold green]", f"{adb_path} ({adb_info})"
|
||||
)
|
||||
except Exception:
|
||||
table.add_row("Android ADB", "[bold green]✔ OK[/bold green]", f"{adb_path}")
|
||||
else:
|
||||
all_passed = False
|
||||
hint = (
|
||||
"Run: winget install Google.PlatformTools"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "Run: brew install android-platform-tools"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "Run: sudo apt-get install -y adb"
|
||||
)
|
||||
table.add_row(
|
||||
"Android ADB",
|
||||
"[bold red]✖ Missing[/bold red]",
|
||||
f"adb not found in PATH or Android SDK. {hint}",
|
||||
)
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI-only extra rows (not engine probes)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 2b. ADB Authentication RSA Keys Check
|
||||
key_status = inspect_adb_keys()
|
||||
if key_status.is_corrupted:
|
||||
if fix:
|
||||
heal_result = heal_adb_keys(adb_path=adb_path)
|
||||
if heal_result.get("success"):
|
||||
table.add_row(
|
||||
"ADB Auth Keys",
|
||||
"[bold green]✔ Repaired[/bold green]",
|
||||
"Corrupted RSA keys detected and automatically regenerated.",
|
||||
)
|
||||
else:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"ADB Auth Keys",
|
||||
"[bold red]✖ Error[/bold red]",
|
||||
f"Failed to auto-heal keys: {heal_result.get('message')}",
|
||||
)
|
||||
else:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"ADB Auth Keys",
|
||||
"[bold red]✖ Corrupted[/bold red]",
|
||||
f"{key_status.error_reason} Run [bold cyan]artemis doctor --fix[/bold cyan] to auto-repair.",
|
||||
)
|
||||
elif key_status.exists:
|
||||
table.add_row(
|
||||
"ADB Auth Keys",
|
||||
"[bold green]✔ OK[/bold green]",
|
||||
f"{key_status.key_path} ({key_status.key_size_bytes} bytes RSA)",
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
"ADB Auth Keys",
|
||||
"[bold cyan]○ Ready[/bold cyan]",
|
||||
"Keys will be generated automatically on first device connection.",
|
||||
)
|
||||
|
||||
# 3. Connected Android Devices
|
||||
devices_info: list[tuple[str, str, str]] = []
|
||||
if adb_path:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[adb_path, "devices", "-l"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
lines = [line_str.strip() for line_str in res.stdout.splitlines() if line_str.strip()]
|
||||
for line in lines[1:]: # skip 'List of devices attached'
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
serial = parts[0]
|
||||
state = parts[1]
|
||||
extra = " ".join(parts[2:])
|
||||
devices_info.append((serial, state, extra))
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
# adb probe failed or timed out; report as "no devices detected".
|
||||
pass
|
||||
|
||||
if devices_info:
|
||||
active_devices = [d for d in devices_info if d[1] == "device"]
|
||||
if active_devices:
|
||||
summary = "\n".join(
|
||||
[
|
||||
f"• [bold cyan]{d[0]}[/bold cyan] ({d[2] or 'Authorized'})"
|
||||
for d in active_devices
|
||||
]
|
||||
)
|
||||
table.add_row("Android Devices", "[bold green]✔ Connected[/bold green]", summary)
|
||||
else:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"Android Devices",
|
||||
"[bold yellow]⚠ Unauthorized[/bold yellow]",
|
||||
"Device detected but unauthorized. Check phone screen for USB Debugging prompt.",
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
"Android Devices",
|
||||
"[bold yellow]⚠ None Found[/bold yellow]",
|
||||
"No Android device or emulator detected. Connect phone via USB or start emulator.",
|
||||
)
|
||||
|
||||
# 4. LLM & Vision OCR Key Configuration
|
||||
gemini_key = settings.get_api_key("google")
|
||||
ocr_key = settings.get_api_key("ocr")
|
||||
openai_key = settings.get_api_key("openai")
|
||||
claude_key = settings.get_api_key("anthropic")
|
||||
openrouter_key = settings.get_api_key("openrouter")
|
||||
|
||||
configured_keys = []
|
||||
if gemini_key:
|
||||
val = gemini_key.get_secret_value()
|
||||
masked = f"{val[:6]}...{val[-4:]}" if len(val) > 10 else "***"
|
||||
configured_keys.append(f"Gemini LLM ({masked})")
|
||||
if openai_key:
|
||||
configured_keys.append("OpenAI")
|
||||
if claude_key:
|
||||
configured_keys.append("Anthropic")
|
||||
if openrouter_key:
|
||||
configured_keys.append("OpenRouter")
|
||||
|
||||
if configured_keys:
|
||||
table.add_row(
|
||||
"LLM Providers", "[bold green]✔ Configured[/bold green]", ", ".join(configured_keys)
|
||||
)
|
||||
else:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"LLM Providers",
|
||||
"[bold red]✖ Missing[/bold red]",
|
||||
"No LLM key found in .env. Run [bold cyan]artemis init[/bold cyan] to configure.",
|
||||
)
|
||||
|
||||
# 5. Vision OCR Key Check (Optional)
|
||||
if is_ocr_configured() and ocr_key:
|
||||
val = ocr_key.get_secret_value()
|
||||
masked = f"{val[:6]}...{val[-4:]}" if len(val) > 10 else "***"
|
||||
table.add_row(
|
||||
"Vision OCR",
|
||||
"[bold green]✔ Configured[/bold green]",
|
||||
f"Google Cloud Vision API ({masked})",
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
"Vision OCR",
|
||||
"[bold cyan]○ Optional[/bold cyan]",
|
||||
"OCR_API_KEY is not set. Perception operates in pure XML layout mode.",
|
||||
)
|
||||
|
||||
# 6. Configuration File Health
|
||||
try:
|
||||
cfg = parse_llm_config()
|
||||
planner_model = cfg.planner.model
|
||||
table.add_row(
|
||||
"Configuration",
|
||||
"[bold green]✔ Valid[/bold green]",
|
||||
f"config/artemis.jsonc parsed (Planner: {planner_model})",
|
||||
)
|
||||
except Exception as e:
|
||||
all_passed = False
|
||||
table.add_row(
|
||||
"Configuration",
|
||||
"[bold red]✖ Error[/bold red]",
|
||||
f"Failed to parse config/artemis.jsonc: {e}",
|
||||
)
|
||||
|
||||
# 7. Video Recording Tools (FFmpeg & scrcpy)
|
||||
ffmpeg_path = toolchain.resolve("ffmpeg")
|
||||
scrcpy_path = toolchain.resolve("scrcpy")
|
||||
|
||||
if ffmpeg_path:
|
||||
table.add_row(
|
||||
"FFmpeg Tools", "[bold green]✔ Installed[/bold green]", f"{ffmpeg_path} (Video ready)"
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"winget install Gyan.FFmpeg"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install ffmpeg"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y ffmpeg"
|
||||
)
|
||||
table.add_row(
|
||||
"FFmpeg Tools",
|
||||
"[dim]⚪ Optional[/dim]",
|
||||
f"Not found. ({hint})",
|
||||
)
|
||||
|
||||
if scrcpy_path:
|
||||
table.add_row(
|
||||
"Screen Recorder",
|
||||
"[bold green]✔ Installed[/bold green]",
|
||||
f"{scrcpy_path} (Live stream ready)",
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"winget install Genymobile.scrcpy"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install scrcpy"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y scrcpy"
|
||||
)
|
||||
table.add_row(
|
||||
"Screen Recorder",
|
||||
"[dim]⚪ Optional[/dim]",
|
||||
f"scrcpy not found. ({hint})",
|
||||
)
|
||||
|
||||
# 8. Node.js & npm (for UI compilation)
|
||||
def _npm_row() -> ExtraRow:
|
||||
npm_path = shutil.which("npm")
|
||||
if npm_path:
|
||||
table.add_row(
|
||||
"Node.js / npm",
|
||||
"[bold green]✔ Installed[/bold green]",
|
||||
f"{npm_path} (Ready to build Showcase UI)",
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"winget install OpenJS.NodeJS.LTS"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install node"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y nodejs npm"
|
||||
)
|
||||
table.add_row(
|
||||
"Node.js / npm",
|
||||
"[dim]⚪ Optional[/dim]",
|
||||
f"Not found in PATH. ({hint})",
|
||||
return ExtraRow(
|
||||
key="nodejs_npm",
|
||||
title="Node.js / npm",
|
||||
status="pass",
|
||||
status_markup="[bold green]✔ Installed[/bold green]",
|
||||
summary="Installed",
|
||||
detail=f"{npm_path} (Ready to build Showcase UI)",
|
||||
)
|
||||
hint = (
|
||||
"winget install OpenJS.NodeJS.LTS"
|
||||
if platform.os_type == OSType.WINDOWS
|
||||
else "brew install node"
|
||||
if platform.os_type == OSType.MACOS
|
||||
else "sudo apt-get install -y nodejs npm"
|
||||
)
|
||||
return ExtraRow(
|
||||
key="nodejs_npm",
|
||||
title="Node.js / npm",
|
||||
status="missing",
|
||||
status_markup="[dim]⚪ Optional[/dim]",
|
||||
summary="Not Found",
|
||||
detail=f"Not found in PATH. ({hint})",
|
||||
)
|
||||
|
||||
# 9. Showcase UI Build Status
|
||||
|
||||
def _showcase_row() -> ExtraRow:
|
||||
from artemis.resources import get_bundled_showcase_dist
|
||||
|
||||
base_dist = ROOT_DIR / "apps" / "showcase_ui" / "dist"
|
||||
@@ -342,22 +176,98 @@ def doctor_command(
|
||||
if bundled_showcase is not None:
|
||||
found_showcase = bundled_showcase / "index.html"
|
||||
if found_showcase:
|
||||
table.add_row(
|
||||
"Showcase UI",
|
||||
"[bold green]✔ Compiled[/bold green]",
|
||||
f"{found_showcase.parent} (Angular ready)",
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
"Showcase UI",
|
||||
"[bold yellow]○ Not Compiled[/bold yellow]",
|
||||
"Run ./start.sh or artemis ui to auto-compile.",
|
||||
return ExtraRow(
|
||||
key="showcase_ui",
|
||||
title="Showcase UI",
|
||||
status="pass",
|
||||
status_markup="[bold green]✔ Compiled[/bold green]",
|
||||
summary="Compiled",
|
||||
detail=f"{found_showcase.parent} (Angular ready)",
|
||||
)
|
||||
return ExtraRow(
|
||||
key="showcase_ui",
|
||||
title="Showcase UI",
|
||||
status="missing",
|
||||
status_markup="[bold yellow]○ Not Compiled[/bold yellow]",
|
||||
summary="Not Compiled",
|
||||
detail="Run ./start.sh or artemis ui to auto-compile.",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rendering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _status_markup(result: ProbeResult) -> str:
|
||||
status = result.status
|
||||
if status is ProbeStatus.PASS:
|
||||
return "[bold green]✔ OK[/bold green]"
|
||||
if status is ProbeStatus.SKIPPED:
|
||||
return "[dim]○ Skipped[/dim]"
|
||||
summary = escape(result.summary)
|
||||
if status is ProbeStatus.WARN:
|
||||
# A warning keeps its summary whether or not it blocks (the host
|
||||
# probe's warnings degrade the verdict without blocking it).
|
||||
return f"[bold yellow]⚠ {summary}[/bold yellow]"
|
||||
if not result.is_blocker:
|
||||
return "[dim]⚪ Optional[/dim]"
|
||||
return f"[bold red]✖ {summary}[/bold red]"
|
||||
|
||||
|
||||
def _action_lines(result: ProbeResult) -> list[str]:
|
||||
"""One plain-text line per remediation; ``&&`` chains split for PowerShell 5.1."""
|
||||
lines: list[str] = []
|
||||
for action in result.actions:
|
||||
payload = action.payload.strip()
|
||||
if not payload:
|
||||
continue
|
||||
if action.action_type == "command":
|
||||
for part in payload.split("&&"):
|
||||
if part.strip():
|
||||
lines.append(f"Run: {part.strip()}")
|
||||
else:
|
||||
lines.append(payload)
|
||||
return lines
|
||||
|
||||
|
||||
def _details_markup(result: ProbeResult) -> str:
|
||||
# Only summary/description/actions are ever rendered. Probe metadata can
|
||||
# carry raw credentials (the console prefills its settings form from it)
|
||||
# and must never reach a terminal or a log.
|
||||
parts = [escape(result.description)]
|
||||
if result.status is not ProbeStatus.PASS:
|
||||
for line in _action_lines(result):
|
||||
if line.startswith("Run: "):
|
||||
parts.append(f"[bold cyan]Run:[/bold cyan] {escape(line[len('Run: ') :])}")
|
||||
else:
|
||||
parts.append(escape(line))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _render_table(results: list[ProbeResult], extras: list[ExtraRow]) -> Table:
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
table.add_column("Component", style="dim", min_width=22, max_width=30)
|
||||
table.add_column("Status", min_width=12, max_width=28)
|
||||
table.add_column("Details & Recommendations", overflow="fold")
|
||||
for result in results:
|
||||
table.add_row(escape(result.title), _status_markup(result), _details_markup(result))
|
||||
for extra in extras:
|
||||
table.add_row(extra.title, extra.status_markup, escape(extra.detail))
|
||||
return table
|
||||
|
||||
|
||||
def _render_fixes(console: Console, fixes: list[FixOutcome]) -> None:
|
||||
lines = []
|
||||
for outcome in fixes:
|
||||
mark = "[bold green]✔[/bold green]" if outcome.success else "[bold red]✖[/bold red]"
|
||||
lines.append(f"{mark} {escape(outcome.fix)}: {escape(outcome.message)}")
|
||||
console.print(Panel("\n".join(lines), title="Repairs (--fix)", expand=False))
|
||||
console.print()
|
||||
|
||||
if all_passed:
|
||||
|
||||
def _render_footer(console: Console, verdict: Verdict, results: list[ProbeResult]) -> None:
|
||||
if verdict == "ready":
|
||||
console.print(
|
||||
Panel(
|
||||
"[bold green]🎉 All system checks passed![/bold green]\n\n"
|
||||
@@ -367,12 +277,128 @@ def doctor_command(
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
if verdict == "degraded":
|
||||
console.print(
|
||||
Panel(
|
||||
"[bold yellow]💡 Tip:[/bold yellow] Run [bold cyan]artemis init[/bold cyan] to quickly set up your API key and device.",
|
||||
title="Action Required",
|
||||
"[bold green]Required checks passed.[/bold green] Optional components are "
|
||||
"missing; tasks can run, but some features (video replay, OCR) are off.\n\n"
|
||||
"Run your first automation task now:\n"
|
||||
' [bold cyan]artemis run "Open Settings and check Battery level"[/bold cyan]',
|
||||
title="Status: Ready (optional gaps)",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return
|
||||
|
||||
failing = [r.title for r in results if r.is_blocker and r.status is not ProbeStatus.PASS]
|
||||
tips = [
|
||||
"[bold yellow]💡 Blocking issues:[/bold yellow] " + escape(", ".join(failing)),
|
||||
"",
|
||||
"• API keys & device setup: run [bold cyan]artemis init[/bold cyan].",
|
||||
"• Follow the [bold cyan]Run:[/bold cyan] lines above, then re-run "
|
||||
"[bold cyan]artemis doctor[/bold cyan].",
|
||||
"• Using an IDE (Claude Code, Cursor, ...)? Ask it to call "
|
||||
"[bold cyan]mobile_diagnose[/bold cyan] - it runs these same checks and can "
|
||||
"apply safe fixes from the assistant.",
|
||||
]
|
||||
if adb_keys_corrupted(results):
|
||||
tips.append(
|
||||
"• Corrupted ADB keys detected: run [bold cyan]artemis doctor --fix[/bold cyan]."
|
||||
)
|
||||
console.print(Panel("\n".join(tips), title="Action Required", expand=False))
|
||||
|
||||
|
||||
def _json_document(
|
||||
results: list[ProbeResult],
|
||||
extras: list[ExtraRow],
|
||||
fixes: list[FixOutcome],
|
||||
verdict: Verdict,
|
||||
) -> dict[str, Any]:
|
||||
# Metadata is intentionally omitted: it may hold raw API keys.
|
||||
checks = [
|
||||
{
|
||||
"id": r.id,
|
||||
"title": r.title,
|
||||
"status": r.status.value,
|
||||
"required": r.is_blocker,
|
||||
"summary": r.summary,
|
||||
"detail": r.description,
|
||||
"fix": []
|
||||
if r.status is ProbeStatus.PASS
|
||||
else [
|
||||
{"type": a.action_type, "label": a.label, "payload": a.payload} for a in r.actions
|
||||
],
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"checks": checks,
|
||||
"extras": {
|
||||
e.key: {
|
||||
"title": e.title,
|
||||
"status": e.status,
|
||||
"summary": e.summary,
|
||||
"detail": e.detail,
|
||||
}
|
||||
for e in extras
|
||||
},
|
||||
"fixes": [{"fix": f.fix, "success": f.success, "message": f.message} for f in fixes],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def doctor_command(
|
||||
fix: bool = typer.Option(
|
||||
False,
|
||||
"--fix",
|
||||
"-f",
|
||||
help=(
|
||||
"Automatically repair fixable environment issues (corrupted ADB authentication "
|
||||
"keys, stale device locks left by crashed runners)."
|
||||
),
|
||||
),
|
||||
json_output: bool = typer.Option(
|
||||
False,
|
||||
"--json",
|
||||
help="Print the diagnosis as a JSON document instead of a table.",
|
||||
),
|
||||
) -> None:
|
||||
"""Run diagnostics to inspect system dependencies, device connectivity, and configuration."""
|
||||
results, fixes = asyncio.run(_diagnose(fix))
|
||||
extras = [_npm_row(), _showcase_row()]
|
||||
verdict = base_verdict(results)
|
||||
|
||||
if json_output:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
_json_document(results, extras, fixes, verdict), indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
else:
|
||||
console = Console()
|
||||
console.print()
|
||||
os_name = sys_platform.system()
|
||||
arch_name = sys_platform.machine()
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold cyan]☕ Artemis System & Environment Doctor[/bold cyan]\n"
|
||||
f"[dim]Platform: {os_name} ({arch_name}) | PAL: {platform.os_type.value}[/dim]",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
if fixes:
|
||||
_render_fixes(console, fixes)
|
||||
console.print(_render_table(results, extras))
|
||||
console.print()
|
||||
_render_footer(console, verdict, results)
|
||||
console.print()
|
||||
|
||||
if verdict == "blocked":
|
||||
raise typer.Exit(code=1)
|
||||
@@ -41,6 +41,7 @@ ARTEMIS_MCP_TOOLS = (
|
||||
"mobile_manage_task",
|
||||
"mobile_get_device_state",
|
||||
"mobile_inspect_trace",
|
||||
"mobile_diagnose",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user