feat: initial ROM & emulator manager plugin (M0–M5)
CI / build (push) Has been cancelled
CI / publish (push) Has been cancelled

A punktfunk-plugin-* package that scans ROM directories, maps them to
emulators, fetches box art, and reconciles them into the host game library
under provider id `rom-manager` — with a console-hosted web UI.

Engine (pure, unit-tested core):
- Table-driven platform registry (~25 consoles) + emulator registry with
  best-effort per-OS detection (PATH / Flatpak / known paths) and RetroArch
  core discovery.
- Scanner with disc folding (m3u/cue/gdi), archive gating, excludes.
- No-Intro title parsing + optional per-platform region dedupe.
- Security-critical quoting seam: POSIX single-quote + Windows double-quote
  with hostile-name refusal; ROM filenames never reach a shell un-quoted.
- Pure desired-state reconcile (stable external_ids, scale guard, fingerprint
  skip) → full-replace PUT /library/provider/rom-manager.

Box art (like Steam ROM Manager): SteamGridDB primary (portrait/hero/logo/
header, fuzzy match, operator API key) behind a provider seam, with keyless
libretro-thumbnails as the zero-setup fallback (`auto` default).

UI: console-hosted via the SDK `servePluginUi` (zero plugin-side auth) with a
plugin-local REST/SSE API and a self-contained React SPA (Setup / Emulators /
Games / Sync). Standalone password-gated fallback for host-only installs.

CLI: scan / detect / preview / sync / uninstall / set-password.

48 engine tests, typecheck + biome clean, SPA builds. Verified end-to-end:
scan → detect → reconcile PUT, fingerprint idempotence, and the standalone
UI serving SPA + REST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 02:40:59 +02:00
commit 3a6c80558d
59 changed files with 6051 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
# 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` **with the plugin-UI surface** (`servePluginUi`). This is what powers the
console-hosted UI; the standalone fallback works without it.
## Install
```sh
cd "$(punktfunk config-dir)/plugins" # e.g. ~/.config/punktfunk/plugins
bun add punktfunk-plugin-rom-manager --registry https://git.unom.io/api/packages/unom/npm/
# 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 use the [standalone UI](#standalone-ui) or
> just drop a `config.json` (below) — the sync engine is fully functional from the file alone.
## Quick start (config file)
The plugin owns `<config_dir>/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": "" }
}
```
See [`config.example.json`](./config.example.json) for every option. 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
### Console-hosted (default)
`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. Four
pages: **Setup** (ROM roots), **Emulators** (detected + per-platform overrides), **Games** (scan
preview with covers + per-title include toggles), **Sync** (status, art settings, sync options).
### Standalone UI
For host-only installs, set `ui.standalone: true` (config). It serves the same SPA on a fixed port:
```jsonc
"ui": { "standalone": true, "port": 47993, "bind": "127.0.0.1" }
```
A **non-loopback bind requires a password** (fail-closed — the UI can edit launch commands, i.e.
host-user code):
```sh
bunx punktfunk-plugin-rom-manager set-password 'your-password'
```
## 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
```sh
bun install # resolves @punktfunk/host from the Gitea npm registry
bun test # engine unit tests (pure core: quoting, scanner, reconcile, art)
bun run typecheck
bun run build:all # backend → dist/, SPA → dist/ui/
cd ui && bun run dev # SPA dev server (point it at a running standalone instance)
```
This plugin requires `@punktfunk/host` ≥ 0.1.0 (the version that introduced `servePluginUi`).
## License
MIT OR Apache-2.0.