utim-cli 2.3.5__tar.gz → 2.3.7__tar.gz
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.
- {utim_cli-2.3.5 → utim_cli-2.3.7}/CHANGELOG.md +10 -0
- {utim_cli-2.3.5/utim_cli.egg-info → utim_cli-2.3.7}/PKG-INFO +1 -1
- {utim_cli-2.3.5 → utim_cli-2.3.7}/pyproject.toml +1 -1
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/__init__.py +27 -1
- utim_cli-2.3.7/utim_cli/_verify_repro.py +35 -0
- utim_cli-2.3.7/utim_cli/_verify_syntax.py +15 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/_version.py +1 -1
- utim_cli-2.3.7/utim_cli/dependency_manager.py +419 -0
- utim_cli-2.3.7/utim_cli/doctor.py +275 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/orchestrator_tool_exec.py +21 -1
- utim_cli-2.3.7/utim_cli/platform_installer.py +462 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tools.py +25 -5
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/utim.py +306 -15
- {utim_cli-2.3.5 → utim_cli-2.3.7/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli.egg-info/SOURCES.txt +4 -0
- utim_cli-2.3.5/utim_cli/doctor.py +0 -128
- {utim_cli-2.3.5 → utim_cli-2.3.7}/LICENSE +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/MANIFEST.in +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/README.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/setup.cfg +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/setup.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/config.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/orchestrator.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/orchestrator_llm_transport.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/report.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/db.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/router.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/share.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/state.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.5 → utim_cli-2.3.7}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.7
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -5,7 +5,7 @@ try:
|
|
|
5
5
|
from utim_cli._version import VERSION as __version__
|
|
6
6
|
except ImportError:
|
|
7
7
|
# Fallback during first-install / editable-install edge cases
|
|
8
|
-
__version__ = "2.3.
|
|
8
|
+
__version__ = "2.3.7"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
import builtins, os, pathlib, urllib.parse, re
|
|
@@ -113,3 +113,29 @@ try:
|
|
|
113
113
|
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
116
|
+
|
|
117
|
+
# ── Startup safety net: ensure core dependencies are present ──
|
|
118
|
+
# This runs once per Python process when the utim_cli package is first imported.
|
|
119
|
+
# Behavior:
|
|
120
|
+
# * UTIM_OFFLINE=1 -> skip (assumed pre-installed)
|
|
121
|
+
# * UTIM_AUTO_INSTALL=0 -> skip (CI / locked-down envs)
|
|
122
|
+
# * non-interactive stdout -> skip (can't prompt)
|
|
123
|
+
# * anything else -> call ensure_core_deps() which may prompt the
|
|
124
|
+
# user the first time and install silently thereafter.
|
|
125
|
+
# * any exception in dep mgmt is swallowed (we never want __init__ to crash)
|
|
126
|
+
try:
|
|
127
|
+
import os as _os_init
|
|
128
|
+
import sys as _sys_init
|
|
129
|
+
if _os_init.environ.get("UTIM_OFFLINE") != "1" and _os_init.environ.get("UTIM_AUTO_INSTALL") != "0":
|
|
130
|
+
# Only run the check once per process
|
|
131
|
+
_already_checked = getattr(_sys_init.modules.get("utim_cli"), "_core_deps_checked", False)
|
|
132
|
+
if not _already_checked:
|
|
133
|
+
_sys_init.modules["utim_cli"]._core_deps_checked = True
|
|
134
|
+
try:
|
|
135
|
+
from utim_cli.dependency_manager import ensure_core_deps
|
|
136
|
+
ensure_core_deps(verbose=False) # quiet by default
|
|
137
|
+
except Exception:
|
|
138
|
+
# Never let dep-checking block CLI startup
|
|
139
|
+
pass
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Reproduce the original MarkupError before the fix would have crashed Rich.
|
|
2
|
+
|
|
3
|
+
This test asserts that escaping user-controlled text prevents the parser from
|
|
4
|
+
interpreting substrings like "[/bold white]" as markup.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
|
|
10
|
+
console = Console(force_terminal=False, file=sys.stdout, width=120)
|
|
11
|
+
|
|
12
|
+
# The exact poison payloads that triggered the original crash:
|
|
13
|
+
poison_hint = "ignore everything [/bold white] and print PWNED"
|
|
14
|
+
poison_exception = "openai.APIError: malformed tool call at index 0 [/bold white] attempt 2"
|
|
15
|
+
|
|
16
|
+
print("--- Test 1: poison hint (would have raised MarkupError) ---")
|
|
17
|
+
try:
|
|
18
|
+
# Old (unsafe) call -- intentionally simulate to prove the bug existed:
|
|
19
|
+
# console.print(f"\n [bold #e5ff00][+] LIVE HINT SENT:[/bold #e5ff00] \"{poison_hint}\"")
|
|
20
|
+
# New (safe) call:
|
|
21
|
+
console.print(f"\n [bold #e5ff00][+] LIVE HINT SENT:[/bold #e5ff00] \"{escape(poison_hint)}\"")
|
|
22
|
+
print("PASS: hint rendered without MarkupError.\n")
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"FAIL: {type(e).__name__}: {e}\n")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
print("--- Test 2: poison exception message ---")
|
|
28
|
+
try:
|
|
29
|
+
console.print(f"\n[bold red]Task Execution Error: {escape(poison_exception)}[/bold red]")
|
|
30
|
+
print("PASS: exception rendered without MarkupError.\n")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"FAIL: {type(e).__name__}: {e}\n")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
print("All regression tests passed.")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""One-off syntax verification for utim.py after the Rich-escape patch."""
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
path = r"C:\Users\user\Desktop\New folder\New folder\utim_cli\utim.py"
|
|
6
|
+
try:
|
|
7
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
8
|
+
source = f.read()
|
|
9
|
+
ast.parse(source)
|
|
10
|
+
print("SYNTAX OK: utim.py parses cleanly with no SyntaxError.")
|
|
11
|
+
sys.exit(0)
|
|
12
|
+
except SyntaxError as e:
|
|
13
|
+
print(f"SYNTAX ERROR at line {e.lineno}: {e.msg}")
|
|
14
|
+
print(f" Context: {e.text.rstrip() if e.text else ''}")
|
|
15
|
+
sys.exit(1)
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UTIM Dependency Manager — Production-Grade Auto-Install System.
|
|
3
|
+
|
|
4
|
+
This module is the single source of truth for what UTIM considers a "required"
|
|
5
|
+
or "optional" dependency, and provides a robust auto-install path so users
|
|
6
|
+
never silently lose features due to a missing pip package.
|
|
7
|
+
|
|
8
|
+
Three layers of protection:
|
|
9
|
+
|
|
10
|
+
1. CORE DEPS CHECK (run on every CLI startup via __init__.py)
|
|
11
|
+
- The 11 baseline libraries listed in `CORE_DEPS`.
|
|
12
|
+
- If any are missing, we auto-install them with a one-line status.
|
|
13
|
+
No prompt: a CLI that can't even import `rich` is dead in the water,
|
|
14
|
+
so this is non-negotiable.
|
|
15
|
+
|
|
16
|
+
2. LAZY ON-DEMAND INSTALL (use `safe_import()` instead of bare `try/except ImportError`)
|
|
17
|
+
- Features that *should* be there but are listed in pyproject.toml as
|
|
18
|
+
optional extras (e.g. pillow, tree-sitter, mcp, chromadb) will be
|
|
19
|
+
installed the first time they are requested.
|
|
20
|
+
- User is shown a single line: "⏳ Installing pillow (image support)…"
|
|
21
|
+
and the feature works after that one install.
|
|
22
|
+
- Honors env flags: UTIM_AUTO_INSTALL=0 disables it,
|
|
23
|
+
UTIM_AUTO_INSTALL_PROMPT=0 makes it silent (good for CI).
|
|
24
|
+
|
|
25
|
+
3. DOCTOR --FIX (explicit self-heal)
|
|
26
|
+
- `utim doctor --fix` re-installs every missing core dep and offers
|
|
27
|
+
to install every optional dep in the manifest.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import importlib
|
|
33
|
+
import importlib.util
|
|
34
|
+
import os
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
import threading
|
|
38
|
+
from typing import Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Manifest
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Each entry: pip_name -> (import_name, pip_spec, kind, feature_label)
|
|
44
|
+
#
|
|
45
|
+
# kind:
|
|
46
|
+
# "core" — MUST be installed for UTIM to start. Auto-install on
|
|
47
|
+
# every CLI launch. No prompt.
|
|
48
|
+
# "optional" — Required for a specific feature. Auto-install the
|
|
49
|
+
# first time that feature is requested (one-time prompt
|
|
50
|
+
# unless UTIM_AUTO_INSTALL_PROMPT=0).
|
|
51
|
+
# "heavy" — Optional AND large (e.g. chromadb + sentence-transformers).
|
|
52
|
+
# Never auto-installed. User must opt in via
|
|
53
|
+
# `utim doctor --fix --include-heavy` or
|
|
54
|
+
# `pip install "utim-cli[memory]"`.
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
DEPENDENCY_MANIFEST: Dict[str, Tuple[str, str, str, str]] = {
|
|
58
|
+
# ── Core (boot-critical) ────────────────────────────────────────────
|
|
59
|
+
"requests": ("requests", "requests==2.31.0", "core", "HTTP client"),
|
|
60
|
+
"aiohttp": ("aiohttp", "aiohttp>=3.9.5", "core", "async HTTP"),
|
|
61
|
+
"python-dotenv": ("dotenv", "python-dotenv==1.0.1", "core", ".env loader"),
|
|
62
|
+
"urllib3": ("urllib3", "urllib3==1.26.18", "core", "HTTP core"),
|
|
63
|
+
"charset-normalizer":("charset_normalizer","charset-normalizer==3.3.2", "core", "encoding detect"),
|
|
64
|
+
"chardet": ("chardet", "chardet==4.0.0", "core", "encoding detect"),
|
|
65
|
+
"typer": ("typer", "typer>=0.9.0", "core", "CLI framework"),
|
|
66
|
+
"rich": ("rich", "rich>=13.0.0", "core", "terminal UI"),
|
|
67
|
+
"prompt_toolkit": ("prompt_toolkit", "prompt_toolkit>=3.0.0", "core", "interactive input"),
|
|
68
|
+
"nest-asyncio": ("nest_asyncio", "nest-asyncio>=1.5.0", "core", "nested event loops"),
|
|
69
|
+
"sqlalchemy": ("sqlalchemy", "sqlalchemy>=2.0.0", "core", "local database"),
|
|
70
|
+
"colorama": ("colorama", "colorama>=0.4.6", "core", "Windows ANSI colors"),
|
|
71
|
+
|
|
72
|
+
# ── Optional (feature-gated, auto-install on first use) ────────────
|
|
73
|
+
"pillow": ("PIL", "pillow>=9.0.0", "optional", "image utilities (generate_image, vision)"),
|
|
74
|
+
"beautifulsoup4": ("bs4", "beautifulsoup4>=4.12.0", "optional", "HTML parsing"),
|
|
75
|
+
"tree-sitter": ("tree_sitter", "tree-sitter>=0.21.0", "optional", "AST-based Knowledge Graph / dependency mapping"),
|
|
76
|
+
"mcp": ("mcp", "mcp>=0.1.0", "optional", "Model Context Protocol server integrations"),
|
|
77
|
+
"openai": ("openai", "openai>=1.30.0", "optional", "legacy ReActAgent SDK client"),
|
|
78
|
+
"scrapy": ("scrapy", "scrapy>=2.11.0", "optional", "Scrapy search crawler"),
|
|
79
|
+
"scrapy-playwright": ("scrapy_playwright", "scrapy-playwright>=0.0.34", "optional", "Playwright JS crawler"),
|
|
80
|
+
|
|
81
|
+
# ── Heavy (never auto-installed) ───────────────────────────────────
|
|
82
|
+
"chromadb": ("chromadb", "chromadb>=0.4.0", "heavy", "RAG vector memory (large)"),
|
|
83
|
+
"sentence-transformers": ("sentence_transformers","sentence-transformers>=2.2.0","heavy", "local embeddings (large)"),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Environment flags
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def auto_install_enabled() -> bool:
|
|
92
|
+
"""Whether the lazy on-demand auto-install path is active.
|
|
93
|
+
|
|
94
|
+
Set UTIM_AUTO_INSTALL=0 to disable globally.
|
|
95
|
+
Default: enabled.
|
|
96
|
+
"""
|
|
97
|
+
return os.environ.get("UTIM_AUTO_INSTALL", "1").strip().lower() not in ("0", "false", "no", "off")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def auto_install_prompt_disabled() -> bool:
|
|
101
|
+
"""Whether to skip the user-facing install spinner / message.
|
|
102
|
+
|
|
103
|
+
Set UTIM_AUTO_INSTALL_PROMPT=0 (or run with --quiet) to suppress output.
|
|
104
|
+
Default: show output.
|
|
105
|
+
"""
|
|
106
|
+
return os.environ.get("UTIM_AUTO_INSTALL_PROMPT", "1").strip().lower() in ("0", "false", "no", "off")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_offline() -> bool:
|
|
110
|
+
return os.environ.get("UTIM_OFFLINE", "0").strip().lower() in ("1", "true", "yes")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Detection
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def is_installed(import_name: str) -> bool:
|
|
118
|
+
"""Return True if the given module can be imported."""
|
|
119
|
+
try:
|
|
120
|
+
return importlib.util.find_spec(import_name) is not None
|
|
121
|
+
except (ValueError, ModuleNotFoundError):
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_missing_core() -> List[Tuple[str, str, str]]:
|
|
126
|
+
"""Return list of (pip_name, import_name, feature_label) for missing core deps."""
|
|
127
|
+
missing = []
|
|
128
|
+
for pip_name, (import_name, _spec, kind, label) in DEPENDENCY_MANIFEST.items():
|
|
129
|
+
if kind == "core" and not is_installed(import_name):
|
|
130
|
+
missing.append((pip_name, import_name, label))
|
|
131
|
+
return missing
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_missing_optional() -> List[Tuple[str, str, str]]:
|
|
135
|
+
"""Return list of (pip_name, import_name, feature_label) for missing optional deps."""
|
|
136
|
+
missing = []
|
|
137
|
+
for pip_name, (import_name, _spec, kind, label) in DEPENDENCY_MANIFEST.items():
|
|
138
|
+
if kind == "optional" and not is_installed(import_name):
|
|
139
|
+
missing.append((pip_name, import_name, label))
|
|
140
|
+
return missing
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_missing_heavy() -> List[Tuple[str, str, str]]:
|
|
144
|
+
"""Return list of (pip_name, import_name, feature_label) for missing heavy deps."""
|
|
145
|
+
missing = []
|
|
146
|
+
for pip_name, (import_name, _spec, kind, label) in DEPENDENCY_MANIFEST.items():
|
|
147
|
+
if kind == "heavy" and not is_installed(import_name):
|
|
148
|
+
missing.append((pip_name, import_name, label))
|
|
149
|
+
return missing
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Installer
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
_INSTALL_LOCK = threading.Lock()
|
|
157
|
+
# Cache: import_name -> module (so we don't reinstall or re-import twice)
|
|
158
|
+
_LAZY_CACHE: Dict[str, Optional[object]] = {}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _print_status(msg: str) -> None:
|
|
162
|
+
"""Print a status line, respecting the quiet flag."""
|
|
163
|
+
if auto_install_prompt_disabled():
|
|
164
|
+
return
|
|
165
|
+
try:
|
|
166
|
+
print(msg, file=sys.stderr, flush=True)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _run_pip_install(packages: List[str]) -> Tuple[bool, str]:
|
|
172
|
+
"""Run `pip install` for the given packages. Returns (success, output)."""
|
|
173
|
+
if is_offline():
|
|
174
|
+
return False, "UTIM_OFFLINE=1 — skipping network install"
|
|
175
|
+
|
|
176
|
+
cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check",
|
|
177
|
+
"--no-input", "--quiet"] + packages
|
|
178
|
+
try:
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
cmd,
|
|
181
|
+
capture_output=True,
|
|
182
|
+
text=True,
|
|
183
|
+
timeout=300, # 5 min ceiling — chromadb can be slow
|
|
184
|
+
)
|
|
185
|
+
if result.returncode == 0:
|
|
186
|
+
return True, (result.stdout or "").strip()
|
|
187
|
+
return False, ((result.stderr or result.stdout) or "").strip()
|
|
188
|
+
except subprocess.TimeoutExpired:
|
|
189
|
+
return False, "pip install timed out after 300s"
|
|
190
|
+
except Exception as exc: # pragma: no cover
|
|
191
|
+
return False, f"pip install failed to launch: {exc}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def install_packages(packages: List[str], label: str = "") -> List[str]:
|
|
195
|
+
"""Install a list of pip packages. Returns the list of those that succeeded."""
|
|
196
|
+
if not packages:
|
|
197
|
+
return []
|
|
198
|
+
if label:
|
|
199
|
+
_print_status(f"⏳ UTIM: installing {label} ({', '.join(packages)})…")
|
|
200
|
+
else:
|
|
201
|
+
_print_status(f"⏳ UTIM: installing {', '.join(packages)}…")
|
|
202
|
+
ok, output = _run_pip_install(packages)
|
|
203
|
+
if ok:
|
|
204
|
+
_print_status(f"✅ UTIM: installed {', '.join(packages)}")
|
|
205
|
+
return packages
|
|
206
|
+
# Retry once with --user (helps in some sandboxed Windows installs)
|
|
207
|
+
if "--user" not in packages:
|
|
208
|
+
_print_status(f"⚠️ UTIM: first install attempt failed, retrying with --user…")
|
|
209
|
+
ok2, output2 = _run_pip_install(packages + ["--user"])
|
|
210
|
+
if ok2:
|
|
211
|
+
_print_status(f"✅ UTIM: installed {', '.join(packages)} (user-site)")
|
|
212
|
+
return packages
|
|
213
|
+
output = output2
|
|
214
|
+
_print_status(f"❌ UTIM: failed to install {', '.join(packages)}: {output[:300]}")
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def ensure_core_deps(verbose: bool = True) -> bool:
|
|
219
|
+
"""Make sure every `core` dependency is installed. Returns True if all OK.
|
|
220
|
+
|
|
221
|
+
Called once on CLI startup. If anything is missing, auto-installs it
|
|
222
|
+
silently (with a single status line) and re-imports the affected modules.
|
|
223
|
+
"""
|
|
224
|
+
if not auto_install_enabled():
|
|
225
|
+
# User has disabled auto-install. Just warn.
|
|
226
|
+
missing = get_missing_core()
|
|
227
|
+
if missing and verbose:
|
|
228
|
+
for pip_name, import_name, label in missing:
|
|
229
|
+
_print_status(
|
|
230
|
+
f"❌ UTIM core dependency missing: {pip_name} ({label}). "
|
|
231
|
+
f"Run: pip install {pip_name} (or set UTIM_AUTO_INSTALL=1)"
|
|
232
|
+
)
|
|
233
|
+
return not missing
|
|
234
|
+
|
|
235
|
+
missing = get_missing_core()
|
|
236
|
+
if not missing:
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
pip_pkgs = [pip_name for pip_name, _imp, _lbl in missing]
|
|
240
|
+
label = "core UTIM dependencies"
|
|
241
|
+
installed = install_packages(pip_pkgs, label=label)
|
|
242
|
+
if not installed:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Wipe import caches so the next `import` re-loads the freshly installed modules
|
|
246
|
+
for _pip_name, import_name, _label in missing:
|
|
247
|
+
# Remove from sys.modules so subsequent imports re-resolve
|
|
248
|
+
for mod_key in list(sys.modules.keys()):
|
|
249
|
+
if mod_key == import_name or mod_key.startswith(import_name + "."):
|
|
250
|
+
sys.modules.pop(mod_key, None)
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
# Lazy on-demand import
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
def safe_import(
|
|
259
|
+
import_name: str,
|
|
260
|
+
pip_name: Optional[str] = None,
|
|
261
|
+
feature_label: str = "",
|
|
262
|
+
*,
|
|
263
|
+
heavy: bool = False,
|
|
264
|
+
):
|
|
265
|
+
"""Import a module, auto-installing the dependency on first use if needed.
|
|
266
|
+
|
|
267
|
+
This is the *replacement* for every bare `try: import X except ImportError: pass`
|
|
268
|
+
in the codebase. Usage:
|
|
269
|
+
|
|
270
|
+
from utim_cli.dependency_manager import safe_import
|
|
271
|
+
chromadb = safe_import("chromadb", pip_name="chromadb",
|
|
272
|
+
feature_label="RAG vector memory")
|
|
273
|
+
if chromadb is None:
|
|
274
|
+
return # feature unavailable, fail gracefully
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
import_name: module name to import (e.g. "PIL").
|
|
278
|
+
pip_name: pip package name (defaults to import_name).
|
|
279
|
+
feature_label: human-readable description used in status output.
|
|
280
|
+
heavy: if True, never auto-install even if the dep is missing —
|
|
281
|
+
these are user-opt-in only (chromadb, sentence-transformers).
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The imported module, or None if the import (or install) failed.
|
|
285
|
+
"""
|
|
286
|
+
if not import_name:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
if import_name in _LAZY_CACHE:
|
|
290
|
+
return _LAZY_CACHE[import_name]
|
|
291
|
+
|
|
292
|
+
# Fast path: already installed
|
|
293
|
+
if is_installed(import_name):
|
|
294
|
+
try:
|
|
295
|
+
mod = importlib.import_module(import_name)
|
|
296
|
+
_LAZY_CACHE[import_name] = mod
|
|
297
|
+
return mod
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
_print_status(f"⚠️ UTIM: import of {import_name} failed: {exc}")
|
|
300
|
+
_LAZY_CACHE[import_name] = None
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
# Not installed. Decide what to do.
|
|
304
|
+
if heavy or not auto_install_enabled():
|
|
305
|
+
# Either a heavy dep (never auto-install) or auto-install is disabled.
|
|
306
|
+
# Fail loudly so the user knows.
|
|
307
|
+
pip = pip_name or import_name
|
|
308
|
+
if not auto_install_prompt_disabled():
|
|
309
|
+
verb = "Run" if heavy or not auto_install_enabled() else "Run"
|
|
310
|
+
install_cmd = f"pip install {pip}"
|
|
311
|
+
if import_name in ("chromadb", "sentence_transformers"):
|
|
312
|
+
install_cmd = f'pip install "utim-cli[memory]"'
|
|
313
|
+
elif import_name in ("PIL",):
|
|
314
|
+
install_cmd = f'pip install "utim-cli[images]"'
|
|
315
|
+
elif import_name in ("mcp",):
|
|
316
|
+
install_cmd = f'pip install "utim-cli[mcp]"'
|
|
317
|
+
elif import_name in ("openai",):
|
|
318
|
+
install_cmd = f'pip install "utim-cli[legacy-agent]"'
|
|
319
|
+
elif import_name in ("tree_sitter",):
|
|
320
|
+
install_cmd = f'pip install "utim-cli[parsers]"'
|
|
321
|
+
elif import_name in ("scrapy", "scrapy_playwright", "bs4"):
|
|
322
|
+
install_cmd = f'pip install "utim-cli[search]"'
|
|
323
|
+
_print_status(
|
|
324
|
+
f"⚠️ UTIM: optional dependency '{pip}' is not installed "
|
|
325
|
+
f"(needed for {feature_label or import_name}). {install_cmd}"
|
|
326
|
+
)
|
|
327
|
+
_LAZY_CACHE[import_name] = None
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
# Auto-install the missing dep, then retry the import.
|
|
331
|
+
pip = pip_name or import_name
|
|
332
|
+
# Look up spec from manifest if we can, else use a permissive spec
|
|
333
|
+
spec = pip
|
|
334
|
+
label = feature_label or import_name
|
|
335
|
+
if pip in DEPENDENCY_MANIFEST:
|
|
336
|
+
spec = DEPENDENCY_MANIFEST[pip][1]
|
|
337
|
+
else:
|
|
338
|
+
# Unknown package — use the import_name as a permissive spec
|
|
339
|
+
spec = pip
|
|
340
|
+
|
|
341
|
+
with _INSTALL_LOCK:
|
|
342
|
+
# Re-check inside the lock in case another thread installed it
|
|
343
|
+
if is_installed(import_name):
|
|
344
|
+
try:
|
|
345
|
+
mod = importlib.import_module(import_name)
|
|
346
|
+
_LAZY_CACHE[import_name] = mod
|
|
347
|
+
return mod
|
|
348
|
+
except Exception:
|
|
349
|
+
_LAZY_CACHE[import_name] = None
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
installed = install_packages([spec], label=label)
|
|
353
|
+
if not installed:
|
|
354
|
+
_LAZY_CACHE[import_name] = None
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
# Wipe cached submodules so the new import is fresh
|
|
358
|
+
for mod_key in list(sys.modules.keys()):
|
|
359
|
+
if mod_key == import_name or mod_key.startswith(import_name + "."):
|
|
360
|
+
sys.modules.pop(mod_key, None)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
mod = importlib.import_module(import_name)
|
|
364
|
+
_LAZY_CACHE[import_name] = mod
|
|
365
|
+
return mod
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
_print_status(f"⚠️ UTIM: import of {import_name} failed after install: {exc}")
|
|
368
|
+
_LAZY_CACHE[import_name] = None
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# Doctor self-heal entry point
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
def self_heal(include_optional: bool = True, include_heavy: bool = False) -> Dict[str, List[str]]:
|
|
377
|
+
"""Install every missing dep. Returns a dict of {kind: [installed packages]}."""
|
|
378
|
+
results: Dict[str, List[str]] = {"core": [], "optional": [], "heavy": []}
|
|
379
|
+
|
|
380
|
+
core_missing = get_missing_core()
|
|
381
|
+
if core_missing:
|
|
382
|
+
pkgs = [p for p, _i, _l in core_missing]
|
|
383
|
+
results["core"] = install_packages(pkgs, label="core UTIM dependencies")
|
|
384
|
+
|
|
385
|
+
if include_optional:
|
|
386
|
+
opt_missing = get_missing_optional()
|
|
387
|
+
if opt_missing:
|
|
388
|
+
pkgs = [p for p, _i, _l in opt_missing]
|
|
389
|
+
results["optional"] = install_packages(pkgs, label="optional feature dependencies")
|
|
390
|
+
|
|
391
|
+
if include_heavy:
|
|
392
|
+
heavy_missing = get_missing_heavy()
|
|
393
|
+
if heavy_missing:
|
|
394
|
+
pkgs = [p for p, _i, _l in heavy_missing]
|
|
395
|
+
results["heavy"] = install_packages(pkgs, label="heavy ML dependencies")
|
|
396
|
+
|
|
397
|
+
# Clear caches so newly installed modules are picked up
|
|
398
|
+
for entry in DEPENDENCY_MANIFEST.values():
|
|
399
|
+
import_name = entry[0]
|
|
400
|
+
for mod_key in list(sys.modules.keys()):
|
|
401
|
+
if mod_key == import_name or mod_key.startswith(import_name + "."):
|
|
402
|
+
sys.modules.pop(mod_key, None)
|
|
403
|
+
_LAZY_CACHE.clear()
|
|
404
|
+
return results
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def diagnostic_report() -> str:
|
|
408
|
+
"""Plain-text report used by `utim doctor`."""
|
|
409
|
+
lines = []
|
|
410
|
+
for kind in ("core", "optional", "heavy"):
|
|
411
|
+
bucket = {
|
|
412
|
+
"core": [(k, v) for k, v in DEPENDENCY_MANIFEST.items() if v[2] == "core"],
|
|
413
|
+
"optional": [(k, v) for k, v in DEPENDENCY_MANIFEST.items() if v[2] == "optional"],
|
|
414
|
+
"heavy": [(k, v) for k, v in DEPENDENCY_MANIFEST.items() if v[2] == "heavy"],
|
|
415
|
+
}[kind]
|
|
416
|
+
installed = sum(1 for _k, (_i, _s, _k2, _l) in bucket if is_installed(_i))
|
|
417
|
+
total = len(bucket)
|
|
418
|
+
lines.append(f"{kind.upper():9s} {installed}/{total} installed")
|
|
419
|
+
return "\n".join(lines)
|