feat(library/providers): let a provider say how to recognize its games
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.
This commit is contained in:
+22
-1
@@ -45,7 +45,7 @@ export default definePluginKit({
|
||||
| `HostClient`, `PluginInfo` | the `pf` facade as services (`request` = the skew-safe untyped seam) |
|
||||
| `makeConfigService` | Schema-driven config: raw shape on disk, defaults ONLY in the Schema (`withDecodingDefaultKey` + `encodingStrategy: "omit"`), atomic writes, world-writable refusal, `changes` stream |
|
||||
| `makeCacheStore` | disposable derived state (corrupt/absent → empty, write-through) |
|
||||
| `ProviderClient` + wire schemas | typed library-provider reconcile over the untyped wire |
|
||||
| `ProviderClient` + wire schemas | typed library-provider reconcile over the untyped wire — including the optional `detect` hint (see below) |
|
||||
| `makeSyncEngine` | poll + fs-watch + debounce + single-flight coalescing + fingerprint skip + status feed |
|
||||
| `serveUi` / `httpApiEnv` | an `effect/unstable/httpapi` HttpApi behind the SDK's `servePluginUi`, core-only layers |
|
||||
| `sseRoute` | the status SSE endpoint (httpapi has no event-stream media type) |
|
||||
@@ -54,6 +54,27 @@ export default definePluginKit({
|
||||
| `@punktfunk/plugin-kit/react` | browser glue: `createPluginRouter` (path→hash→fallback deep-link restore + `pf-ui:navigate`), `resolvePluginBase`, `useIsEmbedded`, `ResultGate`, `sseAtom` |
|
||||
| `@punktfunk/plugin-kit/theme.css` | the console's violet identity for plugin UIs (import first in your Tailwind entry) |
|
||||
|
||||
## Telling the host how to recognize a running title (`detect`)
|
||||
|
||||
A `ProviderEntry` may carry an optional `detect` hint:
|
||||
|
||||
```ts
|
||||
{ external_id: "playnite:9f2…", title: "Hades",
|
||||
launch: { kind: "command", value: "playnite://playnite/start/9f2…" },
|
||||
detect: { install_dir: "D:\\Games\\Hades" } }
|
||||
```
|
||||
|
||||
It is what lets the host tell that the *game* has exited — which ends the streaming session, so the
|
||||
player's client returns to its library instead of showing a bare desktop — and what lets an operator
|
||||
who opted into it end the game when the session ends.
|
||||
|
||||
Omit it and nothing breaks: the host tracks the process it spawns for your launch command. It matters
|
||||
when that command **hands off and exits** — a launcher client, `flatpak run`, a front-end that starts
|
||||
an emulator — because then there is nothing left for the host to watch, and both behaviors go quiet
|
||||
for that title. Send whatever you genuinely know; `install_dir` is the one to send if you send only
|
||||
one, since any process running from under it counts as the game. The host never lets a hint override
|
||||
what it worked out itself, and never adopts a process that was already running before the launch.
|
||||
|
||||
## Publishing
|
||||
|
||||
Tag `plugin-kit-vX.Y.Z` (matching `package.json`) — `.gitea/workflows/plugin-kit-publish.yml`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@punktfunk/plugin-kit",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"description": "Effect-based framework for punktfunk plugins: lifecycle runtime, config/state, sync engine, UI serving, CLI scaffold, and browser helpers.",
|
||||
"type": "module",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
|
||||
@@ -20,6 +20,7 @@ export { type ConfigService, makeConfigService } from "./config.js";
|
||||
export { type CacheStore, makeCacheStore } from "./cache-store.js";
|
||||
export {
|
||||
Artwork,
|
||||
DetectHint,
|
||||
LaunchSpec,
|
||||
PrepStep,
|
||||
ProviderClient,
|
||||
|
||||
@@ -24,11 +24,34 @@ export const PrepStep = Schema.Struct({
|
||||
});
|
||||
export type PrepStep = typeof PrepStep.Type;
|
||||
|
||||
/**
|
||||
* How the host should recognize a title's process once it is running.
|
||||
*
|
||||
* Every field is optional, and omitting the whole thing is fine: the host tracks the process it
|
||||
* spawns for the entry anyway. It matters when your launch command hands off and exits — a launcher
|
||||
* client, a `flatpak run`, a front-end that starts an emulator — because then the host has nothing
|
||||
* left to watch, and the two behaviors this feeds ("end the session when the game exits" and "end the
|
||||
* game when the session ends") go quiet for that title.
|
||||
*
|
||||
* Send whatever you actually know. `install_dir` is the one worth sending if you send only one: any
|
||||
* process running from under it counts as the game.
|
||||
*/
|
||||
export const DetectHint = Schema.Struct({
|
||||
/** Where the title is installed (absolute path on the host). */
|
||||
install_dir: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
||||
/** The game's own executable (absolute path on the host). */
|
||||
exe: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
||||
/** The executable's file name (`Hades.exe`), when its location isn't fixed. Weakest signal. */
|
||||
process_name: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
||||
});
|
||||
export type DetectHint = typeof DetectHint.Type;
|
||||
|
||||
export const ProviderEntry = Schema.Struct({
|
||||
external_id: Schema.String,
|
||||
title: Schema.String,
|
||||
art: Schema.optionalKey(Artwork),
|
||||
launch: Schema.optionalKey(Schema.NullOr(LaunchSpec)),
|
||||
prep: Schema.optionalKey(Schema.Array(PrepStep)),
|
||||
detect: Schema.optionalKey(DetectHint),
|
||||
});
|
||||
export type ProviderEntry = typeof ProviderEntry.Type;
|
||||
|
||||
Reference in New Issue
Block a user