415 lines
15 KiB
Markdown
415 lines
15 KiB
Markdown
# plugin-helper Design
|
|
|
|
`plugin-helper` is a Python CLI for managing Beat Saber plugins in BSManager
|
|
installs. It installs from pinned release artifacts, keeps per-game-version
|
|
plugin selections locked, records exact filesystem changes, and leaves
|
|
compatibility judgment visible enough for an agent to help when upstream
|
|
packaging is inconsistent.
|
|
|
|
The current targets are the local Linux BSManager install and the mounted
|
|
Windows BSManager install:
|
|
|
|
```text
|
|
/home/pleb/Windows/Users/pleb/BSManager/BSInstances
|
|
/home/pleb/.local/share/BSManager/BSInstances
|
|
```
|
|
|
|
## Goals
|
|
|
|
- Manage plugins for one BSManager Beat Saber instance at a time, such as
|
|
`1.44.1`, while supporting the same instance name under multiple roots.
|
|
- 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 targeting that root.
|
|
- Treating Nix as the plugin installer. Nix should package `plugin-helper`; the
|
|
CLI should manage mutable game trees.
|
|
|
|
## 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/
|
|
<plugin-id>/
|
|
backups/
|
|
```
|
|
|
|
For early development, a `--state-dir` option is useful so plans and manifests can be kept in the repo while the format settles.
|
|
|
|
When managing both local Linux and mounted Windows installs, install state must
|
|
be separated by target root as well as by instance name. The current state
|
|
layout is keyed by instance name, so two `1.44.1` installs should not share one
|
|
state directory. A practical repo-local convention is:
|
|
|
|
```text
|
|
.state/ local Linux BSManager state
|
|
.state-windows/ mounted Windows BSManager state
|
|
```
|
|
|
|
The registry and lockfile remain shared for a Beat Saber version. Downloads may
|
|
be copied or re-fetched into each target-specific state directory, but generated
|
|
plans, bootstrap records, backups, and `installed.json` belong to one target
|
|
game tree.
|
|
|
|
## Registry
|
|
|
|
The registry describes plugin sources and install behavior. It should be human-editable because many Beat Saber plugins have small packaging differences.
|
|
|
|
Artifact source preference:
|
|
|
|
1. Prefer upstream GitHub release artifacts for normal plugins.
|
|
2. Use BeatMods as compatibility, dependency, and verification metadata even
|
|
when the artifact comes from GitHub.
|
|
3. Use BeatMods CDN artifacts only when the upstream artifact is inaccessible,
|
|
the package is effectively BeatMods-only, or the package is a framework or
|
|
library dependency that does not have a normal plugin release source.
|
|
4. Record both the artifact source and any BeatMods `modVersion`/version-id/
|
|
`zipHash` metadata used to justify compatibility.
|
|
|
|
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.
|