The store engine moves into the client, and Python goes with it
Reading the catalog, choosing the release that fits this machine, unpacking it,
writing the menu entry and remembering what went where all happen in process now.
There is no interpreter to find, no child process, and no JSON-lines protocol
between the two halves — `PythonEngineProcessRunner`, the runtime locator, the two
engine mappers and the version negotiation are all gone, and with them the one
unchecked cast this codebase had (engine stdout to a typed event).
What that buys a person: on Windows and on a fresh Mac the app simply works. It
used to look for `python3`, `python` and `py -3` and draw a link to python.org
where none answered.
What lands on disk is unchanged, deliberately. `config.json` and `state.json` keep
the shell engine's snake_case shape, its `<scope>:<name>` keys and its file modes,
so a machine whose library was installed by the CLI keeps it — verified against the
Python engine on the same catalog: the same 13-title listing with zero field
differences, byte-identical payloads, identical modes and an identical Info.plist,
and a re-sync over a Python-installed home that writes nothing. Remove, prune,
prune-suppression on a named sync and the v1 state migration were each exercised.
Three things worth knowing about the new code:
- the zip reader is ~150 lines over `node:zlib`, because Node has none and this
application has no runtime dependencies. It restores the executable bit from
each entry's external attributes, without which nothing installed can start,
and it refuses zip64, unknown compression and paths that escape the
destination rather than guessing;
- `SUPPORTED_WARP_ENGINE_VERSIONS` names the engine versions this client is
written against, checked against the `WarpEngine-Version` header every
response carries. `selectCatalogDialect` switches over that list exhaustively,
so adding a version fails the build — type checker and linter both — until
somebody says what its catalog reads like. An absent header is read as the
oldest version, which is what an engine before 0.4.0 is;
- refresh and the language picker are icons at the foot of the side menu now,
both named for a tooltip and a screen reader, the picker still a real
`<select>` under its glyph.
The repository is free of Python as well: the Makefile, the CI check and the
release script read package.json and the forge's JSON with Node.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
# warp-engine-client — the WarpEngine Client app
|
||||
|
||||
The graphical client for a WarpEngine store — the app is called **WarpEngine
|
||||
Store** — driving
|
||||
[`warp-engine-desktop-store`](https://git.teletypegames.org/stores/warp-engine-desktop-store)
|
||||
underneath: the catalog as a grid of cards, one click to install a title into your
|
||||
own application menu, one to play it, one to remove it. Linux, macOS and Windows.
|
||||
The client for a WarpEngine store: the catalog as a grid of cards, one click to install
|
||||
a title into your own application menu, one to play it, one to remove it. Linux, macOS
|
||||
and Windows.
|
||||
|
||||
The CLI stays the product; this is its front door. Every action here runs
|
||||
`desktop_store.py`, so there is one catalog logic, one state file and one delete
|
||||
guard — the window never touches the filesystem itself.
|
||||
**The store engine is part of this application.** Reading the catalog, choosing which
|
||||
release fits this machine, unpacking it, writing the menu entry and remembering what
|
||||
went where all happen in process — there is no interpreter to find and no child process
|
||||
to parse. What lands on disk has not changed: `config.json` and `state.json` keep the
|
||||
shape the shell engine wrote, so a machine whose library was installed by the CLI keeps
|
||||
it, and the engine's own defaults still decide everything a store does not configure.
|
||||
|
||||
It is also **the Windows install path**. The store's own installer is
|
||||
`curl … | sh`, which Windows does not have; this app downloads the store engine
|
||||
itself, into the same folder the shell installer would use.
|
||||
That also makes this the only install path that needs nothing of the machine. The shell
|
||||
store's installer was `curl … | sh`, which Windows does not have.
|
||||
|
||||
Which store it installs is not baked in: the client asks a registry — `GET
|
||||
/api/stores` on the site — and each record says what the store is called, which
|
||||
@@ -20,10 +20,8 @@ catalog it serves and where its configuration lives.
|
||||
|
||||
## What it needs
|
||||
|
||||
- **Python 3** on the machine, because the store is a Python program. The app
|
||||
looks for `python3`, `python` and `py -3`, and says so plainly if none answer.
|
||||
- Nothing else at runtime. No Node, no package manager, no admin rights: the
|
||||
store installs under your own user account.
|
||||
- **Nothing.** No interpreter, no package manager, no admin rights: the app carries its
|
||||
own runtime and the store installs under your own user account.
|
||||
|
||||
To *develop* it you also need **Node 22 or newer** — see below — and nothing else: the
|
||||
toolchain (TypeScript, ESLint, esbuild, electron-builder) installs with `make setup`.
|
||||
@@ -48,8 +46,8 @@ If macOS offers *Open Anyway* under **System Settings ▸ Privacy & Security** a
|
||||
a blocked attempt, that works as well. Notarisation is the only thing that removes
|
||||
the step entirely, and it needs a paid Apple Developer ID.
|
||||
|
||||
Nothing the store itself downloads is affected: Python fetches those files, and
|
||||
Python does not set the quarantine flag.
|
||||
Nothing the store itself downloads is affected: the app fetches those files over its
|
||||
own HTTP client, which does not set the quarantine flag.
|
||||
|
||||
**v1.0.0 could not be opened at all** — it reported *"is damaged"*. The bundle had
|
||||
never been signed; only its main executable carried the linker's ad-hoc signature,
|
||||
@@ -311,9 +309,9 @@ is the map** — the layers, every pattern in use, and the naming rules. The sho
|
||||
| Layer | What lives there |
|
||||
|---|---|
|
||||
| `src/shared/` | the IPC channel table, the bridge contract, the DTOs, the two message bundles |
|
||||
| `src/domain/` | models, ports and errors — no Electron, no Node, no Python |
|
||||
| `src/domain/` | models, ports and errors — no Electron, no Node |
|
||||
| `src/application/` | services and the domain → DTO mappers |
|
||||
| `src/infrastructure/` | the adapters: the Python CLI, HTTP, the filesystem, Electron itself |
|
||||
| `src/infrastructure/` | the adapters: the store engine, HTTP, the archive reader, the filesystem, Electron itself |
|
||||
| `src/main/` | the window, the IPC controllers, the composition root, the self-test |
|
||||
| `src/preload/` | the bridge, bundled into one file — a sandboxed preload cannot require modules |
|
||||
| `src/renderer/` | the state store, the views and the renderer controllers |
|
||||
@@ -334,14 +332,47 @@ Two properties are worth stating because they are what the layers buy:
|
||||
CSP that allows only its own script and stylesheet plus images over HTTPS. Links open
|
||||
in the real browser; the window itself never navigates.
|
||||
|
||||
`PythonStoreCatalogGateway` is the only class that knows the store is a Python
|
||||
program. It talks to the CLI through `--json`, which puts data on stdout and the
|
||||
human-readable log on stderr. That flag arrived with engine **1.1.0**, and the client
|
||||
checks: an older store is met with an offer to refresh it rather than a failed call.
|
||||
`NativeStoreCatalogGateway` is the engine behind the `StoreCatalogGateway` port, and
|
||||
`src/infrastructure/engine/` is the engine itself: the catalog client, the release
|
||||
picker, the host match, the payload installer, the three launcher writers and the state
|
||||
file. Above the port nothing knows any of that exists, which is the point — a second
|
||||
host would be a second gateway, not a second code path.
|
||||
|
||||
`STORE_ENGINES` has one entry today. The RetroArch store has the same command shape,
|
||||
so a second entry is the whole change needed to drive it too — that is why the table is
|
||||
there.
|
||||
`STORE_ENGINES` has one entry today. A RetroArch store writes playlists rather than
|
||||
menu entries, so it would be an entry there and a gateway of its own.
|
||||
|
||||
### Reading a zip without a dependency
|
||||
|
||||
Node has no zip reader and this application has **no runtime dependencies**, so
|
||||
`src/infrastructure/archive/ZipArchive.ts` is one over `node:zlib` — about 150 lines that
|
||||
walk the central directory, inflate `stored` and `deflate` entries, and restore the
|
||||
executable bit from each entry's external attributes. That last part is not a detail: the
|
||||
archive records it, and without it nothing the store installs can start.
|
||||
|
||||
It reads what our own release pipeline produces and refuses the rest: a zip64 archive, an
|
||||
unknown compression method and a path that would escape the destination are all errors
|
||||
rather than best guesses.
|
||||
|
||||
### Which WarpEngine served the catalog
|
||||
|
||||
Every WarpEngine API response carries a `WarpEngine-Version` header, so the client knows
|
||||
the engine's age without asking. `SUPPORTED_WARP_ENGINE_VERSIONS` lists the versions this
|
||||
client is written against, and `selectCatalogDialect` maps each one to the `CatalogDialect`
|
||||
that reads its catalog shape.
|
||||
|
||||
The switch over that list is exhaustive, which is the whole mechanism: adding a version to
|
||||
the array stops the build — in the type checker *and* in the linter — until somebody says
|
||||
what it reads like. A new engine version cannot arrive silently.
|
||||
|
||||
| What the header says | What happens |
|
||||
|---|---|
|
||||
| a supported version | its dialect reads the catalog, and the log names it |
|
||||
| nothing at all | read as the oldest supported version, which is what an engine older than 0.4.0 is |
|
||||
| older than anything supported | the same, and the log says so |
|
||||
| newer than anything supported | the newest dialect is tried anyway, with a warning that titles may be missed |
|
||||
|
||||
Three versions share one dialect today, because the catalog's shape has not changed
|
||||
across them. One class serving three versions is the honest way to say that.
|
||||
|
||||
## Verified, and not
|
||||
|
||||
|
||||
Reference in New Issue
Block a user