Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.4 KiB
@punktfunk/plugin-rom-manager
A 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 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 (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:
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:
{
"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:
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. |
libretro |
no | box-art portrait only | Keyless libretro-thumbnails; 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 /cstring run as the host user. Every path goes through strict per-OS quoting (src/quote.ts); on Windows, names containingcmd.exemetacharacters (" % ! ^ & | < >) are refused rather than risk injection. config.jsonlives 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 — 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. |
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.