init repo for a plugin helper
This commit is contained in:
+378
@@ -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.
|
||||
Reference in New Issue
Block a user