The client had our store's config URL compiled into it, which meant a second store
— or anybody else's site — needed a release of this app. It now asks
`GET /api/stores` and installs what the site offers: one record and there is
nothing to decide, several and the setup screen shows a picker.
From a record the client works the rest out. `storeRepositoryUrl` gives the
`config.json` to read, and that file stays the authority on how the store behaves;
`catalogUrl` and `name` override its `store.base_url` and `store.name`, because the
registry is what says which catalog a store is *for*. The store id — which names
the store home and the folder games land in — comes from the repository name, so
`ttg-desktop-store` becomes `ttg`.
A repository with no `config.json` still installs: the engine merges whatever it
is handed onto its own defaults, so the client writes a three-field config and the
store behaves like the default one pointed at that catalog. That was worth having
rather than an error, and it is tested.
The registry address is now the single thing about a particular site left in the
client, and `STORES_API` overrides it — which is how this was tested, against a
local endpoint serving the same payload the site returns, with one store that has a
config and one that has not. Both installed; the engine listed all ten titles with
the synthesised config.
`npm run uitest` now passes on either outcome — the grid when a store is present,
the setup gate with a populated picker when there is none — and it reports both, so
the gate cannot silently regress into an empty screen. Run with the registry
unreachable it produces the retry gate, and fails, which is the honest verdict: a
client that cannot reach the registry cannot set anything up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desktop store put the catalog on ordinary computers, and then asked people to
open a terminal — which on Windows is not even a workable ask, because the
installer is `curl … | sh`. This is the window: a grid of cards, one click to
install a title into the application menu, one to play it, one to remove it.
The CLI stays the product. Every action runs `desktop_store.py --json`, so there
is one catalog logic, one state file and one delete guard; the window never
touches the filesystem itself. That is also why the engine grew `--json` first
rather than this app growing a parser for prose.
It doubles as the Windows install path: with no store on the machine, the app
downloads the engine, the shared core and a config into the same folder the shell
installer would use — Node's https, no curl. An engine older than 1.1.0 cannot be
driven from a window, so the client checks the version and offers to refresh it
instead of failing on the first call.
Deliberate choices worth knowing:
- No renderer framework and no build step. Plain HTML, CSS and JS, one runtime
dependency. The whole UI is readable in one sitting.
- `contextIsolation` on, `nodeIntegration` off, `sandbox` on, a CSP that permits
only the app's own script and stylesheet plus images over HTTPS. The renderer
can do exactly what preload.js exposes and nothing else.
- English and Hungarian, following the system language. The CLIs and the docs stay
English; this is the one end-user surface where that is not enough.
- `ENGINES` is a list with one entry. The RetroArch store has the same command
shape, so adding it is an entry, not a rewrite.
Two ways to test it without a working installation in the way: `npm run smoke`
drives the bridge with no window at all, and `npm run uitest` loads the window
once and reports what rendered — the only way a renderer error would otherwise be
noticed, since the main process log stays empty. Both accept a sandbox store
through STORE_ROOT / SMOKE_HOME.
Verified on macOS arm64, including the packaged .app: the store is found, ten
titles list, a sync installs three, and the window renders them as installed with
their Play and Remove buttons. Linux and Windows are unproven, as they are for the
CLI itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>