praisonai-code 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Doctor engine for executing health checks.
|
|
3
|
+
|
|
4
|
+
Provides the core execution logic for running doctor checks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Callable, List, Optional
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
CheckCategory,
|
|
18
|
+
CheckResult,
|
|
19
|
+
CheckStatus,
|
|
20
|
+
CheckSeverity,
|
|
21
|
+
DoctorConfig,
|
|
22
|
+
DoctorReport,
|
|
23
|
+
EnvironmentSummary,
|
|
24
|
+
)
|
|
25
|
+
from .registry import CheckRegistry, get_registry
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DoctorEngine:
|
|
29
|
+
"""
|
|
30
|
+
Engine for executing doctor checks.
|
|
31
|
+
|
|
32
|
+
Handles check execution, timeout management, and report generation.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: Optional[DoctorConfig] = None):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the doctor engine.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Doctor configuration
|
|
41
|
+
"""
|
|
42
|
+
self.config = config or DoctorConfig()
|
|
43
|
+
self.registry = get_registry()
|
|
44
|
+
self._results: List[CheckResult] = []
|
|
45
|
+
|
|
46
|
+
def get_environment_summary(self) -> EnvironmentSummary:
|
|
47
|
+
"""Gather environment information."""
|
|
48
|
+
# Get package versions lazily
|
|
49
|
+
praisonai_version = "unknown"
|
|
50
|
+
praisonaiagents_version = "unknown"
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from praisonai.version import __version__ as pai_version
|
|
54
|
+
praisonai_version = pai_version
|
|
55
|
+
except ImportError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
import praisonaiagents
|
|
60
|
+
praisonaiagents_version = getattr(praisonaiagents, "__version__", "unknown")
|
|
61
|
+
except ImportError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return EnvironmentSummary(
|
|
65
|
+
python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
66
|
+
python_executable=sys.executable,
|
|
67
|
+
os_name=platform.system(),
|
|
68
|
+
os_version=platform.release(),
|
|
69
|
+
architecture=platform.machine(),
|
|
70
|
+
praisonai_version=praisonai_version,
|
|
71
|
+
praisonaiagents_version=praisonaiagents_version,
|
|
72
|
+
working_directory=os.getcwd(),
|
|
73
|
+
virtual_env=os.environ.get("VIRTUAL_ENV"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def run_check(
|
|
77
|
+
self,
|
|
78
|
+
check_id: str,
|
|
79
|
+
implementation: Callable,
|
|
80
|
+
timeout: Optional[float] = None,
|
|
81
|
+
) -> CheckResult:
|
|
82
|
+
"""
|
|
83
|
+
Run a single check with timeout handling.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
check_id: Check identifier
|
|
87
|
+
implementation: Check function
|
|
88
|
+
timeout: Timeout in seconds
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Check result
|
|
92
|
+
"""
|
|
93
|
+
timeout = timeout or self.config.timeout
|
|
94
|
+
definition = self.registry.get_check(check_id)
|
|
95
|
+
|
|
96
|
+
start_time = time.time()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Run with timeout using ThreadPoolExecutor
|
|
100
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
101
|
+
future = executor.submit(implementation, self.config)
|
|
102
|
+
try:
|
|
103
|
+
result = future.result(timeout=timeout)
|
|
104
|
+
result.duration_ms = (time.time() - start_time) * 1000
|
|
105
|
+
return result
|
|
106
|
+
except FuturesTimeoutError:
|
|
107
|
+
return CheckResult(
|
|
108
|
+
id=check_id,
|
|
109
|
+
title=definition.title if definition else check_id,
|
|
110
|
+
category=definition.category if definition else CheckCategory.ENVIRONMENT,
|
|
111
|
+
status=CheckStatus.ERROR,
|
|
112
|
+
message=f"Check timed out after {timeout}s",
|
|
113
|
+
remediation="Increase timeout with --timeout or check for hanging operations",
|
|
114
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
115
|
+
severity=CheckSeverity.HIGH,
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
# Capture exception details but redact potential secrets
|
|
119
|
+
tb = traceback.format_exc()
|
|
120
|
+
return CheckResult(
|
|
121
|
+
id=check_id,
|
|
122
|
+
title=definition.title if definition else check_id,
|
|
123
|
+
category=definition.category if definition else CheckCategory.ENVIRONMENT,
|
|
124
|
+
status=CheckStatus.ERROR,
|
|
125
|
+
message=f"Check failed with error: {type(e).__name__}: {str(e)[:100]}",
|
|
126
|
+
details=tb[:500] if not self.config.quiet else None,
|
|
127
|
+
remediation="Check the error details and fix the underlying issue",
|
|
128
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
129
|
+
severity=CheckSeverity.HIGH,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def run_checks(
|
|
133
|
+
self,
|
|
134
|
+
check_ids: Optional[List[str]] = None,
|
|
135
|
+
categories: Optional[List[CheckCategory]] = None,
|
|
136
|
+
) -> List[CheckResult]:
|
|
137
|
+
"""
|
|
138
|
+
Run multiple checks.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
check_ids: Specific check IDs to run (None = all)
|
|
142
|
+
categories: Filter by categories
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of check results
|
|
146
|
+
"""
|
|
147
|
+
# Get filtered checks
|
|
148
|
+
checks = self.registry.filter_checks(
|
|
149
|
+
only=check_ids or self.config.only or None,
|
|
150
|
+
skip=self.config.skip or None,
|
|
151
|
+
categories=categories,
|
|
152
|
+
deep_mode=self.config.deep,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Resolve dependencies and get ordered list
|
|
156
|
+
ordered_ids = self.registry.resolve_dependencies([c.id for c in checks])
|
|
157
|
+
|
|
158
|
+
results = []
|
|
159
|
+
failed_deps = set()
|
|
160
|
+
|
|
161
|
+
for check_id in ordered_ids:
|
|
162
|
+
definition = self.registry.get_check(check_id)
|
|
163
|
+
implementation = self.registry.get_implementation(check_id)
|
|
164
|
+
|
|
165
|
+
if not definition or not implementation:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Check if dependencies failed
|
|
169
|
+
if any(dep in failed_deps for dep in definition.dependencies):
|
|
170
|
+
results.append(CheckResult(
|
|
171
|
+
id=check_id,
|
|
172
|
+
title=definition.title,
|
|
173
|
+
category=definition.category,
|
|
174
|
+
status=CheckStatus.SKIP,
|
|
175
|
+
message="Skipped due to failed dependency",
|
|
176
|
+
severity=definition.severity,
|
|
177
|
+
))
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Run the check
|
|
181
|
+
result = self.run_check(check_id, implementation)
|
|
182
|
+
results.append(result)
|
|
183
|
+
|
|
184
|
+
# Track failures for dependency resolution
|
|
185
|
+
if result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
|
|
186
|
+
failed_deps.add(check_id)
|
|
187
|
+
|
|
188
|
+
# Fail fast if configured
|
|
189
|
+
if self.config.fail_fast:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
self._results = results
|
|
193
|
+
return results
|
|
194
|
+
|
|
195
|
+
def generate_report(self, results: Optional[List[CheckResult]] = None) -> DoctorReport:
|
|
196
|
+
"""
|
|
197
|
+
Generate a complete doctor report.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
results: Check results (uses stored results if None)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Complete doctor report
|
|
204
|
+
"""
|
|
205
|
+
results = results or self._results
|
|
206
|
+
|
|
207
|
+
report = DoctorReport(
|
|
208
|
+
version="1.0.0",
|
|
209
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
210
|
+
environment=self.get_environment_summary(),
|
|
211
|
+
results=results,
|
|
212
|
+
mode="deep" if self.config.deep else "fast",
|
|
213
|
+
filters={
|
|
214
|
+
"only": self.config.only,
|
|
215
|
+
"skip": self.config.skip,
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
report.calculate_summary()
|
|
220
|
+
report.exit_code = report.calculate_exit_code(strict=self.config.strict)
|
|
221
|
+
|
|
222
|
+
return report
|
|
223
|
+
|
|
224
|
+
def run(
|
|
225
|
+
self,
|
|
226
|
+
check_ids: Optional[List[str]] = None,
|
|
227
|
+
categories: Optional[List[CheckCategory]] = None,
|
|
228
|
+
) -> DoctorReport:
|
|
229
|
+
"""
|
|
230
|
+
Run checks and generate report.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
check_ids: Specific check IDs to run
|
|
234
|
+
categories: Filter by categories
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Complete doctor report
|
|
238
|
+
"""
|
|
239
|
+
start_time = time.time()
|
|
240
|
+
|
|
241
|
+
results = self.run_checks(check_ids, categories)
|
|
242
|
+
report = self.generate_report(results)
|
|
243
|
+
|
|
244
|
+
report.duration_ms = (time.time() - start_time) * 1000
|
|
245
|
+
|
|
246
|
+
return report
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run_doctor(
|
|
250
|
+
config: Optional[DoctorConfig] = None,
|
|
251
|
+
check_ids: Optional[List[str]] = None,
|
|
252
|
+
categories: Optional[List[CheckCategory]] = None,
|
|
253
|
+
) -> DoctorReport:
|
|
254
|
+
"""
|
|
255
|
+
Convenience function to run doctor checks.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
config: Doctor configuration
|
|
259
|
+
check_ids: Specific check IDs to run
|
|
260
|
+
categories: Filter by categories
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Complete doctor report
|
|
264
|
+
"""
|
|
265
|
+
engine = DoctorEngine(config)
|
|
266
|
+
return engine.run(check_ids, categories)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatters for the Doctor CLI module.
|
|
3
|
+
|
|
4
|
+
Provides text and JSON formatting with secret redaction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Dict, List, Optional, TextIO
|
|
11
|
+
from .models import CheckResult, CheckStatus, DoctorReport, ReportSummary
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Patterns for detecting secrets
|
|
15
|
+
SECRET_PATTERNS = [
|
|
16
|
+
re.compile(r'(sk-[a-zA-Z0-9]{20,})', re.IGNORECASE), # OpenAI
|
|
17
|
+
re.compile(r'(sk-ant-[a-zA-Z0-9-]{20,})', re.IGNORECASE), # Anthropic
|
|
18
|
+
re.compile(r'(AIza[a-zA-Z0-9_-]{35})', re.IGNORECASE), # Google
|
|
19
|
+
re.compile(r'(tvly-[a-zA-Z0-9]{20,})', re.IGNORECASE), # Tavily
|
|
20
|
+
re.compile(r'(xai-[a-zA-Z0-9]{20,})', re.IGNORECASE), # xAI
|
|
21
|
+
re.compile(r'([a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
|
22
|
+
re.compile(r'([a-zA-Z0-9_-]*token[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
|
23
|
+
re.compile(r'([a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
|
|
24
|
+
re.compile(r'(password\s*=\s*["\']?[^\s"\']{8,})', re.IGNORECASE),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Known API key environment variable names
|
|
28
|
+
API_KEY_ENV_VARS = [
|
|
29
|
+
"OPENAI_API_KEY",
|
|
30
|
+
"ANTHROPIC_API_KEY",
|
|
31
|
+
"GOOGLE_API_KEY",
|
|
32
|
+
"GEMINI_API_KEY",
|
|
33
|
+
"TAVILY_API_KEY",
|
|
34
|
+
"EXA_API_KEY",
|
|
35
|
+
"COHERE_API_KEY",
|
|
36
|
+
"HUGGINGFACE_API_KEY",
|
|
37
|
+
"HF_TOKEN",
|
|
38
|
+
"LANGFUSE_PUBLIC_KEY",
|
|
39
|
+
"LANGFUSE_SECRET_KEY",
|
|
40
|
+
"LANGSMITH_API_KEY",
|
|
41
|
+
"AGENTOPS_API_KEY",
|
|
42
|
+
"WANDB_API_KEY",
|
|
43
|
+
"DATADOG_API_KEY",
|
|
44
|
+
"BRAINTRUST_API_KEY",
|
|
45
|
+
"YDC_API_KEY",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def redact_secrets(text: str, show_prefix_suffix: bool = False) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Redact secrets from text.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
text: Text to redact
|
|
55
|
+
show_prefix_suffix: If True, show first 4 and last 4 chars
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Text with secrets redacted
|
|
59
|
+
"""
|
|
60
|
+
if not text:
|
|
61
|
+
return text
|
|
62
|
+
|
|
63
|
+
result = text
|
|
64
|
+
|
|
65
|
+
for pattern in SECRET_PATTERNS:
|
|
66
|
+
def replace_match(match):
|
|
67
|
+
secret = match.group(1)
|
|
68
|
+
if show_prefix_suffix and len(secret) > 12:
|
|
69
|
+
return f"{secret[:4]}...{secret[-4:]}"
|
|
70
|
+
return "***REDACTED***"
|
|
71
|
+
result = pattern.sub(replace_match, result)
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def redact_dict(data: Dict[str, Any], show_prefix_suffix: bool = False) -> Dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Recursively redact secrets from a dictionary.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
data: Dictionary to redact
|
|
82
|
+
show_prefix_suffix: If True, show first 4 and last 4 chars
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with secrets redacted
|
|
86
|
+
"""
|
|
87
|
+
result = {}
|
|
88
|
+
for key, value in data.items():
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
# Check if key suggests it's a secret
|
|
91
|
+
key_lower = key.lower()
|
|
92
|
+
if any(s in key_lower for s in ["key", "token", "secret", "password", "credential"]):
|
|
93
|
+
if show_prefix_suffix and len(value) > 12:
|
|
94
|
+
result[key] = f"{value[:4]}...{value[-4:]}"
|
|
95
|
+
else:
|
|
96
|
+
result[key] = "***REDACTED***"
|
|
97
|
+
else:
|
|
98
|
+
result[key] = redact_secrets(value, show_prefix_suffix)
|
|
99
|
+
elif isinstance(value, dict):
|
|
100
|
+
result[key] = redact_dict(value, show_prefix_suffix)
|
|
101
|
+
elif isinstance(value, list):
|
|
102
|
+
result[key] = [
|
|
103
|
+
redact_dict(v, show_prefix_suffix) if isinstance(v, dict)
|
|
104
|
+
else redact_secrets(v, show_prefix_suffix) if isinstance(v, str)
|
|
105
|
+
else v
|
|
106
|
+
for v in value
|
|
107
|
+
]
|
|
108
|
+
else:
|
|
109
|
+
result[key] = value
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BaseFormatter:
|
|
114
|
+
"""Base class for output formatters."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
no_color: bool = False,
|
|
119
|
+
quiet: bool = False,
|
|
120
|
+
redact: bool = True,
|
|
121
|
+
show_prefix_suffix: bool = False,
|
|
122
|
+
):
|
|
123
|
+
self.no_color = no_color or not sys.stdout.isatty()
|
|
124
|
+
self.quiet = quiet
|
|
125
|
+
self.redact = redact
|
|
126
|
+
self.show_prefix_suffix = show_prefix_suffix
|
|
127
|
+
|
|
128
|
+
def format_report(self, report: DoctorReport) -> str:
|
|
129
|
+
"""Format a complete report."""
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
|
|
132
|
+
def format_result(self, result: CheckResult) -> str:
|
|
133
|
+
"""Format a single check result."""
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
|
|
136
|
+
def _safe_write(self, output: TextIO, text: str) -> None:
|
|
137
|
+
"""Write text, falling back to ASCII on legacy-console encoding errors.
|
|
138
|
+
|
|
139
|
+
On Windows consoles using a legacy code page (e.g. cp1252), writing
|
|
140
|
+
Unicode characters raises UnicodeEncodeError. When that happens we
|
|
141
|
+
retry with an ASCII-safe representation so the command never crashes.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
output.write(text)
|
|
145
|
+
except UnicodeEncodeError:
|
|
146
|
+
encoding = getattr(output, "encoding", None) or "ascii"
|
|
147
|
+
safe_text = text.encode(encoding, errors="replace").decode(encoding, errors="replace")
|
|
148
|
+
output.write(safe_text)
|
|
149
|
+
|
|
150
|
+
def write(self, report: DoctorReport, output: Optional[TextIO] = None) -> None:
|
|
151
|
+
"""Write formatted report to output."""
|
|
152
|
+
output = output or sys.stdout
|
|
153
|
+
rendered = self.format_report(report)
|
|
154
|
+
if not rendered.endswith("\n"):
|
|
155
|
+
rendered += "\n"
|
|
156
|
+
self._safe_write(output, rendered)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TextFormatter(BaseFormatter):
|
|
160
|
+
"""Text formatter with optional color support."""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
no_color: bool = False,
|
|
165
|
+
quiet: bool = False,
|
|
166
|
+
redact: bool = True,
|
|
167
|
+
show_prefix_suffix: bool = False,
|
|
168
|
+
):
|
|
169
|
+
super().__init__(no_color, quiet, redact, show_prefix_suffix)
|
|
170
|
+
# Cache encoding detection result at initialization
|
|
171
|
+
self._unicode_supported = self._can_encode_unicode()
|
|
172
|
+
|
|
173
|
+
# ANSI color codes
|
|
174
|
+
COLORS = {
|
|
175
|
+
"reset": "\033[0m",
|
|
176
|
+
"bold": "\033[1m",
|
|
177
|
+
"dim": "\033[2m",
|
|
178
|
+
"red": "\033[31m",
|
|
179
|
+
"green": "\033[32m",
|
|
180
|
+
"yellow": "\033[33m",
|
|
181
|
+
"blue": "\033[34m",
|
|
182
|
+
"cyan": "\033[36m",
|
|
183
|
+
"white": "\033[37m",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
STATUS_COLORS = {
|
|
187
|
+
CheckStatus.PASS: "green",
|
|
188
|
+
CheckStatus.WARN: "yellow",
|
|
189
|
+
CheckStatus.FAIL: "red",
|
|
190
|
+
CheckStatus.SKIP: "dim",
|
|
191
|
+
CheckStatus.ERROR: "red",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
STATUS_SYMBOLS = {
|
|
195
|
+
CheckStatus.PASS: "✓",
|
|
196
|
+
CheckStatus.WARN: "⚠",
|
|
197
|
+
CheckStatus.FAIL: "✗",
|
|
198
|
+
CheckStatus.SKIP: "○",
|
|
199
|
+
CheckStatus.ERROR: "✗",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ASCII fallback symbols for systems that don't support UTF-8
|
|
203
|
+
STATUS_SYMBOLS_ASCII = {
|
|
204
|
+
CheckStatus.PASS: "[OK]",
|
|
205
|
+
CheckStatus.WARN: "[!]",
|
|
206
|
+
CheckStatus.FAIL: "[X]",
|
|
207
|
+
CheckStatus.SKIP: "[-]",
|
|
208
|
+
CheckStatus.ERROR: "[X]",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Divider character with ASCII fallback for legacy (cp1252) consoles
|
|
212
|
+
DIVIDER_UNICODE = "━"
|
|
213
|
+
DIVIDER_ASCII = "-"
|
|
214
|
+
|
|
215
|
+
def _color(self, text: str, color: str) -> str:
|
|
216
|
+
"""Apply color to text if colors are enabled."""
|
|
217
|
+
if self.no_color:
|
|
218
|
+
return text
|
|
219
|
+
return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
|
|
220
|
+
|
|
221
|
+
def _divider(self, width: int = 70) -> str:
|
|
222
|
+
"""Return a horizontal divider, using ASCII on non-Unicode consoles."""
|
|
223
|
+
char = self.DIVIDER_UNICODE if self._unicode_supported else self.DIVIDER_ASCII
|
|
224
|
+
return char * width
|
|
225
|
+
|
|
226
|
+
def _can_encode_unicode(self) -> bool:
|
|
227
|
+
"""Check if stdout can encode Unicode symbols."""
|
|
228
|
+
try:
|
|
229
|
+
# Get stdout encoding
|
|
230
|
+
encoding = getattr(sys.stdout, 'encoding', 'ascii') or 'ascii'
|
|
231
|
+
|
|
232
|
+
# UTF-8 always supports Unicode
|
|
233
|
+
if encoding.lower() in ('utf-8', 'utf8'):
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
# Test if we can encode our specific Unicode symbols
|
|
237
|
+
test_symbols = ["✓", "⚠", "✗", "○", self.DIVIDER_UNICODE]
|
|
238
|
+
for symbol in test_symbols:
|
|
239
|
+
try:
|
|
240
|
+
symbol.encode(encoding, errors='strict')
|
|
241
|
+
except (UnicodeEncodeError, LookupError):
|
|
242
|
+
return False
|
|
243
|
+
return True
|
|
244
|
+
except (AttributeError, TypeError):
|
|
245
|
+
# If we can't determine encoding, assume ASCII-only is safer
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def _status_symbol(self, status: CheckStatus) -> str:
|
|
249
|
+
"""Get colored status symbol with encoding safety."""
|
|
250
|
+
if self._unicode_supported:
|
|
251
|
+
symbol = self.STATUS_SYMBOLS.get(status, "?")
|
|
252
|
+
else:
|
|
253
|
+
symbol = self.STATUS_SYMBOLS_ASCII.get(status, "[?]")
|
|
254
|
+
color = self.STATUS_COLORS.get(status, "white")
|
|
255
|
+
return self._color(symbol, color)
|
|
256
|
+
|
|
257
|
+
def format_result(self, result: CheckResult) -> str:
|
|
258
|
+
"""Format a single check result."""
|
|
259
|
+
symbol = self._status_symbol(result.status)
|
|
260
|
+
message = result.message
|
|
261
|
+
if self.redact:
|
|
262
|
+
message = redact_secrets(message, self.show_prefix_suffix)
|
|
263
|
+
|
|
264
|
+
line = f"{symbol} {result.title}: {message}"
|
|
265
|
+
|
|
266
|
+
if result.details and not self.quiet:
|
|
267
|
+
details = result.details
|
|
268
|
+
if self.redact:
|
|
269
|
+
details = redact_secrets(details, self.show_prefix_suffix)
|
|
270
|
+
line += f"\n {self._color(details, 'dim')}"
|
|
271
|
+
|
|
272
|
+
if result.remediation and result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
|
|
273
|
+
line += f"\n {self._color('Fix:', 'yellow')} {result.remediation}"
|
|
274
|
+
|
|
275
|
+
return line
|
|
276
|
+
|
|
277
|
+
def format_summary(self, summary: ReportSummary, strict: bool = False) -> str:
|
|
278
|
+
"""Format the summary section."""
|
|
279
|
+
parts = []
|
|
280
|
+
|
|
281
|
+
if summary.passed > 0:
|
|
282
|
+
parts.append(self._color(f"{summary.passed} passed", "green"))
|
|
283
|
+
if summary.warnings > 0:
|
|
284
|
+
color = "red" if strict else "yellow"
|
|
285
|
+
parts.append(self._color(f"{summary.warnings} warnings", color))
|
|
286
|
+
if summary.failed > 0:
|
|
287
|
+
parts.append(self._color(f"{summary.failed} failed", "red"))
|
|
288
|
+
if summary.skipped > 0:
|
|
289
|
+
parts.append(self._color(f"{summary.skipped} skipped", "dim"))
|
|
290
|
+
if summary.errors > 0:
|
|
291
|
+
parts.append(self._color(f"{summary.errors} errors", "red"))
|
|
292
|
+
|
|
293
|
+
return f"{summary.total} checks: " + ", ".join(parts)
|
|
294
|
+
|
|
295
|
+
def format_report(self, report: DoctorReport) -> str:
|
|
296
|
+
"""Format a complete report."""
|
|
297
|
+
lines = []
|
|
298
|
+
|
|
299
|
+
# Header
|
|
300
|
+
if not self.quiet:
|
|
301
|
+
header = f"PraisonAI Doctor v{report.version}"
|
|
302
|
+
lines.append(self._color(header, "bold"))
|
|
303
|
+
lines.append(self._divider())
|
|
304
|
+
|
|
305
|
+
# Results
|
|
306
|
+
for result in report.results:
|
|
307
|
+
lines.append(self.format_result(result))
|
|
308
|
+
|
|
309
|
+
# Summary
|
|
310
|
+
if not self.quiet:
|
|
311
|
+
lines.append(self._divider())
|
|
312
|
+
lines.append(self.format_summary(report.summary))
|
|
313
|
+
|
|
314
|
+
# Duration
|
|
315
|
+
if not self.quiet and report.duration_ms > 0:
|
|
316
|
+
lines.append(self._color(f"Completed in {report.duration_ms:.0f}ms", "dim"))
|
|
317
|
+
|
|
318
|
+
return "\n".join(lines)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class JsonFormatter(BaseFormatter):
|
|
322
|
+
"""JSON formatter with deterministic output."""
|
|
323
|
+
|
|
324
|
+
def format_result(self, result: CheckResult) -> str:
|
|
325
|
+
"""Format a single check result as JSON."""
|
|
326
|
+
data = result.to_dict()
|
|
327
|
+
if self.redact:
|
|
328
|
+
data = redact_dict(data, self.show_prefix_suffix)
|
|
329
|
+
return json.dumps(data, indent=2, sort_keys=True)
|
|
330
|
+
|
|
331
|
+
def format_report(self, report: DoctorReport) -> str:
|
|
332
|
+
"""Format a complete report as JSON."""
|
|
333
|
+
data = report.to_dict()
|
|
334
|
+
if self.redact:
|
|
335
|
+
data = redact_dict(data, self.show_prefix_suffix)
|
|
336
|
+
|
|
337
|
+
# Ensure deterministic ordering
|
|
338
|
+
return json.dumps(data, indent=2, sort_keys=True)
|
|
339
|
+
|
|
340
|
+
# write() is inherited from BaseFormatter: it appends a trailing newline
|
|
341
|
+
# when absent (json.dumps never emits one) and routes through _safe_write,
|
|
342
|
+
# so the behaviour is identical and no override is needed.
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_formatter(
|
|
346
|
+
format_type: str = "text",
|
|
347
|
+
no_color: bool = False,
|
|
348
|
+
quiet: bool = False,
|
|
349
|
+
redact: bool = True,
|
|
350
|
+
show_prefix_suffix: bool = False,
|
|
351
|
+
) -> BaseFormatter:
|
|
352
|
+
"""
|
|
353
|
+
Get a formatter instance.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
format_type: "text" or "json"
|
|
357
|
+
no_color: Disable ANSI colors
|
|
358
|
+
quiet: Minimal output
|
|
359
|
+
redact: Redact secrets
|
|
360
|
+
show_prefix_suffix: Show partial secrets
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Formatter instance
|
|
364
|
+
"""
|
|
365
|
+
if format_type == "json":
|
|
366
|
+
return JsonFormatter(
|
|
367
|
+
no_color=True, # JSON never has colors
|
|
368
|
+
quiet=quiet,
|
|
369
|
+
redact=redact,
|
|
370
|
+
show_prefix_suffix=show_prefix_suffix,
|
|
371
|
+
)
|
|
372
|
+
return TextFormatter(
|
|
373
|
+
no_color=no_color,
|
|
374
|
+
quiet=quiet,
|
|
375
|
+
redact=redact,
|
|
376
|
+
show_prefix_suffix=show_prefix_suffix,
|
|
377
|
+
)
|