Add known working config profile for reverting changes

This commit is contained in:
pleb
2026-07-05 17:06:51 -07:00
parent 545d407116
commit a94a1ed000
9 changed files with 386 additions and 7 deletions
+51 -1
View File
@@ -14,7 +14,7 @@ from .github import fetch_releases
from .installer import apply_plan, disable_plugin, uninstall_plugin
from .instances import get_instance, list_instances
from .models import load_lockfile, load_registry
from .operations import enable_disabled_plugin
from .operations import enable_disabled_plugin, restore_known_good_set, save_known_good_set
from .planner import create_plan
from .reports import installed_plugins_report, print_installed_plugins
from .scanner import scan_instance
@@ -197,6 +197,24 @@ def build_parser() -> argparse.ArgumentParser:
enable.add_argument("--lockfile")
enable.add_argument("plugin")
save_known_good = subcommands.add_parser(
"save-known-good",
help="Record the currently enabled plugins as the known-good set for this instance",
parents=[_common_parent()],
)
save_known_good.add_argument("--instance", required=True)
save_known_good.add_argument("--json", action="store_true", help="Print full JSON output")
restore_known_good = subcommands.add_parser(
"restore-known-good",
help="Disable plugins outside the saved known-good set and re-enable any that are missing",
parents=[_common_parent()],
)
restore_known_good.add_argument("--instance", required=True)
restore_known_good.add_argument("--registry", default="registry/plugins")
restore_known_good.add_argument("--lockfile")
restore_known_good.add_argument("--json", action="store_true", help="Print full JSON output")
backup = subcommands.add_parser(
"backup-userdata",
help="Copy UserData and Windows AppData into the adjacent backups repo",
@@ -520,6 +538,38 @@ def run(argv: list[str] | None = None) -> int:
print(f"State: {result['statePath']}")
return 0
if args.command == "save-known-good":
result = save_known_good_set(instance=args.instance, state_root=st_root)
if args.json:
_json(result)
else:
print(f"Saved known-good set for {args.instance}: {len(result['pluginIds'])} enabled plugins.")
for plugin_id in result["pluginIds"]:
print(f" {plugin_id}")
return 0
if args.command == "restore-known-good":
instance = get_instance(inst_roots, args.instance)
progress = (lambda message: print(f" {message}", flush=True))
result = restore_known_good_set(
instance=args.instance,
instance_path=instance.path,
state_root=st_root,
registry=args.registry,
lockfile=args.lockfile,
progress=progress,
)
if args.json:
_json(result)
else:
print(f"Disabled: {len(result['disabled'])}")
print(f"Enabled: {len(result['enabled'])}")
for item in result["disableErrors"]:
print(f" disable error {item['plugin']}: {item['error']}")
for item in result["enableErrors"]:
print(f" enable error {item['plugin']}: {item['error']}")
return 2 if result["disableErrors"] or result["enableErrors"] else 0
if args.command == "backup-userdata":
instance = get_instance(inst_roots, args.instance)
root = repo_root()
+71 -2
View File
@@ -6,10 +6,10 @@ from typing import Any
from .bootstrap import ensure_healthy_bootstrap
from .config import repo_root
from .installer import apply_plan
from .installer import apply_plan, disable_plugin
from .models import Lockfile, Registry, load_lockfile, load_registry
from .planner import create_plan
from .state import load_installed_state
from .state import load_installed_state, load_known_good_state, save_known_good_state
def _resolve_paths(
@@ -161,3 +161,72 @@ def enable_disabled_plugins(
enabled.append({"plugin": plugin_id, "planPath": str(batch_path), "applied": applied_by_plugin.get(plugin_id, 0)})
return {"enabled": enabled, "errors": errors}
def save_known_good_set(*, instance: str, state_root: Path) -> dict[str, Any]:
"""Record the currently enabled plugin ids as the known-good set for this instance."""
installed_state = load_installed_state(state_root, instance)
plugin_ids = sorted(installed_state.get("plugins", {}))
known_good = {
"instance": instance,
"beatSaberVersion": installed_state.get("beatSaberVersion"),
"pluginIds": plugin_ids,
}
save_known_good_state(state_root, instance, known_good)
return known_good
def restore_known_good_set(
*,
instance: str,
instance_path: Path,
state_root: Path,
registry: str = "registry/plugins",
lockfile: str | None = None,
repo: Path | None = None,
progress: Callable[[str], None] | None = None,
) -> dict[str, Any]:
"""Disable plugins outside the saved known-good set and re-enable the ones that are missing."""
known_good = load_known_good_state(state_root, instance)
known_good_ids = set(known_good.get("pluginIds", []))
if not known_good:
raise KeyError(f"no known-good set is saved for this instance: {instance}")
installed_state = load_installed_state(state_root, instance)
currently_enabled = set(installed_state.get("plugins", {}))
to_disable = sorted(currently_enabled - known_good_ids)
disabled: list[str] = []
disable_errors: list[dict[str, str]] = []
for plugin_id in to_disable:
try:
result = disable_plugin(instance, instance_path, state_root, plugin_id, False)
if result["stateUpdated"]:
disabled.append(plugin_id)
else:
skipped = "; ".join(f"{item['path']} {item['reason']}" for item in result["skipped"])
disable_errors.append({"plugin": plugin_id, "error": skipped or "no files changed"})
except Exception as exc:
disable_errors.append({"plugin": plugin_id, "error": str(exc)})
installed_state = load_installed_state(state_root, instance)
currently_enabled = set(installed_state.get("plugins", {}))
to_enable = sorted(known_good_ids - currently_enabled)
enable_result = enable_disabled_plugins(
instance=instance,
instance_path=instance_path,
state_root=state_root,
plugin_ids=to_enable,
registry=registry,
lockfile=lockfile,
repo=repo,
progress=progress,
)
return {
"knownGood": known_good,
"disabled": disabled,
"disableErrors": disable_errors,
"enabled": [item["plugin"] for item in enable_result["enabled"]],
"enableErrors": enable_result["errors"],
}
+14
View File
@@ -64,3 +64,17 @@ def backups_dir(state_root: Path, instance: str) -> Path:
path = instance_state_dir(state_root, instance) / "backups"
path.mkdir(parents=True, exist_ok=True)
return path
def known_good_state_path(state_root: Path, instance: str) -> Path:
return instance_state_dir(state_root, instance) / "known-good.json"
def load_known_good_state(state_root: Path, instance: str) -> dict[str, Any]:
return read_json(known_good_state_path(state_root, instance), {})
def save_known_good_state(state_root: Path, instance: str, state: dict[str, Any]) -> None:
state.setdefault("instance", instance)
state["savedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
atomic_write_json(known_good_state_path(state_root, instance), state)
+58 -3
View File
@@ -13,9 +13,14 @@ from textual.widgets import DataTable, Footer, Header, Static
from .bsipa import BSIPA_PLUGIN_ID
from .installer import disable_plugin
from .models import load_lockfile, load_registry
from .operations import enable_disabled_plugin, enable_disabled_plugins
from .operations import (
enable_disabled_plugin,
enable_disabled_plugins,
restore_known_good_set,
save_known_good_set,
)
from .reports import installed_plugins_report
from .state import load_installed_state
from .state import load_installed_state, load_known_good_state
@dataclass(frozen=True)
@@ -44,6 +49,8 @@ class PluginHelperTui(App[int]):
Binding("space", "toggle_plugin", "Toggle", priority=True),
Binding("d", "disable_all_plugins", "Disable all", priority=True),
Binding("e", "enable_all_plugins", "Enable all", priority=True),
Binding("s", "save_known_good", "Save known-good", priority=True),
Binding("g", "restore_known_good", "Restore known-good", priority=True),
Binding("r", "refresh", "Refresh"),
Binding("b", "back", "Back"),
Binding("q", "quit", "Quit"),
@@ -229,6 +236,53 @@ class PluginHelperTui(App[int]):
self._set_bulk_status("Enabled", changed, errors)
self._show_plugins(preserve_status=True)
def action_save_known_good(self) -> None:
if self._busy or self.mode != "plugins" or self.selected_installation is None:
return
target = self.selected_installation
known_good = save_known_good_set(instance=target.instance_name, state_root=target.state_root)
count = len(known_good["pluginIds"])
self._set_status(f"Saved known-good set: {count} enabled plugins.")
async def action_restore_known_good(self) -> None:
if self._busy or self.mode != "plugins" or self.selected_installation is None:
return
target = self.selected_installation
if not load_known_good_state(target.state_root, target.instance_name):
self._set_status("No known-good set saved yet. Press s to save the current selection.")
return
self._busy = True
self._set_status("Restoring known-good set...")
try:
result = await asyncio.to_thread(
restore_known_good_set,
instance=target.instance_name,
instance_path=target.instance_path,
state_root=target.state_root,
repo=self.repo_root,
progress=self._operation_progress,
)
except Exception as exc:
self._set_status(f"Could not restore known-good set: {exc}")
return
finally:
self._busy = False
errors = [f"{item['plugin']}: {item['error']}" for item in result["disableErrors"]] + [
f"{item['plugin']}: {item['error']}" for item in result["enableErrors"]
]
if errors:
preview = "; ".join(errors[:3])
suffix = f"; {len(errors) - 3} more" if len(errors) > 3 else ""
self._set_status(
f"Restored known-good set: disabled {len(result['disabled'])}, "
f"enabled {len(result['enabled'])}; {len(errors)} failed: {preview}{suffix}"
)
else:
self._set_status(
f"Restored known-good set: disabled {len(result['disabled'])}, enabled {len(result['enabled'])}."
)
self._show_plugins(preserve_status=True)
def _operation_progress(self, message: str) -> None:
self.call_from_thread(self._set_status, message)
@@ -294,7 +348,8 @@ class PluginHelperTui(App[int]):
if self.plugin_rows:
back_hint = "" if len(self.choices) == 1 else " b returns to installations."
self._set_status(
f"Space toggles selected. d disables all. e enables all.{back_hint}"
"Space toggles selected. d disables all. e enables all. "
f"s saves known-good. g restores known-good.{back_hint}"
)
else:
self._set_status("No version-locked plugins for this installation.")