enricobuehler b81e03685a refactor: rewrite on @punktfunk/plugin-kit — contract + plugin workspaces
The framework half of this plugin (engine lifecycle, config/state, host
reconcile, UI server, CLI) was ~80% identical to rom-manager's. That code is
now @punktfunk/plugin-kit; this replaces the local copy with it and adopts the
rom-manager blueprint layout.

  contract/  Effect Schemas: the cross-process export contract (the C#
             exporter's other half, with the schema-version guard as a decode
             check), the config schema (one schema, Encoded = authored file
             shape, defaults only via withDecodingDefaultKey), domain DTOs, the
             HttpApi contract, API errors.
  plugin/    src/domain = the pure core (locate/read, art, reconcile), ported
             tests green before any Effect wiring; src/services = the kit's
             ConfigService/CacheStore/SyncEngine/ProviderClient; definePluginKit
             entry, kit CLI, auth-free dev API.

Preserved, because they are what makes playnite work at all: the ingest inbox
is still read FIRST (the de-privileged LocalService runner cannot reach the
interactive user's %APPDATA%), the host/dataurl/off art modes, and the
playnite:// launch hand-back. The C# exporter is untouched.

New: per-game include/exclude overrides, and an allow-listed /api/art proxy so
the SPA can render local Playnite covers — it serves only paths the currently
ingested export references.

Dropped: the standalone password UI server (console surface + CLI only, as in
rom-manager 0.3.0) and the devEntry flag.

Trap found and fixed here: the HttpApi encoder serialises `undefined` as
`null`, so a `Schema.optional` field the server omits cannot be decoded by the
client — silently in the SSE feed, loudly in the typed client. Every optional
EngineStatus field is `Schema.NullOr` with an explicit value on the wire.
2026-07-20 20:56:12 +02:00

@punktfunk/plugin-playnite

Sync your Playnite library into the Punktfunk host game library. Every store and emulator Playnite manages — Steam, GOG, Epic, Xbox, itch, RetroArch, standalone emulators, manually-added games — shows up in Punktfunk's grid on every client, and launching a title hands it straight back to Playnite, which performs the real launch.

It has two halves, both on the same Windows box as the Punktfunk host:

┌─────────────────────────────┐        ┌──────────────────────────────────────────┐
│ Playnite                    │        │ Punktfunk host  (this plugin, in the       │
│  └ Punktfunk Sync extension │  JSON  │ scripting runner)                          │
│     writes                  │ ─────▶ │  reads punktfunk-library.json, embeds art, │
│     punktfunk-library.json  │        │  PUT /library/provider/playnite            │
└─────────────────────────────┘        └──────────────────────────────────────────┘
  • The Playnite exporter (exporter/, C#) is a tiny GenericPlugin that writes your library to a JSON file whenever it changes. Playnite locks its library database while running, so reading it from inside Playnite is the only robust way — this is that adapter, and nothing more.
  • This plugin (TypeScript, on @punktfunk/host) watches that file and reconciles it into the host library as the playnite provider, with a console-hosted web UI. Zero-config auth via the runner.

Install

Two one-time steps.

1 · This plugin (on the host)

cd "$(punktfunk config-dir)\plugins"          # e.g. C:\ProgramData\punktfunk\plugins
# bunfig.toml points the @punktfunk scope at the registry (once)
bun add @punktfunk/plugin-playnite
Enable-ScheduledTask PunktfunkScripting        # enable the scripting runner if it isn't already

Open the Punktfunk console — a Playnite entry appears in the nav. (Headless box without the console? The engine is fully functional from a config.json alone — see Headless.)

2 · The Punktfunk Sync extension (in Playnite)

Download punktfunk-sync.pext from the latest CI build (the exporter job's artifact) and double-click it — Playnite installs it like any add-on. Restart Playnite once.

That's it. The console's Playnite page shows "Exporter connected" and your games sync within seconds of any library change.

The console page can also deploy the exporter for you if you'd rather not handle the .pext — it drops the extension into %APPDATA%\Playnite\Extensions\ and asks you to restart Playnite. (Best-effort: it needs read/write access to the interactive user's %APPDATA%; the .pext is the reliable fallback.)

Launching

Each title syncs with a launch command of start "" "playnite://playnite/start/<gameId>". The host runs that in the interactive Windows session, so Playnite owns the actual launch — the correct store client or emulator, exactly as if you'd hit Play in Playnite. No per-store launch mapping to maintain.

Box art

Playnite stores cover art as local files on the host. Rather than shipping image bytes in the reconcile (which doesn't scale — a large library blows past the host's request-body limit), the plugin sends the local file path and the host serves the cover through its art proxy (/library/art/…, exactly like Steam art). The payload stays tiny at any library size.

art.mode Behaviour
host Default. Send the cover's local path; the host serves the bytes. Scales to thousands of titles. Requires a host with the provider-art proxy.
dataurl Inline the cover as a size-capped data: URL (maxBytes caps one image). Self-contained, but for small libraries only — a big one exceeds the host's body limit.
off Sync titles with no art (lightest payload).

art.includeBackground additionally serves the Playnite background as hero art (off by default).

Filters

Key Default Meaning
filter.installedOnly true Only sync games Playnite marks installed.
filter.includeHidden false Include games flagged Hidden in Playnite.
filter.sources [] If non-empty, keep only these sources ("Steam", "GOG"…). Case-insensitive.
filter.excludeSources [] Drop these sources.

Edit them in the console's Playnite page, or in config.json directly.

Headless / CLI

The plugin owns <config_dir>/playnite/config.json (see config.example.json). It's fully functional from that file alone. A debug CLI mirrors the engine:

bunx punktfunk-plugin-playnite where       # where the exporter output was found (+ probed paths)
bunx punktfunk-plugin-playnite preview     # the desired library — no host write
bunx punktfunk-plugin-playnite sync        # reconcile into the live library
bunx punktfunk-plugin-playnite uninstall   # remove every entry this plugin owns

For a host without the console, set ui.standalone: true and a password (bunx punktfunk-plugin-playnite set-password <pw>); the UI then serves on ui.port (default 47994).

Requirements

  • A Windows Punktfunk host running the scripting runner (punktfunk-scripting).
  • Playnite on the same box, with the Punktfunk Sync extension installed.
  • The mgmt-API provider reconcile is loopback + token; the runner supplies both automatically.

Build from source

bun install
bun run build:all       # backend (dist/) + SPA (dist/ui/)
bun test

The exporter (needs the .NET SDK; builds net462 on any OS via reference assemblies):

dotnet build exporter/PunktfunkSync.csproj -c Release
# package the .pext:
( cd exporter/bin/Release && zip -j ../../../punktfunk-sync.pext extension.yaml PunktfunkSync.dll )

License

MIT OR Apache-2.0.

S
Description
No description provided
Readme 388 KiB
Languages
TypeScript 89.8%
C# 6.3%
JavaScript 3.1%
CSS 0.5%
HTML 0.3%