Thing/tools/eveunity/eveunity-capture-contract.mjs
2026-07-09 10:48:08 +01:00

122 lines
4.2 KiB
JavaScript

import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export function buildUnityCaptureRequest({
advertisement,
capabilityManifest,
advertisementPath = "",
capabilityManifestPath = "",
stamp = new Date().toISOString().replace(/[:.]/g, "-"),
} = {}) {
if (!advertisement || typeof advertisement !== "object") {
throw new Error("advertisement is required");
}
if (!capabilityManifest || typeof capabilityManifest !== "object") {
throw new Error("capabilityManifest is required");
}
const captureContract = capabilityManifest.lifecycle?.capture?.captureContract;
if (!captureContract) {
throw new Error("capability manifest is missing lifecycle.capture.captureContract");
}
if (advertisement.providerId !== captureContract.requiredProvider) {
throw new Error(
`capture provider mismatch: expected ${captureContract.requiredProvider} got ${advertisement.providerId || "missing"}`,
);
}
const targetId = captureContract.targetId || captureContract.runtimeId;
const surface = (advertisement.surfaces || []).find(candidate => candidate.surfaceId === captureContract.requiredSurface);
if (!surface) {
throw new Error(`capture surface not advertised: ${captureContract.requiredSurface}`);
}
const loweringTargets = surface.worldInteraction?.loweringTargets || [];
if (!loweringTargets.includes(targetId)) {
throw new Error(`surface ${surface.surfaceId} does not advertise lowering target ${targetId}`);
}
const artifactPath = replaceStamp(captureContract.artifactPattern, stamp);
return {
schema: captureContract.requestSchema || "gamecult.eve.runtime_capture_request.v1",
runtimeId: capabilityManifest.runtimeId,
ownerRepo: captureContract.ownerRepo,
providerId: advertisement.providerId,
surfaceId: surface.surfaceId,
surfaceKind: surface.surfaceKind || "",
targetId,
projectionKind: surface.worldInteraction?.projectionKind || "",
commandBoundary: surface.worldInteraction?.commandBoundary || "",
receiptSchema: surface.worldInteraction?.receiptSchema || "",
captureKind: captureContract.captureKind,
artifactKind: captureContract.artifactKind,
artifactPath,
conformanceAttachment: captureContract.conformanceAttachment,
authority: captureContract.authority,
sourceAdvertisementPath: advertisementPath,
sourceCapabilityManifestPath: capabilityManifestPath,
issuedAtUtc: stamp,
};
}
export function loadJsonFile(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
export function writeCaptureRequest(request, outputPath) {
mkdirSync(path.dirname(outputPath), { recursive: true });
writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
}
function replaceStamp(pattern, stamp) {
if (!pattern || !pattern.includes("{stamp}")) {
throw new Error(`capture artifact pattern must include {stamp}: ${pattern || "missing"}`);
}
return pattern.replace("{stamp}", stamp);
}
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (!value.startsWith("--")) continue;
const key = value.slice(2);
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
args[key] = true;
} else {
args[key] = next;
index += 1;
}
}
return args;
}
function runCli() {
const args = parseArgs(process.argv.slice(2));
for (const required of ["advertisement", "capability", "output"]) {
if (!args[required]) {
throw new Error(`missing required --${required}`);
}
}
const stamp = args.stamp || new Date().toISOString().replace(/[:.]/g, "-");
const request = buildUnityCaptureRequest({
advertisement: loadJsonFile(args.advertisement),
capabilityManifest: loadJsonFile(args.capability),
advertisementPath: args.advertisement,
capabilityManifestPath: args.capability,
stamp,
});
writeCaptureRequest(request, args.output);
process.stdout.write(`${args.output}\n`);
}
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
const modulePath = fileURLToPath(import.meta.url);
if (invokedPath === modulePath) {
runCli();
}