kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/ui/modal_state_manager.py
CHANGED
|
@@ -72,6 +72,9 @@ class ModalStateManager:
|
|
|
72
72
|
self.saved_snapshot: Optional[TerminalSnapshot] = None
|
|
73
73
|
self.current_layout: Optional[ModalLayout] = None
|
|
74
74
|
self.modal_content_cache: List[str] = []
|
|
75
|
+
# Track max dimensions seen during this modal session for proper clearing
|
|
76
|
+
self._max_rendered_height: int = 0
|
|
77
|
+
self._max_rendered_width: int = 0
|
|
75
78
|
|
|
76
79
|
def _strip_ansi(self, text: str) -> str:
|
|
77
80
|
"""Remove ANSI escape codes from text.
|
|
@@ -141,7 +144,18 @@ class ModalStateManager:
|
|
|
141
144
|
# Cache content for refresh operations
|
|
142
145
|
self.modal_content_cache = content_lines.copy()
|
|
143
146
|
|
|
144
|
-
#
|
|
147
|
+
# Track max dimensions for proper clearing when content shrinks
|
|
148
|
+
current_height = len(content_lines)
|
|
149
|
+
if current_height > self._max_rendered_height:
|
|
150
|
+
self._max_rendered_height = current_height
|
|
151
|
+
|
|
152
|
+
# Track max visible width (strip ANSI codes for accurate width)
|
|
153
|
+
if content_lines:
|
|
154
|
+
max_visible_width = max(len(self._strip_ansi(line)) for line in content_lines)
|
|
155
|
+
if max_visible_width > self._max_rendered_width:
|
|
156
|
+
self._max_rendered_width = max_visible_width
|
|
157
|
+
|
|
158
|
+
# Clear previous modal content (uses max dimensions to clear all artifacts)
|
|
145
159
|
self._clear_modal_content_area()
|
|
146
160
|
|
|
147
161
|
# Render new content using direct terminal output
|
|
@@ -384,6 +398,10 @@ class ModalStateManager:
|
|
|
384
398
|
def _clear_modal_content_area(self) -> bool:
|
|
385
399
|
"""Clear the modal content area.
|
|
386
400
|
|
|
401
|
+
Uses _max_rendered_height and _max_rendered_width to ensure all
|
|
402
|
+
previously rendered content is cleared, even when content shrinks
|
|
403
|
+
(e.g., scrolling to the bottom of a list).
|
|
404
|
+
|
|
387
405
|
Returns:
|
|
388
406
|
True if area was cleared successfully.
|
|
389
407
|
"""
|
|
@@ -393,8 +411,13 @@ class ModalStateManager:
|
|
|
393
411
|
|
|
394
412
|
layout = self.current_layout
|
|
395
413
|
|
|
414
|
+
# Use max rendered dimensions to clear all possible artifacts
|
|
415
|
+
# This handles the case where content shrinks (e.g., at bottom of list)
|
|
416
|
+
clear_height = max(layout.height, self._max_rendered_height)
|
|
417
|
+
clear_width = max(layout.width, self._max_rendered_width)
|
|
418
|
+
|
|
396
419
|
# Clear each line of the modal area with spaces (overwrite modal content)
|
|
397
|
-
for i in range(
|
|
420
|
+
for i in range(clear_height):
|
|
398
421
|
row = layout.start_row + i + 1 # 1-based row positioning
|
|
399
422
|
col = layout.start_col + 1 # 1-based col positioning
|
|
400
423
|
|
|
@@ -402,10 +425,10 @@ class ModalStateManager:
|
|
|
402
425
|
self.terminal_state.write_raw(f"\033[{row};{col}H")
|
|
403
426
|
|
|
404
427
|
# Write spaces to overwrite modal content for this line
|
|
405
|
-
spaces = " " *
|
|
428
|
+
spaces = " " * clear_width
|
|
406
429
|
self.terminal_state.write_raw(spaces)
|
|
407
430
|
|
|
408
|
-
logger.debug("Modal content area cleared")
|
|
431
|
+
logger.debug(f"Modal content area cleared ({clear_height}x{clear_width})")
|
|
409
432
|
return True
|
|
410
433
|
|
|
411
434
|
except Exception as e:
|
|
@@ -419,6 +442,8 @@ class ModalStateManager:
|
|
|
419
442
|
self.current_layout = None
|
|
420
443
|
self.modal_content_cache = []
|
|
421
444
|
self.display_mode = ModalDisplayMode.OVERLAY
|
|
445
|
+
self._max_rendered_height = 0
|
|
446
|
+
self._max_rendered_width = 0
|
|
422
447
|
|
|
423
448
|
def get_modal_state_info(self) -> Dict[str, Any]:
|
|
424
449
|
"""Get current modal state information.
|
core/ui/widgets/base_widget.py
CHANGED
|
@@ -53,6 +53,13 @@ class BaseWidget(ABC):
|
|
|
53
53
|
Returns:
|
|
54
54
|
Current configuration value for this widget's config path.
|
|
55
55
|
"""
|
|
56
|
+
# First check if widget config has an explicit 'value' or 'current_value' field
|
|
57
|
+
# This is used for form modals with pre-populated data
|
|
58
|
+
if "value" in self.config:
|
|
59
|
+
return self.config["value"]
|
|
60
|
+
if "current_value" in self.config:
|
|
61
|
+
return self.config["current_value"]
|
|
62
|
+
|
|
56
63
|
# Try to get real value from config service
|
|
57
64
|
if self.config_service:
|
|
58
65
|
try:
|
core/updates/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Release update checking system for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to check for new releases via GitHub API,
|
|
4
|
+
cache results, and notify users of available updates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .version_check_service import VersionCheckService, ReleaseInfo
|
|
8
|
+
from .version_comparator import is_newer_version, compare_versions
|
|
9
|
+
|
|
10
|
+
__all__ = ["VersionCheckService", "ReleaseInfo", "is_newer_version", "compare_versions"]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Version check service for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
Checks GitHub Releases API for new versions, caches results in config,
|
|
4
|
+
and provides release information for user notifications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
|
|
15
|
+
from .version_comparator import is_newer_version
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ReleaseInfo:
|
|
22
|
+
"""Information about a GitHub release.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
version: Semantic version number (e.g., "0.5.0")
|
|
26
|
+
tag_name: Git tag name (e.g., "v0.5.0")
|
|
27
|
+
name: Human-readable release name (e.g., "Version 0.5.0")
|
|
28
|
+
url: GitHub release page URL
|
|
29
|
+
is_prerelease: Whether this is a pre-release version
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
version: str
|
|
33
|
+
tag_name: str
|
|
34
|
+
name: str
|
|
35
|
+
url: str
|
|
36
|
+
is_prerelease: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VersionCheckService:
|
|
40
|
+
"""Service for checking GitHub releases and notifying of updates.
|
|
41
|
+
|
|
42
|
+
This service:
|
|
43
|
+
- Fetches latest release info from GitHub API
|
|
44
|
+
- Caches results in config for configurable TTL (default 24 hours)
|
|
45
|
+
- Compares versions using semantic versioning
|
|
46
|
+
- Handles network errors gracefully
|
|
47
|
+
- Never blocks application startup
|
|
48
|
+
|
|
49
|
+
Configuration keys:
|
|
50
|
+
core.updates.check_enabled (bool): Enable update checking
|
|
51
|
+
core.updates.check_interval_hours (int): Cache TTL in hours
|
|
52
|
+
core.updates.github_repo (str): Repository path (owner/repo)
|
|
53
|
+
core.updates.timeout_seconds (int): HTTP request timeout
|
|
54
|
+
core.updates.include_prereleases (bool): Include pre-release versions
|
|
55
|
+
|
|
56
|
+
Cache keys (stored in config):
|
|
57
|
+
core.updates.last_check_timestamp (int): Unix timestamp of last check
|
|
58
|
+
core.updates.cached_latest_version (str): Cached version string
|
|
59
|
+
core.updates.cached_release_url (str): Cached release URL
|
|
60
|
+
core.updates.cached_release_name (str): Cached release name
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, config, current_version: str):
|
|
64
|
+
"""Initialize version check service.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config: ConfigService instance for settings and cache storage
|
|
68
|
+
current_version: Current application version string
|
|
69
|
+
"""
|
|
70
|
+
self.config = config
|
|
71
|
+
self.current_version = current_version
|
|
72
|
+
|
|
73
|
+
# Load configuration with defaults
|
|
74
|
+
self.check_enabled = config.get("core.updates.check_enabled", True)
|
|
75
|
+
self.check_interval_hours = config.get("core.updates.check_interval_hours", 24)
|
|
76
|
+
self.github_repo = config.get(
|
|
77
|
+
"core.updates.github_repo", "kollaborai/kollabor-cli"
|
|
78
|
+
)
|
|
79
|
+
self.timeout_seconds = config.get("core.updates.timeout_seconds", 5)
|
|
80
|
+
self.include_prereleases = config.get(
|
|
81
|
+
"core.updates.include_prereleases", False
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# HTTP session (initialized in initialize())
|
|
85
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
86
|
+
|
|
87
|
+
logger.debug(
|
|
88
|
+
f"VersionCheckService initialized: "
|
|
89
|
+
f"enabled={self.check_enabled}, "
|
|
90
|
+
f"interval={self.check_interval_hours}h, "
|
|
91
|
+
f"repo={self.github_repo}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def initialize(self) -> None:
|
|
95
|
+
"""Initialize HTTP session and load cached data.
|
|
96
|
+
|
|
97
|
+
Creates aiohttp ClientSession with configured timeout and headers.
|
|
98
|
+
"""
|
|
99
|
+
if not self.check_enabled:
|
|
100
|
+
logger.debug("Update checking disabled in config")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Create HTTP session with timeout
|
|
104
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout_seconds)
|
|
105
|
+
|
|
106
|
+
self.session = aiohttp.ClientSession(
|
|
107
|
+
timeout=timeout,
|
|
108
|
+
headers={
|
|
109
|
+
"Accept": "application/vnd.github+json",
|
|
110
|
+
"User-Agent": f"Kollabor-CLI/{self.current_version}",
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logger.debug("VersionCheckService session initialized")
|
|
115
|
+
|
|
116
|
+
async def check_for_updates(self) -> Optional[ReleaseInfo]:
|
|
117
|
+
"""Check if a newer version is available.
|
|
118
|
+
|
|
119
|
+
Workflow:
|
|
120
|
+
1. Return None if disabled in config
|
|
121
|
+
2. Check cache validity (24h TTL)
|
|
122
|
+
3. If cache valid, return cached data
|
|
123
|
+
4. If cache invalid/missing, fetch from GitHub API
|
|
124
|
+
5. Parse response and compare versions
|
|
125
|
+
6. Update cache if newer version found
|
|
126
|
+
7. Return ReleaseInfo or None
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
ReleaseInfo if newer version available, None otherwise
|
|
130
|
+
|
|
131
|
+
Note:
|
|
132
|
+
All errors are caught and logged as warnings. This method
|
|
133
|
+
never raises exceptions to avoid blocking startup.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
# Check if enabled
|
|
137
|
+
if not self.check_enabled:
|
|
138
|
+
logger.debug("Update checking disabled")
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# Check cache validity
|
|
142
|
+
if self._is_cache_valid():
|
|
143
|
+
logger.debug("Using cached release data")
|
|
144
|
+
cached_release = self._get_cached_release()
|
|
145
|
+
if cached_release and self._is_update_available(cached_release):
|
|
146
|
+
return cached_release
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Fetch from GitHub API
|
|
150
|
+
logger.debug(f"Fetching latest release from GitHub: {self.github_repo}")
|
|
151
|
+
release_data = await self._fetch_latest_release()
|
|
152
|
+
|
|
153
|
+
if not release_data:
|
|
154
|
+
# API call failed, try cached data
|
|
155
|
+
logger.debug("API call failed, attempting to use cached data")
|
|
156
|
+
cached_release = self._get_cached_release()
|
|
157
|
+
if cached_release and self._is_update_available(cached_release):
|
|
158
|
+
return cached_release
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Parse release data
|
|
162
|
+
release_info = self._parse_release_data(release_data)
|
|
163
|
+
if not release_info:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# Update cache
|
|
167
|
+
self._update_cache(release_info)
|
|
168
|
+
|
|
169
|
+
# Check if update available
|
|
170
|
+
if self._is_update_available(release_info):
|
|
171
|
+
logger.info(f"New version available: {release_info.version}")
|
|
172
|
+
return release_info
|
|
173
|
+
|
|
174
|
+
logger.debug(f"Current version {self.current_version} is up to date")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
# Catch-all for unexpected errors
|
|
179
|
+
logger.warning(f"Unexpected error during update check: {e}")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
async def _fetch_latest_release(self) -> Optional[dict]:
|
|
183
|
+
"""Fetch latest release from GitHub API.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Parsed JSON response or None on error
|
|
187
|
+
"""
|
|
188
|
+
if not self.session:
|
|
189
|
+
logger.warning("HTTP session not initialized")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
url = f"https://api.github.com/repos/{self.github_repo}/releases/latest"
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
async with self.session.get(url) as response:
|
|
196
|
+
if response.status == 200:
|
|
197
|
+
data = await response.json()
|
|
198
|
+
logger.debug(f"GitHub API returned: {data.get('tag_name')}")
|
|
199
|
+
return data
|
|
200
|
+
elif response.status == 404:
|
|
201
|
+
logger.warning(f"Repository {self.github_repo} not found")
|
|
202
|
+
return None
|
|
203
|
+
elif response.status == 403:
|
|
204
|
+
logger.warning("GitHub API rate limit exceeded")
|
|
205
|
+
return None
|
|
206
|
+
else:
|
|
207
|
+
logger.warning(f"GitHub API returned status {response.status}")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
except asyncio.TimeoutError:
|
|
211
|
+
logger.warning(
|
|
212
|
+
f"GitHub API timeout after {self.timeout_seconds}s - using cached data"
|
|
213
|
+
)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
except aiohttp.ClientError as e:
|
|
217
|
+
logger.warning(f"GitHub API error: {e} - using cached data")
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.warning(f"Unexpected error fetching release: {e}")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def _parse_release_data(self, release_json: dict) -> Optional[ReleaseInfo]:
|
|
225
|
+
"""Parse GitHub release API response.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
release_json: Parsed JSON response from GitHub API
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
ReleaseInfo instance or None on error
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
tag_name = release_json["tag_name"]
|
|
235
|
+
name = release_json.get("name", tag_name)
|
|
236
|
+
url = release_json["html_url"]
|
|
237
|
+
is_prerelease = release_json.get("prerelease", False)
|
|
238
|
+
|
|
239
|
+
# Strip 'v' prefix from tag to get version
|
|
240
|
+
version = tag_name.lstrip("v")
|
|
241
|
+
|
|
242
|
+
# Filter pre-releases if configured
|
|
243
|
+
if is_prerelease and not self.include_prereleases:
|
|
244
|
+
logger.debug(f"Skipping pre-release version: {version}")
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
release_info = ReleaseInfo(
|
|
248
|
+
version=version,
|
|
249
|
+
tag_name=tag_name,
|
|
250
|
+
name=name,
|
|
251
|
+
url=url,
|
|
252
|
+
is_prerelease=is_prerelease,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
logger.debug(f"Parsed release info: {version}")
|
|
256
|
+
return release_info
|
|
257
|
+
|
|
258
|
+
except KeyError as e:
|
|
259
|
+
logger.warning(f"Invalid release data: missing field {e}")
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.warning(f"Error parsing release data: {e}")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
def _is_update_available(self, release_info: ReleaseInfo) -> bool:
|
|
267
|
+
"""Check if release is newer than current version.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
release_info: Release information to check
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if release is newer than current version
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
return is_newer_version(self.current_version, release_info.version)
|
|
277
|
+
except ValueError as e:
|
|
278
|
+
logger.warning(f"Version comparison failed: {e}")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def _is_cache_valid(self) -> bool:
|
|
282
|
+
"""Check if cached data is still valid based on TTL.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if cache is valid (age < TTL)
|
|
286
|
+
"""
|
|
287
|
+
last_check = self.config.get("core.updates.last_check_timestamp", 0)
|
|
288
|
+
current_time = int(time.time())
|
|
289
|
+
ttl_seconds = self.check_interval_hours * 3600
|
|
290
|
+
|
|
291
|
+
age_seconds = current_time - last_check
|
|
292
|
+
is_valid = age_seconds < ttl_seconds
|
|
293
|
+
|
|
294
|
+
logger.debug(
|
|
295
|
+
f"Cache validity: age={age_seconds}s, ttl={ttl_seconds}s, valid={is_valid}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return is_valid
|
|
299
|
+
|
|
300
|
+
def _get_cached_release(self) -> Optional[ReleaseInfo]:
|
|
301
|
+
"""Retrieve cached release information from config.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
ReleaseInfo from cache or None if not cached
|
|
305
|
+
"""
|
|
306
|
+
version = self.config.get("core.updates.cached_latest_version")
|
|
307
|
+
if not version:
|
|
308
|
+
logger.debug("No cached release data found")
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
url = self.config.get("core.updates.cached_release_url", "")
|
|
312
|
+
name = self.config.get(
|
|
313
|
+
"core.updates.cached_release_name", f"Version {version}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
release_info = ReleaseInfo(
|
|
317
|
+
version=version,
|
|
318
|
+
tag_name=f"v{version}",
|
|
319
|
+
name=name,
|
|
320
|
+
url=url,
|
|
321
|
+
is_prerelease=False,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
logger.debug(f"Retrieved cached release: {version}")
|
|
325
|
+
return release_info
|
|
326
|
+
|
|
327
|
+
def _update_cache(self, release_info: ReleaseInfo) -> None:
|
|
328
|
+
"""Update cached release information in config.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
release_info: Release information to cache
|
|
332
|
+
"""
|
|
333
|
+
current_time = int(time.time())
|
|
334
|
+
|
|
335
|
+
self.config.set("core.updates.last_check_timestamp", current_time)
|
|
336
|
+
self.config.set("core.updates.cached_latest_version", release_info.version)
|
|
337
|
+
self.config.set("core.updates.cached_release_url", release_info.url)
|
|
338
|
+
self.config.set("core.updates.cached_release_name", release_info.name)
|
|
339
|
+
|
|
340
|
+
logger.debug(
|
|
341
|
+
f"Updated cache with version {release_info.version} at timestamp {current_time}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
async def shutdown(self) -> None:
|
|
345
|
+
"""Close HTTP session and cleanup resources."""
|
|
346
|
+
if self.session:
|
|
347
|
+
await self.session.close()
|
|
348
|
+
logger.debug("VersionCheckService session closed")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Semantic version comparison utilities for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
Provides version comparison logic using the packaging library to handle
|
|
4
|
+
semantic versioning correctly (including pre-releases, build metadata, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from packaging.version import Version, InvalidVersion
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_newer_version(current: str, latest: str) -> bool:
|
|
14
|
+
"""Check if latest version is newer than current version.
|
|
15
|
+
|
|
16
|
+
Uses semantic versioning comparison via the packaging library.
|
|
17
|
+
Handles version prefixes (v0.4.11), pre-releases (0.5.0-beta),
|
|
18
|
+
and build metadata (0.5.0+build123).
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
current: Current version string (e.g., "0.4.11" or "v0.4.11")
|
|
22
|
+
latest: Latest version string from GitHub API
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
True if latest > current, False otherwise
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If version format is invalid
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> is_newer_version("0.4.11", "0.5.0")
|
|
32
|
+
True
|
|
33
|
+
>>> is_newer_version("v0.5.0", "0.4.11")
|
|
34
|
+
False
|
|
35
|
+
>>> is_newer_version("0.5.0-beta", "0.5.0")
|
|
36
|
+
True
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Normalize versions (strip 'v' prefix if present)
|
|
40
|
+
current_clean = current.lstrip("v")
|
|
41
|
+
latest_clean = latest.lstrip("v")
|
|
42
|
+
|
|
43
|
+
# Use packaging.version for robust semantic versioning
|
|
44
|
+
current_ver = Version(current_clean)
|
|
45
|
+
latest_ver = Version(latest_clean)
|
|
46
|
+
|
|
47
|
+
result = latest_ver > current_ver
|
|
48
|
+
|
|
49
|
+
logger.debug(
|
|
50
|
+
f"Version comparison: {current} vs {latest} -> "
|
|
51
|
+
f"latest is newer: {result}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
except InvalidVersion as e:
|
|
57
|
+
error_msg = f"Invalid version format: {e}"
|
|
58
|
+
logger.warning(error_msg)
|
|
59
|
+
raise ValueError(error_msg) from e
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def compare_versions(current: str, latest: str) -> int:
|
|
63
|
+
"""Compare two version strings using semantic versioning.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
current: Current version string
|
|
67
|
+
latest: Latest version string
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
-1 if current < latest
|
|
71
|
+
0 if current == latest
|
|
72
|
+
1 if current > latest
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: If version format is invalid
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> compare_versions("0.4.11", "0.5.0")
|
|
79
|
+
-1
|
|
80
|
+
>>> compare_versions("0.5.0", "0.5.0")
|
|
81
|
+
0
|
|
82
|
+
>>> compare_versions("0.6.0", "0.5.0")
|
|
83
|
+
1
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Normalize versions
|
|
87
|
+
current_clean = current.lstrip("v")
|
|
88
|
+
latest_clean = latest.lstrip("v")
|
|
89
|
+
|
|
90
|
+
current_ver = Version(current_clean)
|
|
91
|
+
latest_ver = Version(latest_clean)
|
|
92
|
+
|
|
93
|
+
if current_ver < latest_ver:
|
|
94
|
+
return -1
|
|
95
|
+
elif current_ver > latest_ver:
|
|
96
|
+
return 1
|
|
97
|
+
else:
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
except InvalidVersion as e:
|
|
101
|
+
error_msg = f"Invalid version format: {e}"
|
|
102
|
+
logger.warning(error_msg)
|
|
103
|
+
raise ValueError(error_msg) from e
|