Skip to content

Commit 2103b27

Browse files
authored
Merge pull request #53 from NikiforovAll/feature/plugin-version
✨ feat(marketplace): display plugin versions with update indicators
2 parents 3916169 + c7bd545 commit 2103b27

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

src/lazyclaude/models/marketplace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class MarketplacePlugin:
3636
is_installed: bool = False
3737
is_enabled: bool = True
3838
install_path: Path | None = None
39+
installed_version: str | None = None
3940
extra_metadata: dict[str, Any] = field(default_factory=dict)
4041

4142

src/lazyclaude/services/marketplace_loader.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(
2525
self._installed_plugin_ids: set[str] | None = None
2626
self._enabled_plugin_ids: set[str] | None = None
2727
self._install_paths: dict[str, Path] | None = None
28+
self._installed_versions: dict[str, str] | None = None
2829
self._marketplaces_cache: list[Marketplace] | None = None
2930

3031
def load_marketplaces(self) -> list[Marketplace]:
@@ -120,6 +121,7 @@ def _parse_plugin(
120121
source = source.get("url", str(source))
121122

122123
install_path = (self._install_paths or {}).get(full_id)
124+
installed_version = (self._installed_versions or {}).get(full_id)
123125

124126
return MarketplacePlugin(
125127
name=name,
@@ -130,6 +132,7 @@ def _parse_plugin(
130132
is_installed=is_installed,
131133
is_enabled=is_enabled if is_installed else True,
132134
install_path=install_path,
135+
installed_version=installed_version,
133136
extra_metadata={
134137
k: v
135138
for k, v in data.items()
@@ -169,13 +172,16 @@ def _load_installed_plugins(self) -> None:
169172
| enabled_in_local
170173
)
171174
self._install_paths = {}
175+
self._installed_versions = {}
172176
for pid, installations in registry.installed.items():
173177
if installations:
174178
self._install_paths[pid] = Path(installations[0].install_path)
179+
self._installed_versions[pid] = installations[0].version
175180
else:
176181
self._installed_plugin_ids = set()
177182
self._enabled_plugin_ids = set()
178183
self._install_paths = {}
184+
self._installed_versions = {}
179185

180186
def get_plugin_source_dir(self, plugin: MarketplacePlugin) -> Path | None:
181187
"""Get the source directory for a plugin (installed or from marketplace)."""
@@ -225,6 +231,7 @@ def refresh(self) -> None:
225231
self._installed_plugin_ids = None
226232
self._enabled_plugin_ids = None
227233
self._install_paths = None
234+
self._installed_versions = None
228235
self._marketplaces_cache = None
229236
if self._plugin_loader:
230237
self._plugin_loader._registry = None

src/lazyclaude/widgets/marketplace_modal.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,31 @@ def _get_filtered_marketplaces(self) -> list[Marketplace]:
266266

267267
return filtered
268268

269+
@staticmethod
270+
def _is_semver(version: str | None) -> bool:
271+
"""Check if version string is semver (x.y.z format)."""
272+
if not version:
273+
return False
274+
try:
275+
parts = version.split(".")
276+
return len(parts) >= 2 and all(part.isdigit() for part in parts)
277+
except (ValueError, AttributeError):
278+
return False
279+
280+
@staticmethod
281+
def _parse_version(version_str: str) -> tuple[int, ...]:
282+
"""Parse version string into comparable tuple of integers."""
283+
return tuple(int(part) for part in version_str.split("."))
284+
285+
def _has_update(self, installed: str, available: str) -> bool:
286+
"""Check if available version is newer than installed."""
287+
if not self._is_semver(installed) or not self._is_semver(available):
288+
return False
289+
try:
290+
return self._parse_version(available) > self._parse_version(installed)
291+
except ValueError:
292+
return False
293+
269294
def _render_marketplace_label(self, marketplace: Marketplace) -> str:
270295
"""Render a marketplace node label."""
271296
count = len(marketplace.plugins)
@@ -284,11 +309,24 @@ def _render_plugin_label(self, plugin: MarketplacePlugin) -> str:
284309
else:
285310
status_icon = "[ ]"
286311

312+
version_display = ""
313+
if plugin.is_installed and plugin.installed_version:
314+
installed_ver = plugin.installed_version
315+
available_ver = plugin.extra_metadata.get("version")
316+
317+
if available_ver and self._has_update(installed_ver, available_ver):
318+
version_display = (
319+
f" [dim]({installed_ver}{available_ver})[/] [cyan]↑[/]"
320+
)
321+
else:
322+
version_display = f" [dim]({installed_ver})[/]"
323+
287324
desc = f" - {plugin.description}" if plugin.description else ""
288-
if len(desc) > 90:
289-
desc = desc[:87] + "..."
325+
max_desc_len = 80
326+
if len(desc) > max_desc_len:
327+
desc = desc[: max_desc_len - 3] + "..."
290328

291-
return f"{status_icon} {plugin.name}{desc}"
329+
return f"{status_icon} {plugin.name}{version_display}{desc}"
292330

293331
def action_close_or_cancel(self) -> None:
294332
"""Close filter input or modal."""

0 commit comments

Comments
 (0)