A registry record is a name and a catalog
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful

`config` — added this morning in 2.1.0 — is gone, and `storeRepositoryUrl` with it, along
with the two store repositories they pointed at.

2.1.0 had the registry say how each store behaves. Wrong shape: how a store behaves is
fixed per installed client, and this application is the only thing that can see the
machine it runs on. A copy of that on a server was a second authority over decisions this
side had already made correctly — including which directories the store may delete from —
and two authorities are a way to disagree.

Keeping two stores on one machine apart needs none of it. It is a subfolder, derived here:
the store id is a slug of the catalog host, the home is `<id>-desktop`, the games folder is
`<id>`, and that folder is the only subtree the store will ever delete from. Derived from
the *catalog* on purpose — the catalog is what a store is, so two records naming the same
one are the same store and land in the same place, which makes installing twice idempotent
instead of a way to orphan what is already there.

Existing installations keep their identity: a store home is recognised by its own
`config.json`, so one installed as `ttg` stays `ttg` in `ttg-desktop` with its games where
they are. Only a new install derives its id.

`StoreProvisioningService` no longer re-reads the registry before installing. That existed
to keep the renderer from supplying a config, and with no config in the record there is
nothing left to protect: a name and a catalog have no paths in them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 07:27:52 +02:00
co-authored by Claude Opus 5
parent 045c7bf5b7
commit 82590d3ec4
13 changed files with 160 additions and 327 deletions
+3 -40
View File
@@ -3,7 +3,6 @@ import path from 'node:path'
import { GameDtoMapper } from '../application/mappers/GameDtoMapper'
import type { CatalogListing } from '../domain/models/CatalogListing'
import type { InstalledStore } from '../domain/models/InstalledStore'
import type { RegistryStore } from '../domain/models/RegistryStore'
import { DESKTOP_STORE_ENGINE } from '../domain/models/StoreEngine'
import { deriveStoreId } from '../domain/models/StoreIdentity'
import { NativeStoreCatalogGateway } from '../infrastructure/engine/NativeStoreCatalogGateway'
@@ -66,53 +65,17 @@ class SmokeTest {
return
}
this.reportOk('registry', `${String(stores.length)} store(s) from ${this.registry.sourceUrl}`)
// The slug is worth printing: it names the store home and the games subfolder, and
// it is derived here rather than told to us, so a wrong catalog URL shows up as a
// wrong folder name before anything is installed.
for (const store of stores) {
this.reportOk(` ${store.name}`, `${store.catalogUrl} · ${deriveStoreId(store)}`)
await this.checkStoreConfig(store)
}
} catch (error: unknown) {
this.reportBad('registry', `${this.registry.sourceUrl}: ${this.describe(error)}`)
}
}
/**
* Where this store's configuration would come from, in the order the installer asks.
*
* 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> {
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) {
this.reportOk(' config', 'none, and no repository — the engine defaults would be used')
return
}
const url = `${store.storeRepositoryUrl.replace(/\/+$/, '')}/raw/branch/master/config.json`
try {
const config: unknown = JSON.parse(await this.httpClient.readText(url))
const sections = typeof config === 'object' && config !== null ? Object.keys(config).length : 0
this.reportOk(' config.json', `${String(sections)} sections from the repository ` +
'(not yet moved to the registry)')
} catch (error: unknown) {
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 {
const sandbox = process.env['SMOKE_HOME']
if (sandbox !== undefined && sandbox.length > 0) {