mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
feat(store): read ledmatrix_min_version, aliases and commit from the registry (#686)
The store reads three optional registry fields: ledmatrix_min_version (an incompatible install/update is refused before any download, with a "Needs LEDMatrix X+" card badge), aliases (update/uninstall/reinstall by registry id find a plugin installed under its manifest id, with registry proof only), and commit (shown and linked on the store card). An older plugins.json behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,33 @@ from src.common.path_safety import resolve_under, safe_path_component
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _compatibility_refusal(plugin_id: str) -> Optional[str]:
|
||||
"""Why the store just refused ``plugin_id`` as incompatible, or None.
|
||||
|
||||
The store records the reason under the id it was handed and, for a
|
||||
reinstall, under the registry id it resolved to; both are cleared here.
|
||||
"""
|
||||
store = api_v3.plugin_store_manager
|
||||
ids = [plugin_id]
|
||||
try:
|
||||
entry = store.get_registry_info(plugin_id)
|
||||
except Exception: # noqa: BLE001 - only used to phrase an error
|
||||
entry = None
|
||||
if isinstance(entry, dict) and isinstance(entry.get('id'), str):
|
||||
ids.append(entry['id'])
|
||||
reason = store.pop_refusal(*ids)
|
||||
return reason if isinstance(reason, str) and reason else None
|
||||
|
||||
|
||||
def _store_incompatibility(plugin: dict) -> Optional[str]:
|
||||
"""The pre-download compatibility verdict for a store listing entry."""
|
||||
try:
|
||||
reason = api_v3.plugin_store_manager.registry_incompatibility(plugin.get('id'), plugin)
|
||||
except Exception: # noqa: BLE001 - a listing must not fail over a hint
|
||||
return None
|
||||
return reason if isinstance(reason, str) and reason else None
|
||||
|
||||
|
||||
def _listed_plugin_dir(base: Path, name: str) -> Optional[Path]:
|
||||
"""The entry of ``base`` called ``name``, or None.
|
||||
|
||||
@@ -145,7 +172,9 @@ def update_plugin():
|
||||
remote_commit = remote_info.get('last_commit_sha') if remote_info else None
|
||||
remote_branch = remote_info.get('branch') if remote_info else None
|
||||
|
||||
# Update the plugin
|
||||
# Update the plugin. A refusal left over from an earlier attempt
|
||||
# (say, the automatic updater's) must not explain this one.
|
||||
_compatibility_refusal(plugin_id)
|
||||
success = api_v3.plugin_store_manager.update_plugin(plugin_id)
|
||||
|
||||
if success:
|
||||
@@ -235,7 +264,11 @@ def update_plugin():
|
||||
message=message
|
||||
)
|
||||
else:
|
||||
if plugin_dir is None or not plugin_dir.exists():
|
||||
refusal = _compatibility_refusal(plugin_id)
|
||||
if refusal:
|
||||
# The plugin is untouched; say why rather than "check logs".
|
||||
client_msg = f'Plugin update refused: {refusal}'
|
||||
elif plugin_dir is None or not plugin_dir.exists():
|
||||
client_msg = 'Plugin update failed: plugin not found'
|
||||
else:
|
||||
git_info = api_v3.plugin_store_manager._get_local_git_info(plugin_dir)
|
||||
@@ -264,7 +297,8 @@ def update_plugin():
|
||||
return error_response(
|
||||
ErrorCode.PLUGIN_UPDATE_FAILED,
|
||||
client_msg,
|
||||
status_code=500
|
||||
# A refusal is the plugin's requirement, not a server fault.
|
||||
status_code=409 if refusal else 500
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -406,6 +440,7 @@ def install_plugin():
|
||||
if api_v3.operation_queue:
|
||||
def install_callback(operation):
|
||||
"""Callback to execute plugin installation."""
|
||||
_compatibility_refusal(plugin_id) # clear any stale one
|
||||
success = api_v3.plugin_store_manager.install_plugin(plugin_id, branch=branch)
|
||||
|
||||
if success:
|
||||
@@ -438,8 +473,10 @@ def install_plugin():
|
||||
error_msg = f'Failed to install plugin {plugin_id}'
|
||||
if branch:
|
||||
error_msg += f' (branch: {branch})'
|
||||
plugin_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id)
|
||||
if not plugin_info:
|
||||
refusal = _compatibility_refusal(plugin_id)
|
||||
if refusal:
|
||||
error_msg += f': {refusal}'
|
||||
elif not api_v3.plugin_store_manager.get_plugin_info(plugin_id):
|
||||
error_msg += ' (plugin not found in registry)'
|
||||
|
||||
# Record failure in history
|
||||
@@ -468,6 +505,7 @@ def install_plugin():
|
||||
)
|
||||
else:
|
||||
# Fallback to direct installation
|
||||
_compatibility_refusal(plugin_id) # clear any stale one
|
||||
success = api_v3.plugin_store_manager.install_plugin(plugin_id, branch=branch)
|
||||
|
||||
if success:
|
||||
@@ -493,8 +531,10 @@ def install_plugin():
|
||||
error_msg = f'Failed to install plugin {plugin_id}'
|
||||
if branch:
|
||||
error_msg += f' (branch: {branch})'
|
||||
plugin_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id)
|
||||
if not plugin_info:
|
||||
refusal = _compatibility_refusal(plugin_id)
|
||||
if refusal:
|
||||
error_msg += f': {refusal}'
|
||||
elif not api_v3.plugin_store_manager.get_plugin_info(plugin_id):
|
||||
error_msg += ' (plugin not found in registry)'
|
||||
|
||||
if api_v3.operation_history:
|
||||
@@ -509,7 +549,7 @@ def install_plugin():
|
||||
return error_response(
|
||||
ErrorCode.PLUGIN_INSTALL_FAILED,
|
||||
error_msg,
|
||||
status_code=500
|
||||
status_code=409 if refusal else 500
|
||||
)
|
||||
|
||||
|
||||
@@ -719,7 +759,17 @@ def list_plugin_store():
|
||||
'version': plugin.get('latest_version') or plugin.get('version', ''),
|
||||
'branch': plugin.get('branch') or plugin.get('default_branch'),
|
||||
'default_branch': plugin.get('default_branch'),
|
||||
'plugin_path': plugin.get('plugin_path', '')
|
||||
'plugin_path': plugin.get('plugin_path', ''),
|
||||
# Registry fields from after 3.7.0; absent (None) in an older
|
||||
# plugins.json. `commit` is the one that introduced `version`.
|
||||
'commit': plugin.get('commit') if isinstance(plugin.get('commit'), str) else None,
|
||||
'ledmatrix_min_version': (plugin.get('ledmatrix_min_version')
|
||||
if isinstance(plugin.get('ledmatrix_min_version'), str) else None),
|
||||
'aliases': [a for a in plugin.get('aliases') or [] if isinstance(a, str)]
|
||||
if isinstance(plugin.get('aliases'), list) else [],
|
||||
# What Install would answer, without trying: the same check the
|
||||
# store runs before downloading.
|
||||
'incompatible_reason': _store_incompatibility(plugin),
|
||||
})
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'plugins': formatted_plugins}})
|
||||
|
||||
@@ -2267,7 +2267,10 @@ function isStorePluginInstalled(pluginIdOrPlugin) {
|
||||
// Derive the actual installed directory name from plugin_path (e.g. "plugins/ledmatrix-weather" → "ledmatrix-weather")
|
||||
const pluginPath = pluginIdOrPlugin.plugin_path || '';
|
||||
const pathDerivedId = pluginPath ? pluginPath.split('/').pop() : null;
|
||||
return installed.some(p => p.id === storeId || (pathDerivedId && p.id === pathDerivedId));
|
||||
// Newer registries also list the other ids outright (the manifest id).
|
||||
const aliases = Array.isArray(pluginIdOrPlugin.aliases) ? pluginIdOrPlugin.aliases : [];
|
||||
return installed.some(p => p.id === storeId || (pathDerivedId && p.id === pathDerivedId)
|
||||
|| aliases.includes(p.id));
|
||||
}
|
||||
|
||||
// ── Plugin Store: search / filter / sort ────────────────────────────────
|
||||
@@ -2425,6 +2428,16 @@ function renderPluginStore(plugins) {
|
||||
const installed = isStorePluginInstalled(plugin);
|
||||
// Registry data: only open real web links, never javascript: URLs.
|
||||
const repoLink = plugin.repo && /^https?:\/\//i.test(plugin.repo) ? plugin.repo : '';
|
||||
// The commit that introduced this version (newer registries only).
|
||||
// Checked as a hex SHA before it goes anywhere near a URL.
|
||||
const commit = typeof plugin.commit === 'string' && /^[0-9a-f]{7,40}$/i.test(plugin.commit) ? plugin.commit : '';
|
||||
const commitUrl = commit && repoLink
|
||||
? repoLink.replace(/\/+$/, '').replace(/\.git$/, '') + '/tree/' + commit
|
||||
+ (plugin.plugin_path ? '/' + plugin.plugin_path.split('/').map(encodeURIComponent).join('/') : '')
|
||||
: '';
|
||||
const commitHtml = !commit ? '' : (commitUrl
|
||||
? `<a href="${escapeAttribute(commitUrl)}" target="_blank" rel="noopener noreferrer" class="text-xs font-mono text-gray-500 hover:underline" title="Source of this version: commit ${escapeAttribute(commit)}">${escapeHtml(commit.slice(0, 7))}</a>`
|
||||
: `<span class="text-xs font-mono text-gray-500" title="Commit ${escapeAttribute(commit)}">${escapeHtml(commit.slice(0, 7))}</span>`);
|
||||
return `
|
||||
<div class="plugin-card">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
@@ -2435,10 +2448,11 @@ function renderPluginStore(plugins) {
|
||||
${installed ? '<span class="badge badge-success"><i class="fas fa-check mr-1"></i>Installed</span>' : ''}
|
||||
${isNewPlugin(plugin.last_updated) ? '<span class="badge badge-info"><i class="fas fa-sparkles mr-1"></i>New</span>' : ''}
|
||||
${plugin._source === 'custom_repository' ? `<span class="badge badge-accent" title="From: ${escapeHtml(plugin._repository_name || plugin._repository_url || 'Custom Repository')}"><i class="fas fa-bookmark mr-1"></i>Custom</span>` : ''}
|
||||
${plugin.incompatible_reason ? `<span class="badge badge-warning" title="${escapeAttribute(plugin.incompatible_reason)}"><i class="fas fa-exclamation-triangle mr-1"></i>Needs LEDMatrix ${escapeHtml(plugin.ledmatrix_min_version || 'update')}+</span>` : ''}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-1.5 mb-3">
|
||||
<p class="flex items-center"><i class="fas fa-user mr-2 text-gray-400 w-4"></i>${escapeHtml(plugin.author || 'Unknown')}</p>
|
||||
${plugin.version ? `<p class="flex items-center"><i class="fas fa-tag mr-2 text-gray-400 w-4"></i>v${escapeHtml(plugin.version)}</p>` : ''}
|
||||
${plugin.version ? `<p class="flex items-center flex-wrap gap-1.5"><i class="fas fa-tag mr-2 text-gray-400 w-4"></i>v${escapeHtml(plugin.version)}${commitHtml}</p>` : ''}
|
||||
<p class="flex items-center"><i class="fas fa-folder mr-2 text-gray-400 w-4"></i>${escapeHtml(plugin.category || 'General')}</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">${escapeHtml(plugin.description || 'No description available')}</p>
|
||||
|
||||
Reference in New Issue
Block a user