commit a9881ebec411ca3b9a0f1331dc464ee6b0cbe836 Author: pleb Date: Sun Jun 14 09:14:40 2026 -0700 init repo for a plugin helper diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..0928ec9 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,378 @@ +# plugin-helper Design + +`plugin-helper` is a Python CLI for managing Beat Saber plugins in a mounted Windows BSManager install. It installs from individual GitHub releases, keeps per-game-version plugin selections pinned, records exact filesystem changes, and leaves compatibility judgment visible enough for an agent to help when upstream packaging is inconsistent. + +The initial target is the Linux side of `incineroar`, after the Windows partition has been mounted manually. The current Beat Saber instances live under: + +```text +/home/pleb/Windows/Users/pleb/BSManager/BSInstances +``` + +## Goals + +- Manage plugins for one BSManager Beat Saber instance at a time, such as `1.40.8`. +- Pull plugin releases directly from configured GitHub repositories. +- Determine candidate updates while respecting the pinned Beat Saber version. +- Support selective updates and explicit pins. +- Resolve dependencies before install, including plugin libraries. +- Install through a dry-run plan first, then apply exactly that plan. +- Record every installed file so uninstall and rollback are deterministic. +- Back up `UserData` separately from plugin installation. +- Package the CLI with a Nix flake so it can be consumed by `brenise.dev/flake.nix` for `incineroar`. + +## Non-Goals + +- Replacing BSManager as a GUI or Beat Saber instance manager. +- Downloading or downgrading Beat Saber versions. +- Running `nixos-rebuild switch`. +- Mutating the Windows partition unless the user has mounted it and explicitly runs an apply command. +- Treating Nix as the plugin installer. Nix should package `plugin-helper`; the CLI should manage the mutable mounted game tree. + +## Core Model + +The project should keep four concepts separate: + +- **Registry**: checked-in knowledge about known plugins and where releases come from. +- **Lockfile**: the selected plugin versions for a specific Beat Saber version. +- **Install state**: exact files currently installed by `plugin-helper`. +- **Plan**: a generated dry-run artifact describing the next filesystem changes. + +This separation lets the script stay deterministic while an agent helps with ambiguous release metadata, compatibility notes, or per-plugin install rules. + +## Files and Directories + +Recommended repository layout: + +```text +plugin-helper/ + flake.nix + pyproject.toml + DESIGN.md + README.md + registry/ + plugins.toml + locks/ + 1.40.8.lock.toml + src/plugin_helper/ + __init__.py + cli.py + config.py + github.py + instances.py + metadata.py + planner.py + installer.py + state.py + userdata.py + tests/ +``` + +Runtime state should not need to live inside the repository. By default, keep mutable state under an XDG data directory, while allowing explicit paths for easy inspection: + +```text +~/.local/state/plugin-helper/ + instances/ + 1.40.8/ + installed.json + plans/ + downloads/ + backups/ +``` + +For early development, a `--state-dir` option is useful so plans and manifests can be kept in the repo while the format settles. + +## Registry + +The registry describes plugin sources and install behavior. It should be human-editable because many Beat Saber plugins have small packaging differences. + +Example: + +```toml +[[plugins]] +id = "songcore" +name = "SongCore" +repo = "Kylemc1413/SongCore" +asset_patterns = ["*SongCore*.zip"] +install_strategy = "bsipa-zip" +category = "library" + +[plugins.compatibility] +source = "metadata-or-release-notes" + +[[plugins.dependencies]] +id = "bs-utils" +constraint = ">=1.0" +required = true +``` + +Important fields: + +- `id`: stable local identifier. Prefer BSIPA metadata ID when known. +- `repo`: GitHub `owner/name`. +- `asset_patterns`: release asset glob patterns, evaluated in order. +- `install_strategy`: known extraction/copy strategy. +- `dependencies`: registry-level fallback when release metadata is missing or incomplete. +- `pin`: optional repository-wide pin, usually better placed in a per-version lockfile. +- `compatibility`: hints for interpreting release metadata. + +## Lockfile + +Each Beat Saber version should have its own lockfile. This is where selective update and "wait for the ecosystem" behavior lives. + +Example: + +```toml +beat_saber_version = "1.40.8" +instance = "1.40.8" + +[[plugins]] +id = "songcore" +repo = "Kylemc1413/SongCore" +tag = "v4.3.0" +asset = "SongCore-4.3.0.zip" +sha256 = "..." +reason = "Pinned until dependent mods support newer SongCore" + +[[plugins]] +id = "some-ui-mod" +repo = "example/some-ui-mod" +tag = "v1.2.1" +asset = "SomeUIMod.dll" +sha256 = "..." +``` + +The lockfile should be the source of truth for reproducible installs. `check` may propose newer versions, but `apply` should install what is present in a plan generated from the lockfile or from an explicit update command. + +## Install State + +The install state records what happened on disk. It should not be hand-edited during normal use. + +Example: + +```json +{ + "instance": "1.40.8", + "beatSaberVersion": "1.40.8", + "plugins": { + "songcore": { + "repo": "Kylemc1413/SongCore", + "tag": "v4.3.0", + "asset": "SongCore-4.3.0.zip", + "assetSha256": "...", + "installedAt": "2026-06-14T16:00:00Z", + "files": [ + { + "path": "Plugins/SongCore.dll", + "sha256": "...", + "size": 123456 + } + ] + } + } +} +``` + +Uninstall should delete only files recorded for that plugin, and should warn before deleting a file whose current hash no longer matches the recorded hash. + +## Plans + +All mutating operations should have a dry-run plan. The plan is both user-facing and machine-readable. + +Plan generation should: + +1. Read the target instance and Beat Saber version. +2. Read the registry and lockfile. +3. Query GitHub releases as needed. +4. Select compatible candidate releases. +5. Resolve dependencies. +6. Download release assets into a staging directory. +7. Inspect archives and DLL metadata where possible. +8. Compute target file changes. +9. Write a plan file. + +Plan application should: + +1. Re-verify downloaded asset hashes. +2. Re-verify the mounted instance path. +3. Back up overwritten files. +4. Copy or extract files exactly as listed in the plan. +5. Write install state atomically. +6. Leave a concise summary of changed files. + +## Compatibility + +Compatibility should be conservative. A release should be considered compatible only if one of these is true: + +- Embedded BSIPA metadata declares the target game version. +- Release notes or asset names explicitly mention the target game version. +- The registry contains a maintained compatibility override. +- The user or agent has reviewed and pinned the release for that Beat Saber version. + +When compatibility is ambiguous, `check` should report the candidate as "needs review" instead of proposing an automatic update. + +Useful data sources: + +- GitHub release tag, title, body, publish date, and assets. +- BSIPA metadata embedded in downloaded DLLs. +- `manifest.json` or similar files inside release archives. +- Existing installed DLL metadata. +- Registry overrides for known edge cases. + +bs-manager has a `ModMetadata` shape worth mirroring: + +```text +name, id, version, gameVersion, dependsOn, conflictsWith, loadAfter, loadBefore +``` + +For `plugin-helper`, this metadata should be part of release inspection and dependency solving, not just display. + +## Dependency Resolution + +Dependency resolution should start simple and explicit: + +- Build an available-version set from registry entries and inspected candidate releases. +- Add selected plugins from the lockfile or command line. +- Add transitive dependencies from embedded metadata and registry fallback rules. +- Reject plans with unresolved required dependencies. +- Warn about conflicts. +- Prefer already locked dependency versions unless an update is explicitly requested. + +This is closer to a small package solver than bs-manager's current dependency closure. It does not need to be sophisticated at first, but it should keep enough structure to grow into constraint handling. + +Initial dependency policy: + +- If a dependency is locked, keep it. +- If a dependency is installed but not locked, treat it as unmanaged and warn. +- If a dependency is missing and the registry has it, select the newest compatible release. +- If multiple compatible releases exist, choose the newest stable semver tag. +- If no compatible release is clear, stop and require review. + +## Install Strategies + +Start with a few explicit strategies instead of generic archive magic: + +- `dll-to-plugins`: copy one DLL to `Plugins/`. +- `zip-to-pending`: extract archive under `IPA/Pending/`. +- `bsipa-zip`: install BSIPA-style archive at the Beat Saber root or pending path as appropriate. +- `root-zip`: extract archive relative to the Beat Saber instance root. +- `manual`: plan only; requires a plugin-specific rule before apply. + +Every strategy should produce a concrete file list before apply. If an archive cannot be mapped safely, the plan should fail. + +## CLI + +Proposed commands: + +```text +plugin-helper instances +plugin-helper scan --instance 1.40.8 +plugin-helper check --instance 1.40.8 +plugin-helper plan --instance 1.40.8 +plugin-helper plan --instance 1.40.8 --update songcore +plugin-helper apply path/to/plan.json +plugin-helper uninstall --instance 1.40.8 songcore +plugin-helper pin --instance 1.40.8 songcore v4.3.0 +plugin-helper backup-userdata --instance 1.40.8 +``` + +Command behavior: + +- `instances`: list BSManager instances found under the configured root. +- `scan`: inspect installed plugins and compare against recorded state. +- `check`: query GitHub and report updates, pins, ambiguity, and missing dependencies. +- `plan`: produce a dry-run plan without mutating the game tree. +- `apply`: execute a previously generated plan. +- `uninstall`: remove files recorded in install state. +- `pin`: update the lockfile for a plugin. +- `backup-userdata`: snapshot `UserData` separately. + +## UserData Backups + +`UserData` backups should be independent from plugin install state because those files change when the game runs, not when plugins are installed. + +Initial backup behavior: + +- Create timestamped archives under the state directory. +- Include a small manifest with source instance, file count, total size, and hashes. +- Avoid pruning automatically until a retention policy is configured. +- Later, consider incremental backups or `rsync`-style deduplication if archives get large. + +## Python Implementation + +Python is the recommended language for the first version because this project needs fast iteration, readable plugin-specific rules, and good filesystem/archive ergonomics more than a static binary. + +Suggested dependencies: + +- `typer` or `click` for CLI. +- `pydantic` for typed config, lockfile, state, and plan schemas. +- `httpx` for GitHub API calls. +- `packaging` for version parsing. +- `tomli-w` for writing TOML if needed. +- Standard library `tomllib`, `zipfile`, `hashlib`, `pathlib`, and `json`. + +Keep domain logic in plain Python modules, not inside CLI command functions. The CLI should parse arguments, call services, and print summaries. + +## Nix Flake + +The flake should package the CLI and expose it as both a package and app: + +```nix +{ + outputs = { self, nixpkgs }: { + packages.x86_64-linux.default = ...; + apps.x86_64-linux.default = { + type = "app"; + program = "${self.packages.x86_64-linux.default}/bin/plugin-helper"; + }; + }; +} +``` + +Use `buildPythonApplication` once `pyproject.toml` exists. The Nix package should install the tool; it should not encode the mutable Windows mount path or plugin selection. + +`incineroar` can then consume it as a flake input and install the CLI into the user or system environment. + +## Agent Skill Boundary + +An agent skill would be useful after the CLI skeleton exists. The skill should instruct the agent to: + +- Read the registry, lockfile, and install state. +- Run `plugin-helper check` and inspect ambiguous releases. +- Use GitHub release notes and downloaded metadata to classify compatibility. +- Edit registry rules when a plugin has unusual packaging. +- Generate a plan, review it, then run `apply` only after the plan is concrete. +- Summarize changed files from the install state. + +The skill should not encourage arbitrary manual copying. Manual copying should become a registry rule or an install strategy so future uninstalls remain reliable. + +## Safety Rules + +- Default all mutating commands to dry-run or require an existing plan. +- Refuse to apply if the BSManager instance path does not exist. +- Refuse to apply if the target path is outside the selected Beat Saber instance. +- Warn before overwriting files not recorded in current state. +- Back up overwritten files before replacement. +- Use atomic writes for lockfiles and state files. +- Never delete files during uninstall unless they are recorded in install state. +- Warn when current file hashes differ from recorded hashes. + +## Open Questions + +- Should lockfiles live in the repo, XDG state, or both? +- Should registry compatibility overrides be committed, or kept in a local override file? +- Should GitHub API calls require a token to avoid rate limits? +- How should prereleases be handled for mods that publish beta compatibility first? +- Does BSIPA need to be managed by `plugin-helper`, or assumed to be installed by BSManager? +- Should `plugin-helper` optionally consume BeatMods metadata as an advisory source while still downloading from GitHub? + +## Initial Milestones + +1. Create the Python package, CLI skeleton, and flake. +2. Implement instance discovery and `instances`. +3. Define registry, lockfile, state, and plan schemas. +4. Implement GitHub release discovery and `check`. +5. Implement archive/DLL inspection enough to produce file plans. +6. Implement `plan` and `apply` for `dll-to-plugins` and `zip-to-pending`. +7. Implement uninstall from install state. +8. Add `backup-userdata`. +9. Add an agent skill once the commands and formats are stable.