Files
punktfunk-plugin-rom-manager/README.md

145 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @punktfunk/plugin-rom-manager
A [Punktfunk](https://git.unom.io/unom/punktfunk) plugin that scans your ROM folders, maps them to
emulators, fetches box art, and reconciles everything into the host game library — so your retro games
appear automatically in the Punktfunk web console, native clients, and Moonlight `/applist`, each
launchable and covered in art. It ships a web UI that lives **inside the Punktfunk console** (no second
password, no second port).
Think of it as [Steam ROM Manager](https://github.com/SteamGridDB/steam-rom-manager) for Punktfunk: it
even uses the same art source (SteamGridDB) when you give it an API key.
---
## Requirements
- A Punktfunk host (Linux or Windows) with the **scripting runner** (`punktfunk-scripting`) enabled.
- [Bun](https://bun.sh) (the runner is Bun).
- `@punktfunk/host` **≥ 0.1.2** + `@punktfunk/plugin-kit` **≥ 0.1.1** — it provides the plugin-UI surface (`servePluginUi`) and discovers
scoped `@punktfunk/plugin-*` packages.
## Install
The plugin is scoped under `@punktfunk`, so one bunfig line points that scope at the Punktfunk registry
while everything else (like `effect`) resolves from npm — no bundling, and `@punktfunk/host` + `effect`
are shared across all your plugins:
```sh
mkdir -p "$(punktfunk config-dir)/plugins" && cd "$(punktfunk config-dir)/plugins"
cat > bunfig.toml <<'EOF'
[install.scopes]
"@punktfunk" = "https://git.unom.io/api/packages/unom/npm/"
EOF
bun add @punktfunk/plugin-rom-manager
# then enable the runner (opt-in):
systemctl --user enable --now punktfunk-scripting # Linux
Enable-ScheduledTask PunktfunkScripting # Windows
```
Open the Punktfunk console — a **ROM Manager** entry appears in the nav. That's it.
> **Headless / host-only boxes** without the console can just drop a `config.json` (below) and use
> the CLI — the sync engine is fully functional from the file alone.
## Quick start (config file)
The plugin owns `<config_dir>/plugin-state/rom-manager/config.json`. A minimal example:
```jsonc
{
"roots": [
{ "dir": "/mnt/roms/snes", "platform": "snes" },
{ "dir": "/mnt/roms/ps1", "platform": "ps1", "excludes": ["*.sav", "bios/**"] }
],
"art": { "provider": "auto", "steamGridDbKey": "" }
}
```
Only the keys you author are stored — defaults live in the schema (`contract/src/config.ts`) and are
never baked into your file. Then, from the plugin dir:
```sh
bunx punktfunk-plugin-rom-manager scan # what the scanner finds
bunx punktfunk-plugin-rom-manager detect # which emulators are installed
bunx punktfunk-plugin-rom-manager preview # the desired library (no host write)
bunx punktfunk-plugin-rom-manager sync # reconcile into the live library
```
## Box art
Two sources, selected by `art.provider`:
| Provider | Key? | Art types | Notes |
|---------------|-------|------------------------------------|-------|
| `steamgriddb` | yes | portrait + hero + logo + header | What Steam ROM Manager uses; best coverage + fuzzy title match. Free key from [steamgriddb.com profile preferences](https://www.steamgriddb.com/profile/preferences). |
| `libretro` | no | box-art portrait only | Keyless [libretro-thumbnails](https://thumbnails.libretro.com); works out of the box. |
| `auto` (default) | — | SteamGridDB if a key is set, else libretro | |
Art URLs are handed to the host as-is; native clients/console fetch them directly (so **clients need
internet for covers**), and Moonlight covers are proxied host-side. Verdicts are cached in
`cache.json` and never re-probed unless you change provider/key.
## Platforms & emulators
~25 built-in platforms (NES, SNES, N64, GameCube, Wii, Switch, GB/GBC/GBA, NDS, 3DS, PS1/2/PSP,
Genesis, Master System, Game Gear, Saturn, Dreamcast, PC Engine, Neo Geo Pocket, WonderSwan, Lynx,
Atari 2600/7800, Xbox). Arcade (MAME/FBNeo romset versioning) is intentionally out of scope.
Emulators are detected best-effort (PATH → Flatpak → known install paths): RetroArch (+ cores),
Dolphin, PCSX2, DuckStation, PPSSPP, mGBA, melonDS, Flycast, xemu, plus Azahar/Ryujinx. Any platform's
emulator, core, and args are overridable per-platform and per-game (config or UI). Missing an
emulator? Add a `custom-*` template.
Disc games are folded so a set is **one entry**: `.m3u` playlists absorb their discs, `.cue`/`.gdi`
sheets hide their tracks, `.chd` stands alone.
## The UI
`servePluginUi` serves the SPA on a loopback ephemeral port behind a per-boot secret and registers it
with the host; the console reverse-proxies it and gates it with your existing console sign-in. Five
pages: **Overview** (health, stats, live sync activity), **Library** (scan preview with covers
+ include toggles), **Sources** (ROM roots), **Emulators** (detection + per-platform overrides),
**Settings** (art, sync, file paths). The console surface is the only UI — the old standalone
password server was removed in 0.3.0 (headless boxes drive the plugin via the CLI).
## Security
- **ROM filenames are untrusted input** that ends up in a `sh -c` / `cmd.exe /c` string run as the host
user. Every path goes through strict per-OS quoting (`src/quote.ts`); on Windows, names containing
`cmd.exe` metacharacters (`" % ! ^ & | < >`) are refused rather than risk injection.
- `config.json` lives in the hardened `<config_dir>`; the plugin refuses a group/world-writable one.
- Outbound network in v1: SteamGridDB (if keyed) and libretro-thumbnails only. No telemetry.
## Development — this repo is the plugin blueprint
rom-manager is the reference consumer of
[`@punktfunk/plugin-kit`](https://git.unom.io/unom/punktfunk/src/branch/main/plugin-kit) — copy this
repo's structure to start a new plugin. Three bun workspaces:
| Workspace | What it is |
|---|---|
| `contract/` | **The single source of truth**: Effect Schemas for the config (raw↔resolved in one schema), domain DTOs, the `HttpApi` contract, API errors. Never published — bundled into the plugin, imported source-level by the UI. No hand-mirrored types anywhere. |
| `plugin/` | The published package. `src/domain` + `src/art` are the pure, injectable, unit-tested core; `src/services` wires them into the kit (ConfigService, CacheStore, SyncEngine, ProviderClient, HttpApi handlers + SSE); `src/index.ts` is the `definePluginKit` entry (async at the runner boundary, Effect inside). |
| `ui/` | The SPA: React 19 + `@unom/ui` + `@unom/app-ui` + the kit's `/react` helpers + `/theme.css`, with an Effect-native data layer (`AtomHttpApi` atoms derived from the contract). Builds into `plugin/dist/ui`. |
```sh
bun install # one workspace install (Gitea registry for @punktfunk/@unom scopes)
bun test # domain + service tests (plugin/)
bun run typecheck # contract → plugin → ui
bun run build # backend bundle + SPA → plugin/dist
cd ui && bun run dev # UI with ZERO host running (in-browser mock layer + scenarios)
cd ui && bun run storybook # per-page stories on the same fixtures (port 6013)
cd plugin && bun run dev # auth-free dev API on :5885 …
cd ui && bun run dev:live # … and the UI proxied against it
```
Requires `@punktfunk/host` ≥ 0.1.2 and `@punktfunk/plugin-kit` ≥ 0.1.1 at runtime (shared with the
runner, not bundled). `@unom/*` + `@effect/atom-react` are build-time only — the SPA ships as static
assets, so the published plugin carries no UI runtime dependencies.
## License
MIT OR Apache-2.0.