d6cf5f3d36
- bun workspaces: contract/ (Schemas = source of truth: config raw↔resolved,
domain DTOs, HttpApi contract, API errors) + plugin/ + ui/
- pure domain moved src/{engine,art}→plugin/src/{domain,art}, types now
imported from @rom-manager/contract and @punktfunk/plugin-kit/wire (the
hand-copied wire.ts dies), loggers injected instead of module-global
- ui.* and devEntry dropped from the config shape (standalone server is
gone; stale keys tolerated on decode, dropped by the next save)
- all 48 domain tests ported and green BEFORE any Effect wiring
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
4.1 KiB
TypeScript
106 lines
4.1 KiB
TypeScript
// The config model. ONE schema: the Encoded side IS the authored file shape (RawConfig),
|
|
// the Type side is the fully-resolved shape the engine consumes (Config). Defaults live
|
|
// ONLY here (`withDecodingDefaultKey` + encodingStrategy "omit") — the kit's ConfigService
|
|
// persists the raw shape verbatim, so a UI save never bakes defaults into the file.
|
|
//
|
|
// Dropped vs 0.2.x: `ui.*` (the standalone password server is gone — console surface +
|
|
// CLI only) and `devEntry` (dev flag out of the prod shape; use `preview` instead).
|
|
// Stale keys in existing files are tolerated on decode and dropped by the next save.
|
|
import { Effect, Schema } from "effect";
|
|
import { EmulatorDef, Platform } from "./domain.js";
|
|
|
|
const withDefault = <S extends Schema.Top>(
|
|
schema: S,
|
|
value: S["Encoded"],
|
|
) =>
|
|
schema.pipe(
|
|
Schema.withDecodingDefaultKey(Effect.succeed(value), {
|
|
encodingStrategy: "omit",
|
|
}),
|
|
);
|
|
|
|
/** A ROM root: a directory paired with the platform its files belong to (design §5). */
|
|
export const RomRoot = Schema.Struct({
|
|
dir: Schema.String,
|
|
platform: Schema.String,
|
|
excludes: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
});
|
|
export type RomRoot = typeof RomRoot.Type;
|
|
|
|
/** A per-platform launch override — replaces the platform's built-in default. */
|
|
export const PlatformLaunch = Schema.Struct({
|
|
emulator: Schema.String,
|
|
core: Schema.optionalKey(Schema.String),
|
|
extraArgs: Schema.optionalKey(Schema.String),
|
|
});
|
|
export type PlatformLaunch = typeof PlatformLaunch.Type;
|
|
|
|
/** A per-game override, keyed by `external_id` (`snes/Chrono Trigger (USA).sfc`). */
|
|
export const GameOverride = Schema.Struct({
|
|
exclude: Schema.optionalKey(Schema.Boolean),
|
|
emulator: Schema.optionalKey(Schema.String),
|
|
core: Schema.optionalKey(Schema.String),
|
|
extraArgs: Schema.optionalKey(Schema.String),
|
|
title: Schema.optionalKey(Schema.String),
|
|
art: Schema.optionalKey(Schema.String),
|
|
});
|
|
export type GameOverride = typeof GameOverride.Type;
|
|
|
|
export const ArtProviderChoice = Schema.Literals([
|
|
"auto",
|
|
"steamgriddb",
|
|
"libretro",
|
|
]);
|
|
export type ArtProviderChoice = typeof ArtProviderChoice.Type;
|
|
|
|
export const SyncOptions = Schema.Struct({
|
|
/** Interval poll in minutes (watchers are unreliable on SMB/NFS — poll is primary). */
|
|
pollMinutes: withDefault(Schema.Number, 15),
|
|
watch: withDefault(Schema.Boolean, true),
|
|
debounceMs: withDefault(Schema.Number, 2000),
|
|
/** Platform ids to region-dedupe ("one entry per title", design §5). */
|
|
dedupeRegions: withDefault(Schema.Array(Schema.String), []),
|
|
regionPriority: withDefault(Schema.Array(Schema.String), [
|
|
"USA",
|
|
"World",
|
|
"Europe",
|
|
"Japan",
|
|
]),
|
|
warnEntries: withDefault(Schema.Number, 2000),
|
|
maxEntries: withDefault(Schema.Number, 5000),
|
|
closeOnEnd: withDefault(Schema.Boolean, false),
|
|
});
|
|
export type SyncOptions = typeof SyncOptions.Type;
|
|
|
|
export const ArtOptions = Schema.Struct({
|
|
enabled: withDefault(Schema.Boolean, true),
|
|
provider: withDefault(ArtProviderChoice, "auto"),
|
|
steamGridDbKey: Schema.optionalKey(Schema.String),
|
|
});
|
|
export type ArtOptions = typeof ArtOptions.Type;
|
|
|
|
export const RomConfigSchema = Schema.Struct({
|
|
roots: withDefault(Schema.Array(RomRoot), []),
|
|
platformLaunch: withDefault(Schema.Record(Schema.String, PlatformLaunch), {}),
|
|
gameOverrides: withDefault(Schema.Record(Schema.String, GameOverride), {}),
|
|
/** Operator-added / overriding platform defs (merged over built-ins by id). */
|
|
platforms: Schema.optionalKey(Schema.Array(Platform)),
|
|
/** Operator-added / overriding emulator defs (merged over built-ins by id). */
|
|
emulators: Schema.optionalKey(Schema.Array(EmulatorDef)),
|
|
sync: withDefault(SyncOptions, {}),
|
|
art: withDefault(ArtOptions, {}),
|
|
});
|
|
|
|
/** The fully-resolved config the engine consumes. */
|
|
export type Config = typeof RomConfigSchema.Type;
|
|
/** The on-disk config as authored (all optional). */
|
|
export type RawConfig = typeof RomConfigSchema.Encoded;
|
|
|
|
/**
|
|
* Resolve an authored raw config to the total shape (defaults filled). Throws on an
|
|
* invalid shape — the sync path for tests and the UI; the server side uses the kit's
|
|
* ConfigService (Effect + typed errors) instead.
|
|
*/
|
|
export const resolveConfig = (raw: unknown): Config =>
|
|
Schema.decodeUnknownSync(RomConfigSchema)(raw);
|