Files
punktfunk/plugin-kit/src/wire.ts
T
enricobuehler 110eabf281 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.
2026-07-26 18:28:13 +02:00

58 lines
2.5 KiB
TypeScript

// The library-provider wire schemas — a browser-safe module (no node imports) so plugin
// CONTRACTS can share these types with their UIs. Mirrors the host's `ProviderEntryInput`
// (crates/punktfunk-host mgmt/library.rs). Identity codecs: plain JSON shapes, so values
// pass through unencoded; the value is the shared type + authoring validation.
import { Schema } from "effect";
export const Artwork = Schema.Struct({
portrait: Schema.optionalKey(Schema.NullOr(Schema.String)),
hero: Schema.optionalKey(Schema.NullOr(Schema.String)),
logo: Schema.optionalKey(Schema.NullOr(Schema.String)),
header: Schema.optionalKey(Schema.NullOr(Schema.String)),
});
export type Artwork = typeof Artwork.Type;
export const LaunchSpec = Schema.Struct({
kind: Schema.Literal("command"),
value: Schema.String,
});
export type LaunchSpec = typeof LaunchSpec.Type;
export const PrepStep = Schema.Struct({
do: Schema.String,
undo: Schema.optionalKey(Schema.NullOr(Schema.String)),
});
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;