Show version-locked plugins in menu
This commit is contained in:
@@ -18,6 +18,9 @@ from .scanner import scan_bootstrap_files
|
||||
from .state import bootstrap_state_path, plugin_downloads_dir, save_bootstrap_state
|
||||
|
||||
|
||||
DEFAULT_IPA_TIMEOUT_SECONDS = 30
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
@@ -112,7 +115,7 @@ def _fetch_github_release_asset(locked: LockedPlugin, destination: Path) -> dict
|
||||
def fetch_locked_bsipa_archive(lockfile: Lockfile, state_root: Path) -> dict[str, Any]:
|
||||
locked = next((plugin for plugin in lockfile.plugins if plugin.id == BSIPA_PLUGIN_ID), None)
|
||||
if not locked:
|
||||
raise ValueError("lockfile does not include a bsipa entry")
|
||||
raise ValueError("version lock does not include a bsipa entry")
|
||||
if not locked.asset:
|
||||
raise ValueError("locked BSIPA entry has no asset")
|
||||
destination = plugin_downloads_dir(state_root, lockfile.instance, BSIPA_PLUGIN_ID) / locked.asset
|
||||
@@ -210,7 +213,7 @@ def run_bootstrap(
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = 120,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
tell = progress or (lambda _message: None)
|
||||
use_native = is_windows() if native is None else native
|
||||
@@ -311,7 +314,7 @@ def ensure_healthy_bootstrap(
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = 120,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
) -> None:
|
||||
if not planning_requires_bootstrap(lockfile.plugins, selected_ids):
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ def check_lock(
|
||||
messages: list[dict[str, str]] = []
|
||||
|
||||
if not registry_plugin:
|
||||
messages.append({"level": "warning", "message": "missing registry entry"})
|
||||
messages.append({"level": "warning", "message": "missing catalog entry"})
|
||||
if strategy == "manual":
|
||||
messages.append({"level": "error", "message": "manual install strategy cannot be applied"})
|
||||
if not locked.asset:
|
||||
|
||||
@@ -95,14 +95,14 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
state = subcommands.add_parser(
|
||||
"state",
|
||||
help="Show recorded plugin-helper install state",
|
||||
help="Show recorded plugin-helper managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
state.add_argument("--instance", required=True)
|
||||
|
||||
installed = subcommands.add_parser(
|
||||
"installed",
|
||||
help="List plugins installed by plugin-helper with locked release versions",
|
||||
help="List plugins from the version lock with managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
installed.add_argument("--instance", required=True)
|
||||
@@ -112,7 +112,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
check = subcommands.add_parser(
|
||||
"check",
|
||||
help="Validate local registry, lockfile, and release asset readiness",
|
||||
help="Validate local catalog, version lock, and release asset readiness",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
check.add_argument("--instance", required=True)
|
||||
@@ -154,7 +154,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
plan = subcommands.add_parser(
|
||||
"plan",
|
||||
help="Create a dry-run install plan from registry and lockfile",
|
||||
help="Create a dry-run install plan from the catalog and version lock",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
plan.add_argument("--instance", required=True)
|
||||
@@ -189,7 +189,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
enable = subcommands.add_parser(
|
||||
"enable",
|
||||
help="Reinstall a disabled locked plugin from local assets",
|
||||
help="Enable a version-locked plugin from local assets",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
enable.add_argument("--instance", required=True)
|
||||
|
||||
@@ -88,7 +88,7 @@ def uninstall_plugin(instance: str, instance_path: Path, state_root: Path, plugi
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
plugin_state = installed_state.get("plugins", {}).get(plugin_id)
|
||||
if not plugin_state:
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
@@ -119,7 +119,7 @@ def disable_plugin(instance: str, instance_path: Path, state_root: Path, plugin_
|
||||
if not plugin_state:
|
||||
if plugin_id in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is already disabled: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
|
||||
@@ -41,8 +41,8 @@ def enable_disabled_plugin(
|
||||
progress: Callable[[str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
if plugin_id not in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is not recorded as disabled: {plugin_id}")
|
||||
if plugin_id in installed_state.get("plugins", {}):
|
||||
raise KeyError(f"plugin is already enabled: {plugin_id}")
|
||||
|
||||
root, registry_path, _lock_path, loaded_lockfile, loaded_registry = _resolve_paths(
|
||||
instance=instance,
|
||||
@@ -51,7 +51,21 @@ def enable_disabled_plugin(
|
||||
repo=repo,
|
||||
)
|
||||
if not any(plugin.id == plugin_id for plugin in loaded_lockfile.plugins):
|
||||
raise KeyError(f"plugin is disabled but not locked for this instance: {plugin_id}")
|
||||
raise KeyError(f"plugin is not locked for this instance: {plugin_id}")
|
||||
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {plugin_id} from local assets")
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
require_bootstrap=False,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
@@ -65,16 +79,7 @@ def enable_disabled_plugin(
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
)
|
||||
tell(f"Applying {plugin_id}")
|
||||
result = apply_plan(plan, state_root)
|
||||
return {"planPath": str(path), **result}
|
||||
|
||||
@@ -100,8 +105,38 @@ def enable_disabled_plugins(
|
||||
repo=repo,
|
||||
)
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
disabled_plugins = installed_state.get("disabledPlugins", {})
|
||||
selected_ids = set(plugin_ids)
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
locked_ids = {plugin.id for plugin in loaded_lockfile.plugins}
|
||||
|
||||
enabled: list[dict[str, Any]] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
plannable_ids: list[str] = []
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is already enabled: {plugin_id}"})
|
||||
continue
|
||||
if plugin_id not in locked_ids:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is not locked for this instance: {plugin_id}"})
|
||||
continue
|
||||
plannable_ids.append(plugin_id)
|
||||
|
||||
if not plannable_ids:
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
selected_ids = set(plannable_ids)
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {len(plannable_ids)} plugins from local assets")
|
||||
batch_plan, batch_path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected=selected_ids,
|
||||
require_bootstrap=False,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
@@ -115,31 +150,14 @@ def enable_disabled_plugins(
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
enabled: list[dict[str, Any]] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id not in disabled_plugins:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is not recorded as disabled: {plugin_id}"})
|
||||
continue
|
||||
if not any(plugin.id == plugin_id for plugin in loaded_lockfile.plugins):
|
||||
errors.append(
|
||||
{"plugin": plugin_id, "error": f"plugin is disabled but not locked for this instance: {plugin_id}"}
|
||||
)
|
||||
continue
|
||||
try:
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
)
|
||||
result = apply_plan(plan, state_root)
|
||||
enabled.append({"plugin": plugin_id, "planPath": str(path), "applied": len(result["applied"])})
|
||||
except Exception as exc:
|
||||
errors.append({"plugin": plugin_id, "error": str(exc)})
|
||||
tell(f"Applying {len(plannable_ids)} plugins")
|
||||
result = apply_plan(batch_plan, state_root)
|
||||
applied_by_plugin: dict[str, int] = {}
|
||||
for item in result["applied"]:
|
||||
plugin_id = item["plugin"]
|
||||
applied_by_plugin[plugin_id] = applied_by_plugin.get(plugin_id, 0) + 1
|
||||
|
||||
for plugin_id in plannable_ids:
|
||||
enabled.append({"plugin": plugin_id, "planPath": str(batch_path), "applied": applied_by_plugin.get(plugin_id, 0)})
|
||||
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
@@ -118,11 +118,11 @@ def create_plan(
|
||||
if strategy not in VALID_STRATEGIES:
|
||||
raise ValueError(f"{locked.id}: invalid install strategy: {strategy}")
|
||||
if strategy == "manual":
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete registry rule first")
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete catalog rule first")
|
||||
if not locked.asset:
|
||||
raise ValueError(f"{locked.id}: lock entry has no asset")
|
||||
if registry_plugin and not _asset_matches_patterns(Path(locked.asset).name, registry_plugin.asset_patterns):
|
||||
warnings.append(f"{locked.id}: asset does not match registry patterns")
|
||||
warnings.append(f"{locked.id}: asset does not match catalog patterns")
|
||||
|
||||
asset_path = _find_asset(locked.asset, state_root, instance, repo_root, locked.id)
|
||||
if not asset_path:
|
||||
|
||||
@@ -13,15 +13,24 @@ def installed_plugins_report(
|
||||
) -> dict[str, Any]:
|
||||
locked_by_id = {plugin.id: plugin for plugin in lockfile.plugins}
|
||||
plugins: list[dict[str, Any]] = []
|
||||
state_plugins = [
|
||||
(plugin_id, plugin_state, "enabled")
|
||||
for plugin_id, plugin_state in installed_state.get("plugins", {}).items()
|
||||
]
|
||||
state_plugins.extend(
|
||||
(plugin_id, plugin_state, "disabled")
|
||||
for plugin_id, plugin_state in installed_state.get("disabledPlugins", {}).items()
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
disabled_plugins = installed_state.get("disabledPlugins", {})
|
||||
plugin_ids = list(locked_by_id)
|
||||
plugin_ids.extend(
|
||||
sorted(
|
||||
plugin_id
|
||||
for plugin_id in set(enabled_plugins) | set(disabled_plugins)
|
||||
if plugin_id not in locked_by_id
|
||||
)
|
||||
)
|
||||
for plugin_id, plugin_state, status in sorted(state_plugins):
|
||||
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
plugin_state = enabled_plugins[plugin_id]
|
||||
status = "enabled"
|
||||
else:
|
||||
plugin_state = disabled_plugins.get(plugin_id, {})
|
||||
status = "disabled"
|
||||
registry_plugin = registry.get(plugin_id)
|
||||
locked = locked_by_id.get(plugin_id)
|
||||
files = plugin_state.get("files", [])
|
||||
@@ -50,9 +59,9 @@ def installed_plugins_report(
|
||||
|
||||
def print_installed_plugins(report: dict[str, Any]) -> None:
|
||||
plugins = report["plugins"]
|
||||
print(f"{report['instance']} managed plugins ({len(plugins)})")
|
||||
print(f"{report['instance']} version-locked plugins ({len(plugins)})")
|
||||
if not plugins:
|
||||
print("No plugins have been installed by plugin-helper yet.")
|
||||
print("No plugins are listed in this instance's version lock.")
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Status", "Version", "Asset", "Files", "Installed")
|
||||
|
||||
@@ -140,7 +140,7 @@ class PluginHelperTui(App[int]):
|
||||
state_root=target.state_root,
|
||||
plugin_id=plugin_id,
|
||||
repo=self.repo_root,
|
||||
progress=self._bootstrap_progress,
|
||||
progress=self._operation_progress,
|
||||
)
|
||||
self._set_status(f"Enabled {plugin_id}; applied {len(result['applied'])} files.")
|
||||
else:
|
||||
@@ -217,7 +217,7 @@ class PluginHelperTui(App[int]):
|
||||
state_root=target.state_root,
|
||||
plugin_ids=plugin_ids,
|
||||
repo=self.repo_root,
|
||||
progress=self._bootstrap_progress,
|
||||
progress=self._operation_progress,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_status(f"Could not enable plugins: {exc}")
|
||||
@@ -229,8 +229,8 @@ class PluginHelperTui(App[int]):
|
||||
self._set_bulk_status("Enabled", changed, errors)
|
||||
self._show_plugins(preserve_status=True)
|
||||
|
||||
def _bootstrap_progress(self, message: str) -> None:
|
||||
self.call_from_thread(self._set_status, f"Bootstrapping: {message}")
|
||||
def _operation_progress(self, message: str) -> None:
|
||||
self.call_from_thread(self._set_status, message)
|
||||
|
||||
def _show_installations(self) -> None:
|
||||
self.mode = "installations"
|
||||
@@ -297,7 +297,7 @@ class PluginHelperTui(App[int]):
|
||||
f"Space toggles selected. d disables all. e enables all.{back_hint}"
|
||||
)
|
||||
else:
|
||||
self._set_status("No managed plugins recorded for this installation.")
|
||||
self._set_status("No version-locked plugins for this installation.")
|
||||
|
||||
def _cursor_index(self, row_count: int) -> int | None:
|
||||
table = self.query_one(DataTable)
|
||||
|
||||
Reference in New Issue
Block a user