Read a store's config from the registry record
`GET /api/stores` records now carry a `config` field — a store's `config.json` moved into the record that already said what the store is — and the client applies it directly. Installing a store no longer depends on a second repository existing and staying reachable, and a store can be configured from the site's admin alone. The order is registry config, then a repository's `config.json`, then the engine's defaults. The middle one is why nothing has to move at once: a registry whose stores have not been migrated is read exactly as before. The window cannot supply a config. It is handed stores to show and hands one back to install, but only as an identity: `RegistryStoreDtoMapper.toModel` drops the config and `StoreProvisioningService` reads the record again from the registry first. A config decides where files are written and, through `paths.subfolder`, which subtree the store may later delete from — not a decision the renderer gets to make, for the same reason a `GameDto` carries no paths. Tested by installing from a record carrying `subfolder: "ATTACKER"` and `install_root: "/tmp/pwned"` and finding neither on disk. A store that has left the registry, or a registry that cannot be re-read, still installs: it falls back to the engine's defaults rather than refusing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,35 +62,49 @@ and there is nothing to decide; several and the setup screen shows a picker.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{ "name": "Teletype Games", "catalogUrl": "https://teletypegames.org", "storeRepositoryUrl": null },
|
|
||||||
{
|
{
|
||||||
"name": "Some Other Store",
|
"name": "Teletype Games",
|
||||||
"catalogUrl": "https://games.example.org",
|
"catalogUrl": "https://teletypegames.org",
|
||||||
"storeRepositoryUrl": "https://git.example.org/stores/other-desktop-store"
|
"storeRepositoryUrl": "https://git.teletypegames.org/stores/ttg-desktop-store",
|
||||||
}
|
"config": { "store": { "id": "ttg" }, "paths": { "subfolder": "teletypegames" } }
|
||||||
|
},
|
||||||
|
{ "name": "Some Other Store", "catalogUrl": "https://games.example.org",
|
||||||
|
"storeRepositoryUrl": null, "config": null }
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
**A store needs no repository of its own.** A name and a catalog are enough: the
|
**A store needs nothing of its own.** A name and a catalog are enough: the store
|
||||||
store engine's built-in defaults already cover the host-to-asset mapping, the
|
engine's built-in defaults already cover the host-to-asset mapping, the install modes,
|
||||||
install modes, the platforms and the behaviour, so what is actually missing from
|
the platforms and the behaviour, so what is actually missing from them is identity — a
|
||||||
them is identity — a slug, a name and a catalog URL — and that is exactly what a
|
slug, a name and a catalog URL — and that is exactly what a registry record carries.
|
||||||
registry record carries. With `storeRepositoryUrl` null the client writes a
|
With `config` and `storeRepositoryUrl` both null the client writes a three-section
|
||||||
three-section config and the store installs.
|
config and the store installs.
|
||||||
|
|
||||||
From a record the client works out the rest:
|
Where a store's configuration comes from, in the order the client asks:
|
||||||
|
|
||||||
|
1. **`config` on the registry record** — the store's own configuration, in the same
|
||||||
|
shape a store's `config.json` had, because it is that file moved into the registry.
|
||||||
|
It costs no request: it arrived with the store list.
|
||||||
|
2. **`storeRepositoryUrl` → `…/raw/branch/master/config.json`** — the same thing in its
|
||||||
|
older home, read for a registry whose stores have not moved over yet. A repository
|
||||||
|
**without** a `config.json` is treated as no repository at all.
|
||||||
|
3. **the engine's defaults** — when there is neither.
|
||||||
|
|
||||||
|
Two fields the record always decides, whatever the config says:
|
||||||
|
|
||||||
- **the store id** — which names the store home and the folder games land in —
|
|
||||||
comes from the repository name when there is one (`ttg-desktop-store` becomes
|
|
||||||
`ttg`), otherwise from the catalog host (`teletypegames.org` becomes
|
|
||||||
`teletypegames`), otherwise from the display name. A `config.json` that sets its
|
|
||||||
own id keeps it.
|
|
||||||
- **`catalogUrl` and `name`** override the config's own `store.base_url` and
|
- **`catalogUrl` and `name`** override the config's own `store.base_url` and
|
||||||
`store.name`. The registry says which catalog this store is *for*, so it wins.
|
`store.name`. The registry says which catalog this store is *for*, so it wins.
|
||||||
- **`storeRepositoryUrl`**, when given → the store's `config.json`, read from
|
- **the store id** — which names the store home and the folder games land in — comes
|
||||||
`…/raw/branch/master/config.json`. That file stays the authority on how the store
|
from the config's `store.id` when it sets one; otherwise from the repository name
|
||||||
behaves: which platforms, which statuses, where things land. A repository
|
(`ttg-desktop-store` becomes `ttg`), then the catalog host (`teletypegames.org`
|
||||||
**without** a `config.json` is treated as no repository at all.
|
becomes `teletypegames`), then the display name.
|
||||||
|
|
||||||
|
**The window never supplies a config.** It is handed stores to show and hands one back
|
||||||
|
to install, but only as an identity: `RegistryStoreDtoMapper.toModel` drops the config,
|
||||||
|
and `StoreProvisioningService` reads the record again from the registry before
|
||||||
|
installing. A config decides where files are written and which subtree the store may
|
||||||
|
later delete from, so it must not be something the renderer can set — the same rule as
|
||||||
|
`GameDto` carrying no paths.
|
||||||
|
|
||||||
What the defaults produce, for a record with no repository: the games land in a
|
What the defaults produce, for a record with no repository: the games land in a
|
||||||
folder named after the store id, and released, archived **and demo** titles are
|
folder named after the store id, and released, archived **and demo** titles are
|
||||||
|
|||||||
+34
-62
@@ -1,57 +1,29 @@
|
|||||||
# WarpEngine Client 2.0.0
|
# WarpEngine Client 2.1.0
|
||||||
|
|
||||||
**The store engine is part of the app. Nothing has to be installed on the machine any
|
**A store's configuration now comes from the registry itself.** `GET /api/stores`
|
||||||
more.** Reading the catalog, choosing which release fits your computer, downloading and
|
records carry a `config` field — the same thing a store's `config.json` held, moved into
|
||||||
unpacking it, writing the menu entry and remembering what went where all happen inside
|
the record that already says what the store is. The client applies it directly, so
|
||||||
the application now. There is no Python to find, no child process, and no JSON protocol
|
installing a store no longer depends on a second repository existing and staying
|
||||||
between the two halves — which is why this is a major version rather than a feature.
|
reachable, and a store can be configured entirely from the site's admin.
|
||||||
|
|
||||||
What that changes for a person: on Windows and on a fresh Mac the app simply works.
|
The old path still works. Where a record has no `config` but names a repository, the
|
||||||
Before it looked for `python3`, `python` and `py -3`, and where none answered it drew a
|
client reads `…/raw/branch/master/config.json` from it exactly as before, so a registry
|
||||||
screen with a link to python.org instead of a catalog. That screen is gone, along with
|
whose stores have not moved over is unaffected. With neither, the engine's defaults
|
||||||
the one that offered to refresh a store engine too old to drive.
|
carry the store, as they always have. The order is: registry config, then repository
|
||||||
|
file, then defaults.
|
||||||
|
|
||||||
**Your existing library is kept.** `config.json` and `state.json` on disk are unchanged —
|
**The window cannot supply a store config.** It is handed stores to show and hands one
|
||||||
the same field names, the same `<scope>:<name>` keys, the same file modes — so a machine
|
back to install, but only as an identity — the config is dropped on the way in, and the
|
||||||
whose games were installed by the shell store keeps them. Opening this version against
|
main process reads the record again from the registry before installing anything. This
|
||||||
such a store lists them as installed, offers no needless update, and a sync reports
|
is not tidiness: a config decides where files are written and, through
|
||||||
*already up to date*. A `version: 1` state file is still migrated on first read.
|
`paths.subfolder`, which subtree the store may later delete from. That is not a decision
|
||||||
|
the renderer gets to make, for the same reason a `GameDto` carries no paths.
|
||||||
The two Python files an earlier install left in the store folder are removed the next
|
|
||||||
time that store is set up. Nothing reads them, and a folder that still looks like it
|
|
||||||
holds the engine invites someone to run it against a state file this app is also writing.
|
|
||||||
|
|
||||||
**The client knows which WarpEngine served a catalog.** Every WarpEngine API response
|
|
||||||
carries a `WarpEngine-Version` header, and the client now reads it, names the version in
|
|
||||||
the log, and picks the catalog dialect for it. `SUPPORTED_WARP_ENGINE_VERSIONS` lists what
|
|
||||||
this build was written against — 0.2, 0.3 and 0.4 — and the four cases are all handled:
|
|
||||||
|
|
||||||
| The header says | What the client does |
|
|
||||||
|---|---|
|
|
||||||
| a supported version | reads the catalog with that version's dialect |
|
|
||||||
| nothing at all | reads it as the oldest supported version — an engine before 0.4.0 sent no header |
|
|
||||||
| something older | the same, and says so in the log |
|
|
||||||
| something newer | tries the newest dialect anyway, warning that titles may be missed |
|
|
||||||
|
|
||||||
Adding a version to that list fails the build until somebody says what it reads like, in
|
|
||||||
the type checker and in the linter both. A new engine version cannot arrive unnoticed.
|
|
||||||
|
|
||||||
**Refresh and the language picker are icons.** They sit together at the foot of the side
|
|
||||||
menu, and the *Actions* heading that used to head a section of one button is gone. Both
|
|
||||||
carry their name as a tooltip and to a screen reader, and the language picker is still a
|
|
||||||
real `<select>` underneath — the native dropdown, keyboard and all, with only the glyph
|
|
||||||
showing.
|
|
||||||
|
|
||||||
### Also
|
### Also
|
||||||
|
|
||||||
No runtime dependencies, still: the zip reader the installer needs is about 150 lines over
|
The headless check now reports *which* source configured a store — registry, repository
|
||||||
`node:zlib` rather than a package. It restores the executable bit from each entry's
|
or defaults — and names the resulting prune boundary, because "it installed" and "it
|
||||||
external attributes, which is what makes an unpacked game able to start at all, and it
|
installed where I meant" are different claims.
|
||||||
refuses a zip64 archive, an unknown compression method or an entry that would be written
|
|
||||||
outside its destination rather than guessing.
|
|
||||||
|
|
||||||
The repository itself is free of Python too — the Makefile, the CI check and the release
|
|
||||||
script read `package.json` and the forge's JSON with Node now.
|
|
||||||
|
|
||||||
### Opening it on macOS
|
### Opening it on macOS
|
||||||
|
|
||||||
@@ -68,19 +40,19 @@ Windows (installer, portable) packages the pipeline builds when the tag is pushe
|
|||||||
|
|
||||||
### Verified
|
### Verified
|
||||||
|
|
||||||
`make check` is clean: typecheck, lint, the headless smoke test and the window self-test.
|
`make check` is clean: typecheck, lint, the headless smoke test and the window
|
||||||
|
self-test.
|
||||||
|
|
||||||
The new engine was measured against the old one rather than trusted. On the same catalog
|
All three configuration sources were exercised end to end against the real serializer
|
||||||
and the same config, the Python engine and this one produce **the same 13-title listing
|
output, with the client's own classes and a sandbox store root:
|
||||||
with zero field differences** and the same resolved paths. Installing three titles — a
|
|
||||||
bare TIC-80 binary wrapped in a bundle, a Godot `.app` symlinked, and a hosted web entry —
|
|
||||||
gives **byte-identical payloads, identical file modes and an identical `Info.plist`**; the
|
|
||||||
only difference in the two trees is the sandbox path inside the generated launcher script.
|
|
||||||
`state.json` matches record for record.
|
|
||||||
|
|
||||||
The upgrade path was tested directly: pointed at a store home installed by the Python
|
| Registry record | What configured the store | Prune boundary written |
|
||||||
engine, this one reports all three titles installed with no update available, and a
|
|---|---|---|
|
||||||
re-sync writes nothing. Remove, prune, prune-suppression on a named sync, and the v1→v2
|
| `config` present | the registry, with no extra request | `teletypegames` |
|
||||||
state migration were each exercised. The zip reader was checked against Python's
|
| `config: null`, repository given | the repository's `config.json` | `teletypegames` |
|
||||||
`extractall` on an archive holding stored, deflated, directory and symlink entries —
|
| neither | the engine's defaults | `teletypegames` |
|
||||||
identical bytes and identical modes — and its zip-slip and not-a-zip guards both fire.
|
|
||||||
|
The renderer barrier was tested adversarially rather than assumed: an install started
|
||||||
|
from a record carrying `paths.subfolder: "ATTACKER"` and `paths.install_root:
|
||||||
|
"/tmp/pwned"` wrote `subfolder: teletypegames` and `install_root: null` to disk, because
|
||||||
|
the service re-read the registry and the mapper had already dropped the config.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "warp-engine-client",
|
"name": "warp-engine-client",
|
||||||
"productName": "WarpEngine Client",
|
"productName": "WarpEngine Client",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"description": "Graphical client for WarpEngine stores: install a catalog into your own application menu.",
|
"description": "Graphical client for WarpEngine stores: install a catalog into your own application menu.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Teletype Games <games@teletype.hu>",
|
"author": "Teletype Games <games@teletype.hu>",
|
||||||
|
|||||||
@@ -16,12 +16,20 @@ export class RegistryStoreDtoMapper {
|
|||||||
return stores.map((store: RegistryStore): RegistryStoreDto => this.toDto(store))
|
return stores.map((store: RegistryStore): RegistryStoreDto => this.toDto(store))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The window hands a record straight back when asking for an install. */
|
/**
|
||||||
|
* The window hands a record back when asking for an install — as an identity only.
|
||||||
|
*
|
||||||
|
* There is no `config` here on purpose. A store's config decides where files are
|
||||||
|
* written and which subtree the store may delete from, so it must not be something
|
||||||
|
* the window can supply; `StoreProvisioningService` reads the real record from the
|
||||||
|
* registry instead. That is the same rule as `GameDto` carrying no paths.
|
||||||
|
*/
|
||||||
public toModel (dto: RegistryStoreDto): RegistryStore {
|
public toModel (dto: RegistryStoreDto): RegistryStore {
|
||||||
return {
|
return {
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
catalogUrl: dto.catalogUrl,
|
catalogUrl: dto.catalogUrl,
|
||||||
storeRepositoryUrl: dto.storeRepositoryUrl
|
storeRepositoryUrl: dto.storeRepositoryUrl,
|
||||||
|
config: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,44 @@ export class StoreProvisioningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async installStore (
|
public async installStore (
|
||||||
store: RegistryStore,
|
chosen: RegistryStore,
|
||||||
progress?: EngineProgressListener
|
progress?: EngineProgressListener
|
||||||
): Promise<InstalledStore> {
|
): Promise<InstalledStore> {
|
||||||
|
const store = await this.resolveFromRegistry(chosen, progress)
|
||||||
const home = this.stores.resolveDefaultHome(deriveStoreId(store))
|
const home = this.stores.resolveDefaultHome(deriveStoreId(store))
|
||||||
const installed = await this.installer.installEngine(home, store, progress)
|
const installed = await this.installer.installEngine(home, store, progress)
|
||||||
return this.selection.adoptStore(installed)
|
return this.selection.adoptStore(installed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registry's own record for the store that was chosen.
|
||||||
|
*
|
||||||
|
* The window is handed stores to display and hands one back to install, but what it
|
||||||
|
* hands back is not what gets used. A store's config decides where files are written
|
||||||
|
* and which subtree the store may later delete from, so it cannot be something the
|
||||||
|
* window supplies — the choice is treated as an identity, a name and a catalog, and
|
||||||
|
* the record behind it is read again here.
|
||||||
|
*
|
||||||
|
* A store that has since left the registry, or a registry that cannot be reached, is
|
||||||
|
* not a reason to refuse the install: it proceeds on the engine's defaults, which is
|
||||||
|
* what a store with no config gets anyway.
|
||||||
|
*/
|
||||||
|
private async resolveFromRegistry (
|
||||||
|
chosen: RegistryStore,
|
||||||
|
progress?: EngineProgressListener
|
||||||
|
): Promise<RegistryStore> {
|
||||||
|
try {
|
||||||
|
const listed = await this.registry.listStores()
|
||||||
|
const found = listed.find((store: RegistryStore): boolean =>
|
||||||
|
store.catalogUrl === chosen.catalogUrl && store.name === chosen.name)
|
||||||
|
if (found !== undefined) return found
|
||||||
|
progress?.onLog?.(
|
||||||
|
`${chosen.name} is no longer in the registry — installing on the engine's defaults`)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const reason = error instanceof Error ? error.message : String(error)
|
||||||
|
progress?.onLog?.(
|
||||||
|
`the registry could not be read again (${reason}) — installing on the engine's defaults`)
|
||||||
|
}
|
||||||
|
return { ...chosen, config: null }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
/**
|
/**
|
||||||
* A store the site's registry offers.
|
* A store the site's registry offers.
|
||||||
*
|
*
|
||||||
* A name and a catalog are what make a store; the repository is optional. When
|
* A name and a catalog are what make a store; the other two fields are optional.
|
||||||
* there is one it stays the authority on how that store behaves — which platforms
|
*
|
||||||
* it offers, where things land — and when there is not, the engine's own defaults
|
* `config` is how that store behaves — which platforms it offers, which release
|
||||||
* cover all of it and this record covers the identity. That is the whole reason a
|
* statuses it shows, where its games land — in the same shape a store's `config.json`
|
||||||
* store needs no repository of its own.
|
* had, because it is the same thing moved into the registry. With none, the engine's
|
||||||
|
* defaults cover all of it and this record covers the identity, which is why a store
|
||||||
|
* needs nothing of its own to be installable.
|
||||||
|
*
|
||||||
|
* `storeRepositoryUrl` is where the store's own repository is, when it has one. It is
|
||||||
|
* still read as a config source for a registry that has not moved its stores over yet.
|
||||||
*/
|
*/
|
||||||
export interface RegistryStore {
|
export interface RegistryStore {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly catalogUrl: string
|
readonly catalogUrl: string
|
||||||
readonly storeRepositoryUrl: string | null
|
readonly storeRepositoryUrl: string | null
|
||||||
|
/**
|
||||||
|
* Deliberately not `JsonRecord`: `domain` imports nothing from `infrastructure`, and
|
||||||
|
* a JSON object is describable without it.
|
||||||
|
*/
|
||||||
|
readonly config: Readonly<Record<string, unknown>> | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { RegistryUnavailableError } from '../../domain/errors/RegistryUnavailableError'
|
import { RegistryUnavailableError } from '../../domain/errors/RegistryUnavailableError'
|
||||||
import type { RegistryStore } from '../../domain/models/RegistryStore'
|
import type { RegistryStore } from '../../domain/models/RegistryStore'
|
||||||
import type { StoreRegistryRepository } from '../../domain/ports/StoreRegistryRepository'
|
import type { StoreRegistryRepository } from '../../domain/ports/StoreRegistryRepository'
|
||||||
import { asRecord, readString, type JsonRecord } from '../json/JsonRecord'
|
import { asRecord, readRecord, readString, type JsonRecord } from '../json/JsonRecord'
|
||||||
import { BuildConfiguration } from '../config/BuildConfiguration'
|
import { BuildConfiguration } from '../config/BuildConfiguration'
|
||||||
import type { HttpTextClient } from '../http/HttpTextClient'
|
import type { HttpTextClient } from '../http/HttpTextClient'
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ const DEFAULT_REGISTRY_URL = 'https://teletypegames.org/api/stores'
|
|||||||
* field a build was packaged with (for shipping a client for another site), and finally
|
* field a build was packaged with (for shipping a client for another site), and finally
|
||||||
* the address of ours.
|
* the address of ours.
|
||||||
*
|
*
|
||||||
* A record needs a name and a catalog URL; those two make a store. The repository
|
* A record needs a name and a catalog URL; those two make a store. The config and the
|
||||||
* is optional and arrives as null when absent — a store configured by nothing but
|
* repository are both optional and arrive as null when absent — a store configured by
|
||||||
* this record installs on the engine's defaults. Records missing either of the two
|
* nothing but this record installs on the engine's defaults. Records missing either of
|
||||||
* required fields are dropped rather than half-used.
|
* the two required fields are dropped rather than half-used.
|
||||||
*/
|
*/
|
||||||
export class HttpStoreRegistryRepository implements StoreRegistryRepository {
|
export class HttpStoreRegistryRepository implements StoreRegistryRepository {
|
||||||
public readonly sourceUrl: string
|
public readonly sourceUrl: string
|
||||||
@@ -53,7 +53,8 @@ export class HttpStoreRegistryRepository implements StoreRegistryRepository {
|
|||||||
return {
|
return {
|
||||||
name: readString(row, 'name').trim(),
|
name: readString(row, 'name').trim(),
|
||||||
catalogUrl: (readString(row, 'catalogUrl') || readString(row, 'catalog_url')).trim(),
|
catalogUrl: (readString(row, 'catalogUrl') || readString(row, 'catalog_url')).trim(),
|
||||||
storeRepositoryUrl: repository.length > 0 ? repository : null
|
storeRepositoryUrl: repository.length > 0 ? repository : null,
|
||||||
|
config: readRecord(row, 'config')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((store: RegistryStore): boolean =>
|
.filter((store: RegistryStore): boolean =>
|
||||||
|
|||||||
@@ -77,13 +77,15 @@ export class NativeStoreEngineInstaller implements StoreEngineInstaller {
|
|||||||
/**
|
/**
|
||||||
* The store's configuration.
|
* The store's configuration.
|
||||||
*
|
*
|
||||||
* Three cases, and all of them install:
|
* Four cases, and all of them install:
|
||||||
*
|
*
|
||||||
* - **a repository with a config.json** — that file is the authority on how the
|
* - **a config on the registry record** — the authority on how the store behaves:
|
||||||
* store behaves: which platforms it offers, which statuses it shows, where
|
* which platforms it offers, which statuses it shows, where things land. No
|
||||||
* things land;
|
* request at all, because it arrived with the store list;
|
||||||
|
* - **a repository with a config.json** — the same thing in its older home, read
|
||||||
|
* for a registry whose stores have not moved over yet;
|
||||||
* - **a repository without one** (404) — the engine's defaults, as below;
|
* - **a repository without one** (404) — the engine's defaults, as below;
|
||||||
* - **no repository at all** — the same defaults, without the round trip.
|
* - **neither** — the same defaults, without the round trip.
|
||||||
*
|
*
|
||||||
* The engine's built-in defaults already cover the host-to-asset mapping, the
|
* The engine's built-in defaults already cover the host-to-asset mapping, the
|
||||||
* modes, the platforms and the behaviour, so what a store actually has to supply is
|
* modes, the platforms and the behaviour, so what a store actually has to supply is
|
||||||
@@ -113,6 +115,13 @@ export class NativeStoreEngineInstaller implements StoreEngineInstaller {
|
|||||||
storeId: string,
|
storeId: string,
|
||||||
progress: EngineProgressListener
|
progress: EngineProgressListener
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
|
// The registry's own answer wins, and needs no request: a store's configuration is
|
||||||
|
// part of its record now rather than a file in a repository that has to exist and
|
||||||
|
// stay reachable.
|
||||||
|
if (store.config !== null) {
|
||||||
|
progress.onLog?.(`${store.name} is configured by the registry`)
|
||||||
|
return { ...store.config }
|
||||||
|
}
|
||||||
const repositoryUrl = store.storeRepositoryUrl
|
const repositoryUrl = store.storeRepositoryUrl
|
||||||
if (repositoryUrl === null) {
|
if (repositoryUrl === null) {
|
||||||
progress.onLog?.(`${store.name} has no store repository — using the engine defaults`)
|
progress.onLog?.(`${store.name} has no store repository — using the engine defaults`)
|
||||||
|
|||||||
@@ -76,24 +76,43 @@ class SmokeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store needs no repository, and a repository needs no config.json: either way
|
* Where this store's configuration would come from, in the order the installer asks.
|
||||||
* the engine's defaults carry it. So both absences are reported, not failed.
|
*
|
||||||
|
* A store needs no config and no repository: either way the engine's defaults carry
|
||||||
|
* it, so every absence here is reported rather than failed. What is worth seeing is
|
||||||
|
* *which* source answered — a store still being configured by a repository is a store
|
||||||
|
* that has not moved over to the registry yet.
|
||||||
*/
|
*/
|
||||||
private async checkStoreConfig (store: RegistryStore): Promise<void> {
|
private async checkStoreConfig (store: RegistryStore): Promise<void> {
|
||||||
|
if (store.config !== null) {
|
||||||
|
const subfolder = this.readSubfolder(store.config)
|
||||||
|
this.reportOk(' config', `${String(Object.keys(store.config).length)} sections ` +
|
||||||
|
`from the registry${subfolder === null ? '' : `, subfolder ${subfolder}`}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (store.storeRepositoryUrl === null) {
|
if (store.storeRepositoryUrl === null) {
|
||||||
this.reportOk(' config', 'no repository — the engine defaults would be used')
|
this.reportOk(' config', 'none, and no repository — the engine defaults would be used')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const url = `${store.storeRepositoryUrl.replace(/\/+$/, '')}/raw/branch/master/config.json`
|
const url = `${store.storeRepositoryUrl.replace(/\/+$/, '')}/raw/branch/master/config.json`
|
||||||
try {
|
try {
|
||||||
const config: unknown = JSON.parse(await this.httpClient.readText(url))
|
const config: unknown = JSON.parse(await this.httpClient.readText(url))
|
||||||
const sections = typeof config === 'object' && config !== null ? Object.keys(config).length : 0
|
const sections = typeof config === 'object' && config !== null ? Object.keys(config).length : 0
|
||||||
this.reportOk(' config.json', `${String(sections)} sections`)
|
this.reportOk(' config.json', `${String(sections)} sections from the repository ` +
|
||||||
|
'(not yet moved to the registry)')
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.reportOk(' config.json', `absent (${this.describe(error)}) — defaults would be used`)
|
this.reportOk(' config.json', `absent (${this.describe(error)}) — defaults would be used`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The prune boundary, which is the field worth seeing at a glance. */
|
||||||
|
private readSubfolder (config: Readonly<Record<string, unknown>>): string | null {
|
||||||
|
const paths = config['paths']
|
||||||
|
if (typeof paths !== 'object' || paths === null) return null
|
||||||
|
const subfolder = (paths as Readonly<Record<string, unknown>>)['subfolder']
|
||||||
|
return typeof subfolder === 'string' ? subfolder : null
|
||||||
|
}
|
||||||
|
|
||||||
private findStore (): InstalledStore | null {
|
private findStore (): InstalledStore | null {
|
||||||
const sandbox = process.env['SMOKE_HOME']
|
const sandbox = process.env['SMOKE_HOME']
|
||||||
if (sandbox !== undefined && sandbox.length > 0) {
|
if (sandbox !== undefined && sandbox.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user