3d63c8a0b0976b252e395565ac936dbb8f0149c8
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d63c8a0b0 |
TypeScript, in layers, with a strict linter
The client was one main.js, one preload.js, three files in lib/ and one renderer
script. It is now a typed application whose imports point inward: domain (models,
ports, errors) knows nothing about Electron, Node or Python; application orchestrates
it through those ports; infrastructure holds the adapters — the Python CLI, HTTP, the
filesystem, Electron itself — and main, preload and renderer sit on top as hosts.
STRUCTURE.md is the map, and the deliverable as much as the code is: every layer, every
pattern in use (ports and adapters, repository vs gateway, service, DTO and mapper,
composition root, controller and router, single flight, observer streams, state store
with unidirectional flow, passive view, coded error hierarchy, frozen constant tables,
untrusted-data readers) and the naming rules — files, classes, and a verb vocabulary
for methods where find/require/read/list/apply/render/handle each state a contract.
Two properties fell out of the move, and they are why it was worth doing:
- The catalog can be driven with no window and no Electron at all. The smoke test
assembles the same services against the same ports in a plain Node process; it used
to be a script that reimplemented the bridge.
- The window never receives a filesystem path. A title crosses the bridge without
one, and launching is asked for by name, resolved in the main process from the
store's own state. Verified with a fake launcher: an unknown name answers false, a
native title resolves to its menu entry, a hosted one to its catalog URL.
Types are mandatory, including where inference would manage: explicit return,
parameter and property types, strict plus noUncheckedIndexedAccess,
exactOptionalPropertyTypes, noImplicitOverride and noPropertyAccessFromIndexSignature,
typescript-eslint strictTypeChecked and stylisticTypeChecked, exhaustive switches, no
any, no non-null assertions, and no casts on foreign data — engine stdout and the
registry go through readers that turn unknown into typed values. naming-convention
enforces the patterns rather than trusting them.
Two rule conflicts had to be decided rather than papered over. typedef and
no-inferrable-types disagree about `fallback: string = ''`: the annotation wins, since a
signature states its types. erasableSyntaxOnly is off, because it forbids constructor
parameter properties, which are how dependencies are declared here.
The preload and the renderer are bundled by esbuild into one file each: a sandboxed
preload may not require its own modules, and a module script over file:// is blocked by
the page's own origin rules. tsc compiles the rest. The package ships build/** and
package.json — 111 entries, no sources, no toolchain.
New targets: build, typecheck, lint, lint-fix, and check — typecheck, lint, then both
test suites, cheapest failure first. Every script that runs the app builds first, so a
stale bundle cannot be tested.
Nothing about the window changed: same side menu, same categories, same switcher, same
two languages. make check is clean, both test suites pass with one store and with two,
the packaged 1.3.0 bundle drives the real store, and the window was photographed before
and after — the two are the same picture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cd5361e222 |
A side menu, categories, and a store switcher
Everything that is not a title moved out of the bar into a menu on the left that folds away with the button in the bar; the state is remembered between runs. It carries the stores on this machine, the two actions, the categories and the language. Categories narrow the grid one at a time, with counts: the state of a title on this machine, then a row per platform and per kind. The axes are built from what the catalog contains — a WarpEngine catalog has no genre — and a category that disappears under you falls back to Everything rather than leaving a blank grid. With more than one store installed, clicking another in the menu opens it: the grid, the categories and the folders follow, the choice is remembered, and stores sharing an id are told apart by their folder. app:state now reports every store, store:use switches, and the engine is re-checked per store because two stores can be at different versions. While the CLI runs, the menu, the log drawer and the filters stay live; only what would start a second call is disabled. Fixes the grid, which was broken and passing every check: its implicit rows split the window's height evenly instead of following their content, so every card came out 94px tall with its box art collapsed to nothing and its buttons clipped away. The DOM was intact throughout — ten cards, twenty buttons — which is exactly why the counts said nothing. Rows are content-sized now, and the art is one band of one height, with the title's first letter where the catalog has no image. So the window is looked at and not only counted, `SELFTEST_SHOT` has it photograph itself; the terminal has no screen-recording permission here. The selftest also clicks the store that is not open when there are two, and checks that the bar, the grid and the categories follow. Also fixes scripts/release.sh, which could not upload a package whose name has a space in it — "WarpEngine Store-1.1.0-arm64.dmg" does — because the list was one string split on whitespace. And `make release` cleans dist/ first, so a release cannot pick up the previous version's artifacts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cb8a28b156 |
Ask a registry which store to install
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> |
||
|
|
e635a032ea |
Sign the macOS bundle, or it arrives "damaged"
The first release could not be opened: macOS said "WarpEngine Store is damaged and can't be opened. You should move it to the Bin." Not a wording problem — an integrity one. electron-builder found no signing identity and skipped signing, so the bundle kept only the linker's ad-hoc signature on its main executable, with no resource seal. `codesign --verify` said "code has no resources but signature indicates they must be present", and Gatekeeper reports that as damaged and offers no way past it, unlike an un-notarised app which can at least be approved. `scripts/after-pack.js` now signs the bundle itself during packaging. Measured on a copy unzipped from the artifact with the quarantine flag set by hand: before code has no resources but signature indicates they must be present after valid on disk; satisfies its Designated Requirement and the identifier is ours rather than `Electron`. `syspolicy_check` is down to its expected "adhoc signed" warning. A downloaded copy still has to be approved — that is Gatekeeper policy for anything un-notarised, and notarisation needs a paid Developer ID — so the README and the release notes lead with the one command that does it. Two smaller things the failure turned up: - The self-test was passing silently. With a copy of the app already open, the second process lost the single-instance lock and exited 0 with no output, which reads exactly like success. It now uses its own user-data directory and skips the lock, and it caught a real launch failure immediately afterwards. - The README claimed right-click ▸ Open was enough. It was not, and I had not checked it — replaced with what the measurements support. v1.0.0's attachments are withdrawn rather than left downloadable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f88340d63c |
An Electron client for the desktop store
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> |