33e91e4d00
Reliability + correctness pass on the packaging and UI. Packaging — drop the self-contained bundle: - Rename to `@punktfunk/plugin-rom-manager` (scoped). An unscoped package can't be split across registries, which is the only reason bundling effect + the SDK into every plugin seemed necessary. Scoped names resolve from one scope-map, so the plugin depends on `@punktfunk/host` + `effect` as SHARED (hoisted) deps — no duplication, no ~1 MB effect copy per plugin. Build is back to plain `tsc`. (Runner-side discovery of `@punktfunk/plugin-*` shipped in @punktfunk/host 0.1.1.) UI — use the console's design system so it reads as family: - SPA rebuilt on @unom/style tokens + Tailwind v4 + the console's exact theme (violet chrome, Geist, card/button vocabulary). Build-time only → static assets in dist/ui, no @unom runtime dep. Skips @unom/ui's AnimatedButton/Card, which statically import ~7 MB of UI sound assets — same look, 416 KB dist/ui. - Add a custom-emulator editor (Emulators page) writing config.emulators — you can now add a launcher Punktfunk doesn't ship a definition for. - Fix brand capitalization to "Punktfunk" in all UI copy + docs. Install docs + CI updated for the scoped `bun add @punktfunk/plugin-rom-manager` flow. 48 engine tests green; backend + UI typecheck + biome clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
6.6 KiB
Markdown
150 lines
6.6 KiB
Markdown
# @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.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 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 # backend deps (@punktfunk/host + effect) from the Gitea registry
|
||
bun test # engine unit tests (pure core: quoting, scanner, reconcile, art)
|
||
bun run typecheck
|
||
bun run build:all # backend → dist/ (tsc), SPA → dist/ui/ (Vite)
|
||
cd ui && bun run dev # SPA dev server (point it at a running standalone instance)
|
||
```
|
||
|
||
- The backend is plain `tsc` output — it depends on `@punktfunk/host` + `effect` at runtime (shared
|
||
with the runner and other plugins, not bundled).
|
||
- The **SPA** (`ui/`) uses the console's design system (`@unom/ui` + `@unom/style` + Tailwind v4) so it
|
||
reads as family. Those are **build-time only** — the SPA compiles to static assets in `dist/ui`, so
|
||
the published plugin carries no `@unom` runtime dependency. Installing them needs the `@unom` scope in
|
||
your `~/.npmrc` (with the Gitea token); CI needs the same token as a secret.
|
||
|
||
Requires `@punktfunk/host` ≥ 0.1.1.
|
||
|
||
## License
|
||
|
||
MIT OR Apache-2.0.
|