One-liner plugin management replacing the manual scripting-dir + bunfig +
bun-add ritual: package ops forward to the bun runner (new sdk plugins
module + runner-cli subcommands, 11 tests green), enable/disable/status
drive the systemd unit on Linux and the PunktfunkScripting scheduled task
on Windows (installer support in the ISS). Docs page rewritten as .mdx
with per-platform Tabs (registered in mdx.tsx).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runner only found unscoped `punktfunk-plugin-*` packages. An unscoped
package can't be split across registries, which forced plugins to bundle their
own copy of @punktfunk/host + effect (the plugin's Gitea registry has no
`effect`; `--registry <gitea>` 404s it). Discovering the scoped
`@punktfunk/plugin-*` convention lets a plugin resolve cleanly from one scope
map and depend on @punktfunk/host + effect as SHARED (hoisted) deps instead of
bundling — no per-plugin duplication. Additive: unscoped names still work.
Bumps @punktfunk/host to 0.1.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerated api/openapi.json (host built on Linux) with the plugins tag —
registerPlugin/listPlugins/deregisterPlugin/getPluginUiCredential + the
PluginRegistration/PluginSummary/PluginUi/PluginUiPublic/UiCredential schemas —
and the SDK's generated client. Drift test + 44 mgmt/events host tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- package.json: drop private; point main/types/exports/bin at a tsc-built dist/;
add publishConfig (unom/npm registry), files, repo metadata, and the
MIT OR Apache-2.0 license; effect becomes a peerDependency (shared instance).
- tsconfig.build.json: emit dist/ JS + .d.ts (bun shebang preserved on the bin).
- .npmrc: map the @punktfunk scope to the registry (no token committed).
- sdk-publish.yml: publish on sdk-v* tags or manual dispatch, reusing the
REGISTRY_TOKEN secret; typecheck/test/build/tag-matches-version gate.
- README: Install section for consumers.
Verified: build green, frozen lockfile stable, bun publish --dry-run packs
@punktfunk/host@0.1.0 (dist + README only) to the unom registry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add sdk/examples/virtualhere-dualsense.ts — bind a real USB DualSense (shared
from the couch via VirtualHere USB-over-IP) to the host for the length of each
connection and release it after, for full gyro/touchpad/adaptive-trigger/USB-
rumble passthrough instead of the emulated pad. Brackets on client.connected/
disconnected and releases the pad on SIGTERM for a clean runner stop.
Document it in the Events & hooks page with a zero-code hooks.json variant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop Orval for the first-party @effect/openapi-generator (OpenAPI 3.1 ->
Effect Schema + a typed HttpClient client) and bump effect 3.19 ->
4.0.0-beta.98. Port the hand-written surfaces to the v4 API (Result over
Either, Context.Service, Codec, Literals/Union arrays, Stream/Schedule/
Effect renames). Transport (CA-pinning fetch) and the reconnecting SSE
source are kept intact.
Make the SDK approachable for non-Effect users:
- Add pf.api.* on the Promise facade: the generated client surfaced as
typed, Promise-native methods (await pf.api.listPairedClients()), so REST
calls are autocompleted and checked instead of stringly-typed
pf.request(method, path, body) + `as` casts. Zero-drift veneer over
make(httpClient), backed by the same pinning fetch. pf.request stays as
the untyped escape hatch.
- Re-tier examples into a 1-4 complexity ladder, rewritten onto pf.api.*
(the typed payloads caught a wrong `launch` shape in provider-sync);
the Effect example is labelled advanced. Add examples/ to tsconfig so
they are typechecked (stops rot).
typecheck + 19 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The optional supervision layer (RFC §8): one service runs everything in
<config_dir>/scripts/ plus installed punktfunk-plugin-* packages
(<config_dir>/plugins/node_modules/), as Effect fibers.
- Plugins (a definePlugin default export, either main shape) are
SUPERVISED: a failure restarts them with capped exponential backoff
(jittered, 1s→60s); a clean return completes them. The Effect shape
runs under the PunktfunkHost layer; the async-fn shape gets a facade
client whose close is scope-guaranteed.
- Bare scripts are one-shot: importing them is the run, no restart
(export a plugin to be supervised).
- Shutdown is STRUCTURAL: SIGINT/SIGTERM interrupt the whole fiber tree,
so Effect plugins' scoped finalizers run and clients close before
exit — the systemctl-stop story, and the reason the Effect plugin
shape exists at all.
- The sshd rule applies to unit files (world-writable → refused loudly);
cache-busted imports make restarts real; --list for inventory.
6 new bun tests (17 total green): discovery + refusal, both plugin
shapes against a mock host, crash→restart with backoff, one-shot
semantics, and finalizer-on-interrupt. Live-verified against a real
host: a supervised watcher plugin received library.changed through the
pinned tunnel, and SIGTERM shut the tree down structurally (exit 0).
Deferred to the packaging follow-up (release.yml is in flight in a
parallel session): the vendored-Bun deb/rpm/iss packages and the
host-log-ring tee (needs a host ingest endpoint); console page rides
the other console surfaces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External game-library providers become first-class (RFC §8): a plugin
computes its desired title list and PUTs it — the host owns the diff.
- CustomEntry gains `provider` + `external_id` (API-set only; never on
manual entries). GameEntry surfaces `provider` for console attribution
and the new `GET /library?provider=` filter.
- PUT /api/v1/library/provider/{p}: atomic declarative reconcile keyed
on the provider's `external_id` — host ids stay stable across syncs,
orphans drop, manual entries and other providers are never touched,
an empty array clears the set. Validated: provider id [a-z0-9._-]
(`manual` reserved), unique non-empty external_ids.
- DELETE /api/v1/library/provider/{p}: clean uninstall, returns the
removed count.
- Ownership is unambiguous both ways: manual CRUD now returns 409 for a
provider-owned entry (MutateOutcome::ProviderOwned) instead of letting
an edit be silently clobbered at the next sync.
- library.changed now carries the mutating source (`manual` or the
provider id) — hooks and the SDK filter on it.
- Spec + SDK schemas regenerated; sdk/examples/provider-sync.ts is the
provider-plugin skeleton.
347 host tests green (pure reconcile: stable ids, orphan drop,
idempotence, bystanders untouched; name/payload validation; route 400s)
+ 11 SDK tests. Live-verified end to end THROUGH the SDK against a real
host: sync → filtered list → manual-delete 409 → re-sync with stable id
+ orphan drop → uninstall (removed=2), with three
library.changed(source=romm) events observed on the live stream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New top-level sdk/ package (RFC §7): a typed management-API client plus
the lifecycle event stream, built on Effect, two surfaces over one core:
- @punktfunk/host — the Promise facade front door: connect() resolves
URL/token/TLS pin from the host's own files (zero config on the box),
fails fast on bad credentials, pf.events.on() with typed callbacks
(exact kinds, domain.* prefixes, "*", "dropped", "unknown"),
pf.request() for the REST surface. Effect never required.
- @punktfunk/host/effect — the PunktfunkHost service + PunktfunkHostLive
layer, Stream-based events()/eventsRaw(), typed errors
(AuthError | ApiError | TransportError | VersionSkew — a 2xx that
fails its schema is a typed skew, not undefined later), and every
wire shape as an effect/Schema: REST generated via orval
client:'effect' from api/openapi.json (S3 spike: works well; the
text/event-stream payload is out of its reach), events hand-mirrored
from the host's snapshot-tested wire format as a kind-discriminated
union.
One reconnecting SSE core under both surfaces: spec-shaped parser,
exponential+jittered backoff (capped, resets after a healthy
connection), Last-Event-ID resume, 401 terminal. Default is LIVE tail
only — a fresh notify script must not re-fire on the host's replayed
ring (since: 0 opts into full replay).
TLS: the pin trusts exactly the host's self-signed identity cert
(chain-verified; hostname check waived — the cert is deliberately
CN-only for fingerprint pinning). Bun via fetch tls, Node via an undici
dispatcher (optionalDependency).
definePlugin() accepts both main shapes (async fn | Effect requiring
PunktfunkHost). Examples in both styles; README carries the compat
contract + systemd/Task Scheduler templates.
11 bun tests green (wire decode against the Rust snapshot strings,
SSE parser/reconnect/Last-Event-ID/401, both surfaces vs a mock host).
Live-verified against a real host on Bun AND Node through the pinned
loopback hop: connect → REST mutate → live event received → resume
cursor advanced; a wrong CA is rejected. npm publish + CI wiring
deferred (npm org = RFC open question 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>