Emulation-and-beyond libraries need more than a title and a poster. Every
library shape (GameEntry, CustomEntry, CustomInput, ProviderEntryInput)
now carries a shared, flattened GameMeta: platform, description,
developer, publisher, release_year, genres, tags, region, players. All
fields are optional and flat on the wire, so existing library.json files,
provider plugins, and clients keep working unchanged.
- Installed-store scanners (Steam, Lutris, Heroic, Epic, GOG, Xbox) stamp
platform=PC; custom/provider entries carry whatever was authored.
- GET /library grows a ?platform= filter (case-insensitive) beside
?provider=.
- Console: the add/edit form gets a Details section (round-tripping every
field, since update replaces the entry), the poster tile a platform
badge (non-PC only — the store badge already implies PC) and the year.
- plugin-kit: ProviderEntry accepts the same fields (new GameMeta schema,
spread flat); SDK + OpenAPI spec regenerated.
- pf-client-core decodes platform for future client badges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A plugin's titles launch through the provider's own client, which hands off and
exits — so the host had nothing left to watch, and both lifetime behaviors went
quiet for exactly the entries a provider contributes. A `ProviderEntry` (and a
manual custom entry) may now carry an optional `detect` hint: install dir, exe,
or process name.
It is deliberately a subset of what the host tracks internally. A Steam appid or
a launcher's environment marker are things the host discovers for itself and
would be meaningless — or dangerous — to take on someone's word; where a title
is installed is something only the provider knows. The host's own findings win
where both exist, so a stale export can never redirect the matcher, and a blank
field is treated as absent rather than as "match everything" — an empty install
dir would otherwise prefix-match every process on the box, and this feature can
end processes.
`process_name` is the weakest of the three and the only one typed by hand, so it
is matched case-insensitively against the image's file name and nothing else:
`retroarch` finds RetroArch, not a helper whose name merely starts the same way,
and not a script that happens to live in a `retroarch/` directory. The
never-adopt-a-pre-existing-process rule still bounds it.
Also: the tray summary gains the running-game row (with the closing-in countdown
for a game whose client is gone — visible at the machine without opening the
console), the SDK mirrors the `game.*` events, and its generated client catches
up with the endpoints Phase 1 added.
Gates on .21: check + clippy --all-targets clean, 299 tests, fmt CI-parity,
openapi regenerated (GameEntry still carries no `detect` outbound); SDK tsc +
54 tests green.
servePluginUi's Bun.serve leaves idleTimeout at Bun's 10s default, so the old
15s keepalive could never arrive — an idle status feed was closed before its
first ping, which is exactly what a live host showed. Default is now 5s.
Also adds the regression suite that should have caught it: the original test
only covered a finite, self-driving stream. The new one exercises the
production shape (PubSub-backed, published after the response opens) plus the
keepalive — with a reader that does not re-enter read(), which is what made the
first version of this test lie.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@unom/ui rings cards ring-2 ring-accent, sized for its own subtle accent; in
this palette --accent is the brand violet, so every card shouted. The console
already softens the same ring in its Card wrapper — plugin UIs now inherit that
instead of each wrapping AnimatedCard themselves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@unom/ui reads --secondary as muted TEXT; this palette (like the console's) uses
it as a dark SURFACE, so EmptyState captions, badge text and table headers
rendered dark-on-dark. Tailwind derives both utilities from one token, so the
text lane is re-pointed at --muted-foreground here — once, for every plugin,
instead of in each plugin's stylesheet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plugin contracts share ProviderEntry/Artwork/LaunchSpec/PrepStep with their
UIs; the kit root stays node-only (reconcile keeps the HostClient transport).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ~80% of every plugin that was copy-pasted (rom-manager ↔ playnite),
extracted as one Effect-v4 package:
- runtime: definePluginKit — async-main boundary hiding a ManagedRuntime
(two-effect-instances discipline), signal-driven interruption with a
bounded shutdown grace
- config: Schema-driven raw round-trip (defaults only in the Schema via
withDecodingDefaultKey + encodingStrategy omit; file stays authored-shape),
atomic writes, world-writable refusal, changes stream
- cache-store: disposable derived state, corrupt→empty, write-through
- reconcile: kit-owned provider wire schemas + typed client over the
skew-safe untyped pf.request seam
- sync-engine: generic poll/watch/debounce/single-flight-coalesce/
fingerprint-skip engine with a status PubSub (the SSE feed)
- ui-server + sse: effect/unstable/httpapi behind servePluginUi with
core-only env layers (validated by the phase-0 spikes); raw SSE route
(httpapi has no event-stream media type)
- cli: minimal command dispatcher reusing the plugin layer graph
(deliberately not effect/unstable/cli — it needs platform packages)
- react subpath: plugin router (path→hash→fallback deep-link restore +
pf-ui:navigate bridge), ResultGate, sseAtom, resolvePluginBase
- theme.css: the console's violet identity packaged for plugin UIs
18 bun tests incl. the two phase-0 spikes; publish workflow mirrors
sdk-publish (tag plugin-kit-v*; 0.1.0 published manually).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two spikes validating the riskiest seams of the plugin-kit design before
implementation:
- HttpApi (effect/unstable/httpapi) served on Bun behind the SDK's
servePluginUi using ONLY effect-core layers (Etag.layerWeak, Path.layer,
HttpPlatform.layer + FileSystem.layerNoop) — no platform package. Auth,
schema validation, and static fallthrough all verified end-to-end.
- Browser client prefix strategy for the console proxy: both
transformClient+prependUrl AND baseUrl preserve the /plugin-ui/<id>
path prefix. Nested Schema.withDecodingDefaultKey defaults confirmed
(Effect-valued defaults; encodingStrategy "omit" restores raw shape
on encode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>