feat: Effect services + HttpApi implementation + kit entry/CLI/dev server
- RomConfig/RomCache on the kit's ConfigService/CacheStore; RomSync wires the pure domain into the kit SyncEngine (poll/watch/coalesce/fingerprint) and ProviderClient; EngineStatus assembly shared by REST + SSE - makeApi: HttpApiBuilder groups over live service values (handler runtime shares the plugin runtime's singletons) + sseRoute status feed - index.ts: definePluginKit entry (async-main boundary); headless-first (UI serve failure logged, engine continues) - cli.ts on the kit dispatcher: scan/detect/preview offline, sync/uninstall online; set-password is gone with the standalone server - dev.ts: auth-free loopback API server (:5885) — the dev:live proxy target; never bundled - verified live host-less: raw config round-trip on disk, status/platforms/ preview endpoints, soft-fail reconcile without a host - CI: workspace install, per-package typecheck, publish from plugin/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+64
-48
@@ -1,5 +1,7 @@
|
||||
// The one HttpApi contract — HttpApiBuilder implements it server-side, AtomHttpApi
|
||||
// derives the UI's typed client + atoms from it. No hand-mirrored types anywhere.
|
||||
|
||||
import { ProviderEntry } from "@punktfunk/plugin-kit/wire";
|
||||
import { Schema } from "effect";
|
||||
import {
|
||||
HttpApi,
|
||||
@@ -7,9 +9,18 @@ import {
|
||||
HttpApiGroup,
|
||||
HttpApiSchema,
|
||||
} from "effect/unstable/httpapi";
|
||||
import { ProviderEntry } from "@punktfunk/plugin-kit/wire";
|
||||
import { DetectedEmulator, EngineStatus, Platform, SyncReport } from "./domain.js";
|
||||
import { ConfigInvalid, DetectFailed, ScanFailed, SyncInProgress } from "./errors.js";
|
||||
import {
|
||||
DetectedEmulator,
|
||||
EngineStatus,
|
||||
Platform,
|
||||
SyncReport,
|
||||
} from "./domain.js";
|
||||
import {
|
||||
ConfigInvalid,
|
||||
DetectFailed,
|
||||
ScanFailed,
|
||||
SyncInProgress,
|
||||
} from "./errors.js";
|
||||
|
||||
/** GET /api/config + PUT /api/config response: the authored raw shape, verbatim.
|
||||
* The UI resolves it locally by decoding through the SHARED RomConfigSchema. */
|
||||
@@ -29,55 +40,60 @@ export const EmulatorsPayload = Schema.Struct({
|
||||
});
|
||||
export type EmulatorsPayload = typeof EmulatorsPayload.Type;
|
||||
|
||||
export const RomManagerApi = HttpApi.make("rom-manager").add(
|
||||
HttpApiGroup.make("status").add(
|
||||
HttpApiEndpoint.get("get", "/api/status", { success: EngineStatus }),
|
||||
),
|
||||
).add(
|
||||
HttpApiGroup.make("config")
|
||||
.add(
|
||||
HttpApiEndpoint.get("get", "/api/config", { success: ConfigPayload }),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("put", "/api/config", {
|
||||
payload: Schema.Unknown,
|
||||
success: ConfigPayload,
|
||||
error: ConfigInvalid.pipe(HttpApiSchema.status(400)),
|
||||
export const RomManagerApi = HttpApi.make("rom-manager")
|
||||
.add(
|
||||
HttpApiGroup.make("status").add(
|
||||
HttpApiEndpoint.get("get", "/api/status", { success: EngineStatus }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("config")
|
||||
.add(
|
||||
HttpApiEndpoint.get("get", "/api/config", { success: ConfigPayload }),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("put", "/api/config", {
|
||||
payload: Schema.Unknown,
|
||||
success: ConfigPayload,
|
||||
error: ConfigInvalid.pipe(HttpApiSchema.status(400)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("library").add(
|
||||
HttpApiEndpoint.get("preview", "/api/preview", {
|
||||
success: PreviewResult,
|
||||
error: ScanFailed.pipe(HttpApiSchema.status(500)),
|
||||
}),
|
||||
),
|
||||
).add(
|
||||
HttpApiGroup.make("library").add(
|
||||
HttpApiEndpoint.get("preview", "/api/preview", {
|
||||
success: PreviewResult,
|
||||
error: ScanFailed.pipe(HttpApiSchema.status(500)),
|
||||
}),
|
||||
),
|
||||
).add(
|
||||
HttpApiGroup.make("catalog")
|
||||
.add(
|
||||
HttpApiEndpoint.get("platforms", "/api/platforms", {
|
||||
success: Schema.Array(Platform),
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("emulators", "/api/emulators", {
|
||||
success: EmulatorsPayload,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("detect", "/api/detect", {
|
||||
success: EmulatorsPayload,
|
||||
error: DetectFailed.pipe(HttpApiSchema.status(500)),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("catalog")
|
||||
.add(
|
||||
HttpApiEndpoint.get("platforms", "/api/platforms", {
|
||||
success: Schema.Array(Platform),
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("emulators", "/api/emulators", {
|
||||
success: EmulatorsPayload,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("detect", "/api/detect", {
|
||||
success: EmulatorsPayload,
|
||||
error: DetectFailed.pipe(HttpApiSchema.status(500)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("sync").add(
|
||||
HttpApiEndpoint.post("run", "/api/sync", {
|
||||
success: SyncReport,
|
||||
error: SyncInProgress.pipe(HttpApiSchema.status(409)),
|
||||
}),
|
||||
),
|
||||
).add(
|
||||
HttpApiGroup.make("sync").add(
|
||||
HttpApiEndpoint.post("run", "/api/sync", {
|
||||
success: SyncReport,
|
||||
error: SyncInProgress.pipe(HttpApiSchema.status(409)),
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
/** The SSE status feed — outside the HttpApi (no event-stream media type in httpapi).
|
||||
* Frames: `event: status`, data = EngineStatus JSON. */
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
import { Effect, Schema } from "effect";
|
||||
import { EmulatorDef, Platform } from "./domain.js";
|
||||
|
||||
const withDefault = <S extends Schema.Top>(
|
||||
schema: S,
|
||||
value: S["Encoded"],
|
||||
) =>
|
||||
const withDefault = <S extends Schema.Top>(schema: S, value: S["Encoded"]) =>
|
||||
schema.pipe(
|
||||
Schema.withDecodingDefaultKey(Effect.succeed(value), {
|
||||
encodingStrategy: "omit",
|
||||
|
||||
+10
-9
@@ -2,7 +2,8 @@
|
||||
// the derived types keep the original domain names so the pure core reads unchanged.
|
||||
import { Schema } from "effect";
|
||||
|
||||
export const Os = Schema.Literals(["linux", "windows", "mac"]);
|
||||
/** Host OS as the launch/quoting domain sees it (macOS hosts use the linux lane). */
|
||||
export const Os = Schema.Literals(["linux", "windows"]);
|
||||
export type Os = typeof Os.Type;
|
||||
|
||||
/** Default emulator + core for a platform (operator-overridable per platform / per game). */
|
||||
@@ -50,13 +51,13 @@ export const DetectedEmulator = Schema.Struct({
|
||||
name: Schema.String,
|
||||
template: Schema.String,
|
||||
supportsArchives: Schema.Boolean,
|
||||
contested: Schema.optionalKey(Schema.Boolean),
|
||||
contested: Schema.optional(Schema.Boolean),
|
||||
exeToken: Schema.String,
|
||||
exePath: Schema.optionalKey(Schema.String),
|
||||
appId: Schema.optionalKey(Schema.String),
|
||||
exePath: Schema.optional(Schema.String),
|
||||
appId: Schema.optional(Schema.String),
|
||||
via: Schema.Literals(["path", "file", "flatpak"]),
|
||||
coresDir: Schema.optionalKey(Schema.String),
|
||||
cores: Schema.optionalKey(Schema.Array(Schema.String)),
|
||||
coresDir: Schema.optional(Schema.String),
|
||||
cores: Schema.optional(Schema.Array(Schema.String)),
|
||||
});
|
||||
export type DetectedEmulator = typeof DetectedEmulator.Type;
|
||||
|
||||
@@ -96,9 +97,9 @@ export const EngineStatus = Schema.Struct({
|
||||
os: Os,
|
||||
artProvider: Schema.NullOr(Schema.String),
|
||||
syncing: Schema.Boolean,
|
||||
lastSync: Schema.optionalKey(LastSync),
|
||||
lastReport: Schema.optionalKey(SyncReport),
|
||||
detectedAt: Schema.optionalKey(Schema.Number),
|
||||
lastSync: Schema.optional(LastSync),
|
||||
lastReport: Schema.optional(SyncReport),
|
||||
detectedAt: Schema.optional(Schema.Number),
|
||||
paths: Schema.Struct({
|
||||
dir: Schema.String,
|
||||
config: Schema.String,
|
||||
|
||||
Reference in New Issue
Block a user