codepp 0.0.437__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.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"""Interactive terminal UI for browsing and installing remote agent skills.
|
|
2
|
+
|
|
3
|
+
Launched from `/skills install` (wiring may live elsewhere). Provides a
|
|
4
|
+
split-panel prompt_toolkit UI:
|
|
5
|
+
- Left: categories, then skills within a category
|
|
6
|
+
- Right: live details preview for the current selection
|
|
7
|
+
|
|
8
|
+
Installation happens after the TUI exits, with a confirmation prompt via
|
|
9
|
+
`safe_input()`, and uses `download_and_install_skill()` to fetch and extract
|
|
10
|
+
remote ZIPs.
|
|
11
|
+
|
|
12
|
+
This module is intentionally defensive: if the remote catalog isn't available,
|
|
13
|
+
it shows an empty menu and returns False.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from prompt_toolkit.application import Application
|
|
23
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
24
|
+
from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
|
|
25
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
26
|
+
from prompt_toolkit.widgets import Frame
|
|
27
|
+
|
|
28
|
+
from code_puppy.command_line.utils import safe_input
|
|
29
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
30
|
+
from code_puppy.plugins.agent_skills.downloader import download_and_install_skill
|
|
31
|
+
from code_puppy.plugins.agent_skills.installer import InstallResult
|
|
32
|
+
from code_puppy.plugins.agent_skills.skill_catalog import SkillCatalogEntry, catalog
|
|
33
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
PAGE_SIZE = 12
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_skill_installed(skill_id: str) -> bool:
|
|
41
|
+
"""Return True if the skill is already installed locally."""
|
|
42
|
+
|
|
43
|
+
return (Path.home() / ".code_puppy" / "skills" / skill_id / "SKILL.md").is_file()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_bytes(num_bytes: int) -> str:
|
|
47
|
+
"""Format bytes into a human-readable string."""
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
size = float(max(0, int(num_bytes)))
|
|
51
|
+
except Exception:
|
|
52
|
+
return "0 B"
|
|
53
|
+
|
|
54
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
55
|
+
if size < 1024.0 or unit == "GB":
|
|
56
|
+
if unit == "B":
|
|
57
|
+
return f"{int(size)} {unit}"
|
|
58
|
+
return f"{size:.1f} {unit}"
|
|
59
|
+
size /= 1024.0
|
|
60
|
+
return f"{size:.1f} GB"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _wrap_text(text: str, width: int) -> List[str]:
|
|
64
|
+
"""Simple word-wrap for display in the details panel."""
|
|
65
|
+
|
|
66
|
+
if not text:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
words = text.split()
|
|
70
|
+
lines: list[str] = []
|
|
71
|
+
current = ""
|
|
72
|
+
|
|
73
|
+
for word in words:
|
|
74
|
+
if not current:
|
|
75
|
+
current = word
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if len(current) + 1 + len(word) > width:
|
|
79
|
+
lines.append(current)
|
|
80
|
+
current = word
|
|
81
|
+
else:
|
|
82
|
+
current = f"{current} {word}"
|
|
83
|
+
|
|
84
|
+
if current:
|
|
85
|
+
lines.append(current)
|
|
86
|
+
|
|
87
|
+
return lines
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _category_key(category: str) -> str:
|
|
91
|
+
"""Normalize a category string for icon lookup."""
|
|
92
|
+
|
|
93
|
+
return "".join(ch for ch in (category or "").casefold() if ch.isalnum())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SkillsInstallMenu:
|
|
97
|
+
"""Interactive TUI for browsing and installing remote skills."""
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
"""Initialize the skills install menu with catalog data."""
|
|
101
|
+
|
|
102
|
+
self.catalog = catalog
|
|
103
|
+
self.categories: List[str] = []
|
|
104
|
+
self.current_category: Optional[str] = None
|
|
105
|
+
self.current_skills: List[SkillCatalogEntry] = []
|
|
106
|
+
|
|
107
|
+
# State
|
|
108
|
+
self.view_mode = "categories" # categories | skills
|
|
109
|
+
self.selected_category_idx = 0
|
|
110
|
+
self.selected_skill_idx = 0
|
|
111
|
+
self.current_page = 0
|
|
112
|
+
self.result: Optional[str] = None
|
|
113
|
+
self.pending_entry: Optional[SkillCatalogEntry] = None
|
|
114
|
+
|
|
115
|
+
# UI controls
|
|
116
|
+
self.menu_control: Optional[FormattedTextControl] = None
|
|
117
|
+
self.preview_control: Optional[FormattedTextControl] = None
|
|
118
|
+
|
|
119
|
+
self._initialize_catalog()
|
|
120
|
+
|
|
121
|
+
def _initialize_catalog(self) -> None:
|
|
122
|
+
"""Load categories from the remote-backed catalog."""
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self.categories = self.catalog.list_categories() if self.catalog else []
|
|
126
|
+
except Exception as e:
|
|
127
|
+
emit_error(f"Skill catalog not available: {e}")
|
|
128
|
+
self.categories = []
|
|
129
|
+
|
|
130
|
+
def _get_category_icon(self, category: str) -> str:
|
|
131
|
+
"""Return an emoji icon for a skill category name."""
|
|
132
|
+
|
|
133
|
+
icons = {
|
|
134
|
+
"data": "📊",
|
|
135
|
+
"finance": "💰",
|
|
136
|
+
"legal": "⚖️",
|
|
137
|
+
"office": "📄",
|
|
138
|
+
"productmanagement": "📦",
|
|
139
|
+
"sales": "💼",
|
|
140
|
+
"biology": "🧬",
|
|
141
|
+
}
|
|
142
|
+
return icons.get(_category_key(category), "📁")
|
|
143
|
+
|
|
144
|
+
def _get_current_category(self) -> Optional[str]:
|
|
145
|
+
"""Get the currently highlighted category name."""
|
|
146
|
+
|
|
147
|
+
if 0 <= self.selected_category_idx < len(self.categories):
|
|
148
|
+
return self.categories[self.selected_category_idx]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _get_current_skill(self) -> Optional[SkillCatalogEntry]:
|
|
152
|
+
"""Get the currently highlighted skill entry."""
|
|
153
|
+
|
|
154
|
+
if self.view_mode == "skills" and self.current_skills:
|
|
155
|
+
if 0 <= self.selected_skill_idx < len(self.current_skills):
|
|
156
|
+
return self.current_skills[self.selected_skill_idx]
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def _render_navigation_hints(self, lines: List) -> None:
|
|
160
|
+
"""Render keyboard shortcut hints at the bottom."""
|
|
161
|
+
|
|
162
|
+
lines.append(("", "\n"))
|
|
163
|
+
lines.append(("fg:ansibrightblack", " ↑/↓ "))
|
|
164
|
+
lines.append(("", "Navigate "))
|
|
165
|
+
lines.append(("fg:ansibrightblack", "←/→ "))
|
|
166
|
+
lines.append(("", "Page\n"))
|
|
167
|
+
|
|
168
|
+
if self.view_mode == "categories":
|
|
169
|
+
lines.append(("fg:ansigreen", " Enter "))
|
|
170
|
+
lines.append(("", "Browse Skills\n"))
|
|
171
|
+
else:
|
|
172
|
+
lines.append(("fg:ansigreen", " Enter "))
|
|
173
|
+
lines.append(("", "Install Skill\n"))
|
|
174
|
+
lines.append(("fg:ansibrightblack", " Esc/Back "))
|
|
175
|
+
lines.append(("", "Back\n"))
|
|
176
|
+
|
|
177
|
+
lines.append(("fg:ansired", " Ctrl+C "))
|
|
178
|
+
lines.append(("", "Cancel"))
|
|
179
|
+
|
|
180
|
+
def _render_category_list(self) -> List:
|
|
181
|
+
"""Render the left panel with category navigation."""
|
|
182
|
+
|
|
183
|
+
lines = []
|
|
184
|
+
|
|
185
|
+
lines.append(("bold cyan", " 📂 CATEGORIES"))
|
|
186
|
+
lines.append(("", "\n\n"))
|
|
187
|
+
|
|
188
|
+
if not self.categories:
|
|
189
|
+
lines.append(("fg:ansiyellow", " No remote categories available."))
|
|
190
|
+
lines.append(("", "\n"))
|
|
191
|
+
lines.append(
|
|
192
|
+
(
|
|
193
|
+
"fg:ansibrightblack",
|
|
194
|
+
" (Remote catalog unavailable or empty)\n",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
self._render_navigation_hints(lines)
|
|
198
|
+
return lines
|
|
199
|
+
|
|
200
|
+
total_pages = (len(self.categories) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
201
|
+
start_idx = self.current_page * PAGE_SIZE
|
|
202
|
+
end_idx = min(start_idx + PAGE_SIZE, len(self.categories))
|
|
203
|
+
|
|
204
|
+
for i in range(start_idx, end_idx):
|
|
205
|
+
category = self.categories[i]
|
|
206
|
+
is_selected = i == self.selected_category_idx
|
|
207
|
+
icon = self._get_category_icon(category)
|
|
208
|
+
count = 0
|
|
209
|
+
try:
|
|
210
|
+
count = (
|
|
211
|
+
len(self.catalog.get_by_category(category)) if self.catalog else 0
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
count = 0
|
|
215
|
+
|
|
216
|
+
prefix = " > " if is_selected else " "
|
|
217
|
+
label = f"{prefix}{icon} {category} ({count})"
|
|
218
|
+
|
|
219
|
+
if is_selected:
|
|
220
|
+
lines.append(("fg:ansibrightcyan bold", label))
|
|
221
|
+
else:
|
|
222
|
+
lines.append(("fg:ansibrightblack", label))
|
|
223
|
+
lines.append(("", "\n"))
|
|
224
|
+
|
|
225
|
+
lines.append(("", "\n"))
|
|
226
|
+
if total_pages > 1:
|
|
227
|
+
lines.append(
|
|
228
|
+
("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
|
|
229
|
+
)
|
|
230
|
+
lines.append(("", "\n"))
|
|
231
|
+
|
|
232
|
+
self._render_navigation_hints(lines)
|
|
233
|
+
return lines
|
|
234
|
+
|
|
235
|
+
def _render_skill_list(self) -> List:
|
|
236
|
+
"""Render the middle panel with skills in the selected category."""
|
|
237
|
+
|
|
238
|
+
lines = []
|
|
239
|
+
|
|
240
|
+
if not self.current_category:
|
|
241
|
+
lines.append(("fg:ansiyellow", " No category selected."))
|
|
242
|
+
lines.append(("", "\n\n"))
|
|
243
|
+
self._render_navigation_hints(lines)
|
|
244
|
+
return lines
|
|
245
|
+
|
|
246
|
+
icon = self._get_category_icon(self.current_category)
|
|
247
|
+
lines.append(("bold cyan", f" {icon} {self.current_category.upper()}"))
|
|
248
|
+
lines.append(("", "\n\n"))
|
|
249
|
+
|
|
250
|
+
if not self.current_skills:
|
|
251
|
+
lines.append(("fg:ansiyellow", " No skills in this category."))
|
|
252
|
+
lines.append(("", "\n\n"))
|
|
253
|
+
self._render_navigation_hints(lines)
|
|
254
|
+
return lines
|
|
255
|
+
|
|
256
|
+
total_pages = (len(self.current_skills) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
257
|
+
start_idx = self.current_page * PAGE_SIZE
|
|
258
|
+
end_idx = min(start_idx + PAGE_SIZE, len(self.current_skills))
|
|
259
|
+
|
|
260
|
+
for i in range(start_idx, end_idx):
|
|
261
|
+
entry = self.current_skills[i]
|
|
262
|
+
is_selected = i == self.selected_skill_idx
|
|
263
|
+
|
|
264
|
+
installed = is_skill_installed(entry.id)
|
|
265
|
+
status_icon = "✓" if installed else "○"
|
|
266
|
+
status_style = "fg:ansigreen" if installed else "fg:ansibrightblack"
|
|
267
|
+
|
|
268
|
+
prefix = " > " if is_selected else " "
|
|
269
|
+
label = f"{prefix}{status_icon} {entry.display_name}"
|
|
270
|
+
|
|
271
|
+
if is_selected:
|
|
272
|
+
lines.append(("fg:ansibrightcyan bold", label))
|
|
273
|
+
else:
|
|
274
|
+
lines.append((status_style, label))
|
|
275
|
+
|
|
276
|
+
lines.append(("", "\n"))
|
|
277
|
+
|
|
278
|
+
lines.append(("", "\n"))
|
|
279
|
+
if total_pages > 1:
|
|
280
|
+
lines.append(
|
|
281
|
+
("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
|
|
282
|
+
)
|
|
283
|
+
lines.append(("", "\n"))
|
|
284
|
+
|
|
285
|
+
self._render_navigation_hints(lines)
|
|
286
|
+
return lines
|
|
287
|
+
|
|
288
|
+
def _render_details(self) -> List:
|
|
289
|
+
"""Render the right panel with details for the selected skill."""
|
|
290
|
+
|
|
291
|
+
lines = []
|
|
292
|
+
|
|
293
|
+
lines.append(("bold cyan", " 📋 DETAILS"))
|
|
294
|
+
lines.append(("", "\n\n"))
|
|
295
|
+
|
|
296
|
+
if self.view_mode == "categories":
|
|
297
|
+
category = self._get_current_category()
|
|
298
|
+
if not category:
|
|
299
|
+
lines.append(("fg:ansiyellow", " No category selected."))
|
|
300
|
+
return lines
|
|
301
|
+
|
|
302
|
+
icon = self._get_category_icon(category)
|
|
303
|
+
lines.append(("bold", f" {icon} {category}"))
|
|
304
|
+
lines.append(("", "\n\n"))
|
|
305
|
+
|
|
306
|
+
skills = []
|
|
307
|
+
try:
|
|
308
|
+
skills = self.catalog.get_by_category(category) if self.catalog else []
|
|
309
|
+
except Exception:
|
|
310
|
+
skills = []
|
|
311
|
+
|
|
312
|
+
lines.append(("fg:ansibrightblack", f" {len(skills)} skills available"))
|
|
313
|
+
lines.append(("", "\n\n"))
|
|
314
|
+
|
|
315
|
+
# Show a preview of the first few skills
|
|
316
|
+
if skills:
|
|
317
|
+
lines.append(("bold", " Preview:"))
|
|
318
|
+
lines.append(("", "\n"))
|
|
319
|
+
for entry in skills[:6]:
|
|
320
|
+
lines.append(("fg:ansibrightblack", f" • {entry.display_name}"))
|
|
321
|
+
lines.append(("", "\n"))
|
|
322
|
+
|
|
323
|
+
return lines
|
|
324
|
+
|
|
325
|
+
entry = self._get_current_skill()
|
|
326
|
+
if not entry:
|
|
327
|
+
lines.append(("fg:ansiyellow", " No skill selected."))
|
|
328
|
+
return lines
|
|
329
|
+
|
|
330
|
+
installed = is_skill_installed(entry.id)
|
|
331
|
+
installed_text = "Installed" if installed else "Not installed"
|
|
332
|
+
installed_style = "fg:ansigreen" if installed else "fg:ansiyellow"
|
|
333
|
+
|
|
334
|
+
lines.append(("bold", f" {entry.display_name}"))
|
|
335
|
+
lines.append(("", "\n"))
|
|
336
|
+
lines.append((installed_style, f" {installed_text}"))
|
|
337
|
+
lines.append(("", "\n\n"))
|
|
338
|
+
|
|
339
|
+
lines.append(("bold", " ID:"))
|
|
340
|
+
lines.append(("", "\n"))
|
|
341
|
+
lines.append(("fg:ansibrightblack", f" {entry.id}"))
|
|
342
|
+
lines.append(("", "\n\n"))
|
|
343
|
+
|
|
344
|
+
lines.append(("bold", " Description:"))
|
|
345
|
+
lines.append(("", "\n"))
|
|
346
|
+
desc = entry.description or "No description available"
|
|
347
|
+
for line in _wrap_text(desc, 56):
|
|
348
|
+
lines.append(("fg:ansibrightblack", f" {line}"))
|
|
349
|
+
lines.append(("", "\n"))
|
|
350
|
+
lines.append(("", "\n"))
|
|
351
|
+
|
|
352
|
+
lines.append(("bold", " Category:"))
|
|
353
|
+
lines.append(("", "\n"))
|
|
354
|
+
lines.append(("fg:ansibrightblack", f" {entry.category}"))
|
|
355
|
+
lines.append(("", "\n\n"))
|
|
356
|
+
|
|
357
|
+
lines.append(("bold", " Tags:"))
|
|
358
|
+
lines.append(("", "\n"))
|
|
359
|
+
tags = entry.tags or []
|
|
360
|
+
lines.append(("fg:ansicyan", f" {', '.join(tags) if tags else '(none)'}"))
|
|
361
|
+
lines.append(("", "\n\n"))
|
|
362
|
+
|
|
363
|
+
lines.append(("bold", " Contents:"))
|
|
364
|
+
lines.append(("", "\n"))
|
|
365
|
+
lines.append(
|
|
366
|
+
(
|
|
367
|
+
"fg:ansibrightblack",
|
|
368
|
+
f" scripts: {'yes' if entry.has_scripts else 'no'}",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
lines.append(("", "\n"))
|
|
372
|
+
lines.append(
|
|
373
|
+
(
|
|
374
|
+
"fg:ansibrightblack",
|
|
375
|
+
f" references: {'yes' if entry.has_references else 'no'}",
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
lines.append(("", "\n"))
|
|
379
|
+
lines.append(("fg:ansibrightblack", f" files: {entry.file_count}"))
|
|
380
|
+
lines.append(("", "\n\n"))
|
|
381
|
+
|
|
382
|
+
lines.append(("bold", " Download:"))
|
|
383
|
+
lines.append(("", "\n"))
|
|
384
|
+
lines.append(
|
|
385
|
+
(
|
|
386
|
+
"fg:ansibrightblack",
|
|
387
|
+
f" size: {_format_bytes(entry.zip_size_bytes)}",
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
lines.append(("", "\n"))
|
|
391
|
+
lines.append(("fg:ansibrightblack", f" url: {entry.download_url}"))
|
|
392
|
+
lines.append(("", "\n"))
|
|
393
|
+
|
|
394
|
+
return lines
|
|
395
|
+
|
|
396
|
+
def update_display(self) -> None:
|
|
397
|
+
"""Refresh all three panels of the TUI display."""
|
|
398
|
+
|
|
399
|
+
if self.view_mode == "categories":
|
|
400
|
+
self.menu_control.text = self._render_category_list()
|
|
401
|
+
else:
|
|
402
|
+
self.menu_control.text = self._render_skill_list()
|
|
403
|
+
|
|
404
|
+
self.preview_control.text = self._render_details()
|
|
405
|
+
|
|
406
|
+
def _enter_category(self) -> None:
|
|
407
|
+
"""Enter the currently highlighted category to browse skills."""
|
|
408
|
+
|
|
409
|
+
category = self._get_current_category()
|
|
410
|
+
if not category or not self.catalog:
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
self.current_category = category
|
|
414
|
+
try:
|
|
415
|
+
self.current_skills = self.catalog.get_by_category(category)
|
|
416
|
+
except Exception:
|
|
417
|
+
self.current_skills = []
|
|
418
|
+
|
|
419
|
+
self.view_mode = "skills"
|
|
420
|
+
self.selected_skill_idx = 0
|
|
421
|
+
self.current_page = 0
|
|
422
|
+
self.update_display()
|
|
423
|
+
|
|
424
|
+
def _go_back_to_categories(self) -> None:
|
|
425
|
+
"""Navigate back from skill list to category list."""
|
|
426
|
+
|
|
427
|
+
self.view_mode = "categories"
|
|
428
|
+
self.current_category = None
|
|
429
|
+
self.current_skills = []
|
|
430
|
+
self.selected_skill_idx = 0
|
|
431
|
+
self.current_page = 0
|
|
432
|
+
self.update_display()
|
|
433
|
+
|
|
434
|
+
def _select_current_skill(self) -> None:
|
|
435
|
+
"""Download and install the currently highlighted skill."""
|
|
436
|
+
|
|
437
|
+
entry = self._get_current_skill()
|
|
438
|
+
if entry:
|
|
439
|
+
self.pending_entry = entry
|
|
440
|
+
self.result = "pending_install"
|
|
441
|
+
|
|
442
|
+
def run(self) -> bool:
|
|
443
|
+
"""Run the skills install menu.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
True if a skill was installed, False otherwise.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
# Build UI
|
|
450
|
+
self.menu_control = FormattedTextControl(text="")
|
|
451
|
+
self.preview_control = FormattedTextControl(text="")
|
|
452
|
+
|
|
453
|
+
menu_window = Window(
|
|
454
|
+
content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
|
|
455
|
+
)
|
|
456
|
+
preview_window = Window(
|
|
457
|
+
content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Browse")
|
|
461
|
+
preview_frame = Frame(
|
|
462
|
+
preview_window, width=Dimension(weight=65), title="Details"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
root_container = VSplit([menu_frame, preview_frame])
|
|
466
|
+
|
|
467
|
+
kb = KeyBindings()
|
|
468
|
+
|
|
469
|
+
@kb.add("up")
|
|
470
|
+
def _(event):
|
|
471
|
+
"""Move cursor up."""
|
|
472
|
+
|
|
473
|
+
if self.view_mode == "categories":
|
|
474
|
+
if self.selected_category_idx > 0:
|
|
475
|
+
self.selected_category_idx -= 1
|
|
476
|
+
self.current_page = self.selected_category_idx // PAGE_SIZE
|
|
477
|
+
else:
|
|
478
|
+
if self.selected_skill_idx > 0:
|
|
479
|
+
self.selected_skill_idx -= 1
|
|
480
|
+
self.current_page = self.selected_skill_idx // PAGE_SIZE
|
|
481
|
+
self.update_display()
|
|
482
|
+
|
|
483
|
+
@kb.add("down")
|
|
484
|
+
def _(event):
|
|
485
|
+
"""Move cursor down."""
|
|
486
|
+
|
|
487
|
+
if self.view_mode == "categories":
|
|
488
|
+
if self.selected_category_idx < len(self.categories) - 1:
|
|
489
|
+
self.selected_category_idx += 1
|
|
490
|
+
self.current_page = self.selected_category_idx // PAGE_SIZE
|
|
491
|
+
else:
|
|
492
|
+
if self.selected_skill_idx < len(self.current_skills) - 1:
|
|
493
|
+
self.selected_skill_idx += 1
|
|
494
|
+
self.current_page = self.selected_skill_idx // PAGE_SIZE
|
|
495
|
+
self.update_display()
|
|
496
|
+
|
|
497
|
+
@kb.add("left")
|
|
498
|
+
def _(event):
|
|
499
|
+
"""Navigate to previous page."""
|
|
500
|
+
|
|
501
|
+
if self.current_page > 0:
|
|
502
|
+
self.current_page -= 1
|
|
503
|
+
if self.view_mode == "categories":
|
|
504
|
+
self.selected_category_idx = self.current_page * PAGE_SIZE
|
|
505
|
+
else:
|
|
506
|
+
self.selected_skill_idx = self.current_page * PAGE_SIZE
|
|
507
|
+
self.update_display()
|
|
508
|
+
|
|
509
|
+
@kb.add("right")
|
|
510
|
+
def _(event):
|
|
511
|
+
"""Navigate to next page."""
|
|
512
|
+
|
|
513
|
+
if self.view_mode == "categories":
|
|
514
|
+
total_items = len(self.categories)
|
|
515
|
+
else:
|
|
516
|
+
total_items = len(self.current_skills)
|
|
517
|
+
|
|
518
|
+
total_pages = (total_items + PAGE_SIZE - 1) // PAGE_SIZE
|
|
519
|
+
if self.current_page < total_pages - 1:
|
|
520
|
+
self.current_page += 1
|
|
521
|
+
if self.view_mode == "categories":
|
|
522
|
+
self.selected_category_idx = self.current_page * PAGE_SIZE
|
|
523
|
+
else:
|
|
524
|
+
self.selected_skill_idx = self.current_page * PAGE_SIZE
|
|
525
|
+
self.update_display()
|
|
526
|
+
|
|
527
|
+
@kb.add("enter")
|
|
528
|
+
def _(event):
|
|
529
|
+
"""Select/enter the current item."""
|
|
530
|
+
|
|
531
|
+
if self.view_mode == "categories":
|
|
532
|
+
self._enter_category()
|
|
533
|
+
else:
|
|
534
|
+
self._select_current_skill()
|
|
535
|
+
event.app.exit()
|
|
536
|
+
|
|
537
|
+
@kb.add("escape")
|
|
538
|
+
def _(event):
|
|
539
|
+
"""Go back."""
|
|
540
|
+
|
|
541
|
+
if self.view_mode == "skills":
|
|
542
|
+
self._go_back_to_categories()
|
|
543
|
+
|
|
544
|
+
@kb.add("backspace")
|
|
545
|
+
def _(event):
|
|
546
|
+
"""Go back."""
|
|
547
|
+
|
|
548
|
+
if self.view_mode == "skills":
|
|
549
|
+
self._go_back_to_categories()
|
|
550
|
+
|
|
551
|
+
@kb.add("c-c")
|
|
552
|
+
def _(event):
|
|
553
|
+
"""Quit the menu."""
|
|
554
|
+
|
|
555
|
+
event.app.exit()
|
|
556
|
+
|
|
557
|
+
layout = Layout(root_container)
|
|
558
|
+
app = Application(
|
|
559
|
+
layout=layout,
|
|
560
|
+
key_bindings=kb,
|
|
561
|
+
full_screen=False,
|
|
562
|
+
mouse_support=False,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
set_awaiting_user_input(True)
|
|
566
|
+
|
|
567
|
+
# Enter alternate screen buffer
|
|
568
|
+
sys.stdout.write("\033[?1049h")
|
|
569
|
+
sys.stdout.write("\033[2J\033[H")
|
|
570
|
+
sys.stdout.flush()
|
|
571
|
+
time.sleep(0.05)
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
self.update_display()
|
|
575
|
+
sys.stdout.write("\033[2J\033[H")
|
|
576
|
+
sys.stdout.flush()
|
|
577
|
+
|
|
578
|
+
app.run(in_thread=True)
|
|
579
|
+
|
|
580
|
+
finally:
|
|
581
|
+
sys.stdout.write("\033[?1049l")
|
|
582
|
+
sys.stdout.flush()
|
|
583
|
+
|
|
584
|
+
# Flush any buffered input to prevent stale keypresses
|
|
585
|
+
try:
|
|
586
|
+
import termios
|
|
587
|
+
|
|
588
|
+
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
|
|
589
|
+
except Exception:
|
|
590
|
+
pass # ImportError on Windows, termios.error, or not a tty
|
|
591
|
+
|
|
592
|
+
# Small delay to let terminal settle before any output
|
|
593
|
+
time.sleep(0.1)
|
|
594
|
+
set_awaiting_user_input(False)
|
|
595
|
+
|
|
596
|
+
# Handle install after TUI exits
|
|
597
|
+
if self.result == "pending_install" and self.pending_entry:
|
|
598
|
+
return _prompt_and_install(self.pending_entry)
|
|
599
|
+
|
|
600
|
+
emit_info("✓ Exited skills install browser")
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _prompt_and_install(entry: SkillCatalogEntry) -> bool:
|
|
605
|
+
"""Prompt for confirmation and install the given skill."""
|
|
606
|
+
|
|
607
|
+
installed = is_skill_installed(entry.id)
|
|
608
|
+
size_str = _format_bytes(entry.zip_size_bytes)
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
if installed:
|
|
612
|
+
answer = safe_input(
|
|
613
|
+
f"Skill '{entry.display_name}' is already installed. Reinstall ({size_str})? [y/N] "
|
|
614
|
+
)
|
|
615
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
616
|
+
emit_info("Installation cancelled")
|
|
617
|
+
return False
|
|
618
|
+
force = True
|
|
619
|
+
else:
|
|
620
|
+
answer = safe_input(
|
|
621
|
+
f"Install skill '{entry.display_name}' ({size_str})? [y/N] "
|
|
622
|
+
)
|
|
623
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
624
|
+
emit_info("Installation cancelled")
|
|
625
|
+
return False
|
|
626
|
+
force = False
|
|
627
|
+
|
|
628
|
+
except (KeyboardInterrupt, EOFError):
|
|
629
|
+
emit_warning("Installation cancelled")
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
emit_info(f"Downloading: {entry.display_name} ({size_str})")
|
|
633
|
+
|
|
634
|
+
result: InstallResult
|
|
635
|
+
try:
|
|
636
|
+
result = download_and_install_skill(
|
|
637
|
+
skill_name=entry.id,
|
|
638
|
+
download_url=entry.download_url,
|
|
639
|
+
force=force,
|
|
640
|
+
)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
logger.exception(f"Unexpected error during skill install: {e}")
|
|
643
|
+
emit_error(f"Installation error: {e}")
|
|
644
|
+
return False
|
|
645
|
+
|
|
646
|
+
if result.success:
|
|
647
|
+
emit_success(result.message)
|
|
648
|
+
if result.installed_path:
|
|
649
|
+
emit_info(f"Installed to: {result.installed_path}")
|
|
650
|
+
return True
|
|
651
|
+
|
|
652
|
+
emit_error(result.message)
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def run_skills_install_menu() -> bool:
|
|
657
|
+
"""Run the bundled skills install menu.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
True if a skill was installed, False otherwise.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
menu = SkillsInstallMenu()
|
|
664
|
+
return menu.run()
|