Live-verified against a running host: status frames (including a syncing
transition) now stream through the plugin UI server. The previous keepalive
outlived Bun's default idle timeout, so an idle feed was closed before its
first ping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The text-secondary re-point moved into the kit's theme.css (every plugin hits
it), joined there by the card-ring softening the console does in its own Card
wrapper. The SPA's stylesheet is back to imports only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Preview imports the mock transport statically and wraps every story in
a fresh RegistryProvider seeded with mock mode + the story's scenario
parameter (dark default, light via toolbar). One story file per page
(Default + FirstRun/Syncing/Errors where meaningful) plus the whole
shell. tools/screenshots.mjs mirrors the console's harness: headless
Chromium over storybook-static, every Pages/* + Shell/* story.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AtomHttpApi.Service derives the typed client straight from the shared
RomManagerApi contract — no hand-mirrored types, no react-query. Queries
carry reactivity keys; mutations pass them per-call (constants exported
beside each mutation). liveStatusAtom merges the kit sseAtom feed over
GET /api/status; statusEventsAtom folds frames into the activity feed.
useConfigDraft keeps a raw-config working copy (defaults never baked in)
with dirty tracking and toast-reporting save.
Mock mode is a full in-memory RomManagerApi as an HttpClient layer plus
a scenario ticker (healthy/firstRun/syncing/errors), registered through
a fixture-free seam so production chunks stay clean — mocks/http.ts is
only imported behind import.meta.env.DEV and by Storybook. Scenario and
mode atoms are keepAlive: the transport reads them outside the reactive
graph and registry-seeded values must survive pages that never subscribe
them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New workspace member built on @unom/ui 0.9.1 + @unom/app-ui 0.2.0 +
@punktfunk/plugin-kit/react, effect 4.0.0-beta.99 exact. base:'./' with
build straight into ../plugin/dist/ui; :5599 dev server proxies /api to
the plugin dev API only in live mode. styles.css = tailwind + tw-animate
+ the kit's violet theme (NOT @unom/ui's own neutral palette) + bare-dir
@source globs over the design-system dist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RomConfig/RomCache on the kit's ConfigService/CacheStore; RomSync wires the
pure domain into the kit SyncEngine (poll/watch/coalesce/fingerprint) and
ProviderClient; EngineStatus assembly shared by REST + SSE
- makeApi: HttpApiBuilder groups over live service values (handler runtime
shares the plugin runtime's singletons) + sseRoute status feed
- index.ts: definePluginKit entry (async-main boundary); headless-first (UI
serve failure logged, engine continues)
- cli.ts on the kit dispatcher: scan/detect/preview offline, sync/uninstall
online; set-password is gone with the standalone server
- dev.ts: auth-free loopback API server (:5885) — the dev:live proxy target;
never bundled
- verified live host-less: raw config round-trip on disk, status/platforms/
preview endpoints, soft-fail reconcile without a host
- CI: workspace install, per-package typecheck, publish from plugin/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bun workspaces: contract/ (Schemas = source of truth: config raw↔resolved,
domain DTOs, HttpApi contract, API errors) + plugin/ + ui/
- pure domain moved src/{engine,art}→plugin/src/{domain,art}, types now
imported from @rom-manager/contract and @punktfunk/plugin-kit/wire (the
hand-copied wire.ts dies), loggers injected instead of module-global
- ui.* and devEntry dropped from the config shape (standalone server is
gone; stale keys tolerated on decode, dropped by the next save)
- all 48 domain tests ported and green BEFORE any Effect wiring
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The check imported the default export as `p` and then read `p.default` —
a double-default that is always undefined for a module using
`export default plugin`, so the build job failed on every push (publish
skipped) and v0.2.x never actually published through CI. Read the default
import directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The managed runner is de-privileged on Windows now (runs as LocalService),
and %ProgramData%\punktfunk is locked read-only to it — so writing config/
cache straight under the config dir fails EPERM (proven on-glass). Move the
plugin's state to <config_dir>/plugin-state/rom-manager, which
`punktfunk-host plugins enable` grants the runner write on. On Linux the
runner owns the config dir, so the path is writable there too — one path, no
branch.
Migrate a pre-0.2.1 config/cache from the old <config_dir>/rom-manager on
first load (copy, guarded, best-effort) so an existing operator's roots +
launch templates survive the move instead of silently resetting to defaults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workflow used runs-on: ubuntu-latest, which no Gitea runner is labeled with,
so nothing ran. Mirror the main repo's sdk-publish.yml: ubuntu-24.04 runner, the
oven/bun:1 container (install git+CA certs for checkout), and auth via the shared
REGISTRY_TOKEN secret written to ~/.npmrc (covers @punktfunk + @unom).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reliability + correctness pass on the packaging and UI.
Packaging — drop the self-contained bundle:
- Rename to `@punktfunk/plugin-rom-manager` (scoped). An unscoped package can't
be split across registries, which is the only reason bundling effect + the SDK
into every plugin seemed necessary. Scoped names resolve from one scope-map, so
the plugin depends on `@punktfunk/host` + `effect` as SHARED (hoisted) deps —
no duplication, no ~1 MB effect copy per plugin. Build is back to plain `tsc`.
(Runner-side discovery of `@punktfunk/plugin-*` shipped in @punktfunk/host 0.1.1.)
UI — use the console's design system so it reads as family:
- SPA rebuilt on @unom/style tokens + Tailwind v4 + the console's exact theme
(violet chrome, Geist, card/button vocabulary). Build-time only → static assets
in dist/ui, no @unom runtime dep. Skips @unom/ui's AnimatedButton/Card, which
statically import ~7 MB of UI sound assets — same look, 416 KB dist/ui.
- Add a custom-emulator editor (Emulators page) writing config.emulators — you can
now add a launcher Punktfunk doesn't ship a definition for.
- Fix brand capitalization to "Punktfunk" in all UI copy + docs.
Install docs + CI updated for the scoped `bun add @punktfunk/plugin-rom-manager`
flow. 48 engine tests green; backend + UI typecheck + biome clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unscoped package lives on the Gitea npm registry but its deps span two
registries (`@punktfunk/host` on Gitea, `effect`/`undici` on npmjs). Installing
by name with `--registry <gitea>` sets the default registry for the whole tree,
so `effect` 404s (Gitea doesn't proxy npm), and an unscoped name can't be
scope-mapped. Fix: bundle the backend into ONE self-contained `dist/index.js`
(+ `dist/cli.js`) via `bun build --target=bun` with `@punktfunk/host` + `effect`
inlined — mirroring the scripting runner. The published package now has zero
runtime dependencies and installs with a single `--registry` flag.
- build → `bun build src/{index,cli}.ts --target=bun --outdir dist --external undici`
- @punktfunk/host + effect → devDependencies (bundled, not resolved at install)
- resolveUiDir(): walk to the SPA dir so staticDir works in the bundled
(dist/index.js) and unbundled (dist/server/index.js) layouts
- version 0.1.0 → 0.1.1
Verified: the bundle loads + scans/detects/previews with NO node_modules present.
A punktfunk-plugin-* package that scans ROM directories, maps them to
emulators, fetches box art, and reconciles them into the host game library
under provider id `rom-manager` — with a console-hosted web UI.
Engine (pure, unit-tested core):
- Table-driven platform registry (~25 consoles) + emulator registry with
best-effort per-OS detection (PATH / Flatpak / known paths) and RetroArch
core discovery.
- Scanner with disc folding (m3u/cue/gdi), archive gating, excludes.
- No-Intro title parsing + optional per-platform region dedupe.
- Security-critical quoting seam: POSIX single-quote + Windows double-quote
with hostile-name refusal; ROM filenames never reach a shell un-quoted.
- Pure desired-state reconcile (stable external_ids, scale guard, fingerprint
skip) → full-replace PUT /library/provider/rom-manager.
Box art (like Steam ROM Manager): SteamGridDB primary (portrait/hero/logo/
header, fuzzy match, operator API key) behind a provider seam, with keyless
libretro-thumbnails as the zero-setup fallback (`auto` default).
UI: console-hosted via the SDK `servePluginUi` (zero plugin-side auth) with a
plugin-local REST/SSE API and a self-contained React SPA (Setup / Emulators /
Games / Sync). Standalone password-gated fallback for host-only installs.
CLI: scan / detect / preview / sync / uninstall / set-password.
48 engine tests, typecheck + biome clean, SPA builds. Verified end-to-end:
scan → detect → reconcile PUT, fingerprint idempotence, and the standalone
UI serving SPA + REST.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>