import childProcess from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fromBinary, toJsonString } from "@bufbuild/protobuf"; import type { PackageDescriptor } from "./gen/quixos/package_pb.js"; import { PackageDescriptorSchema } from "./gen/quixos/package_pb.js"; const protoPaths = () => { const candidates = [ ...(process.env.QUIXOS_PROTO_PATH ?? "") .split(path.delimiter) .map((entry) => entry.trim()) .filter(Boolean), path.resolve(process.cwd(), "proto"), path.resolve(process.cwd(), "quixos-protocol/proto"), ]; return [...new Set(candidates)].filter((candidate) => fs.existsSync(path.join(candidate, "quixos/package.proto"))); }; export const parsePackageDescriptorTextproto = (text: string): PackageDescriptor => { const paths = protoPaths(); if (paths.length === 0) { throw new Error("QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors"); } const result = childProcess.spawnSync( "protoc", [ ...paths.map((protoPath) => `--proto_path=${protoPath}`), "--encode=quixos.PackageDescriptor", "quixos/package.proto", ], { input: Buffer.from(text), maxBuffer: 1024 * 1024 * 8, }, ); if (result.error) { throw result.error; } if (result.status !== 0) { throw new Error(`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`); } return fromBinary(PackageDescriptorSchema, result.stdout); }; export const packageDescriptorToJson = (descriptor: PackageDescriptor) => toJsonString(PackageDescriptorSchema, descriptor, { prettySpaces: 2, }); export const validatePackageDescriptor = (descriptor: PackageDescriptor): string[] => { const errors: string[] = []; if (!descriptor.packageId) { errors.push("packageId is required"); } if (!descriptor.packageRevisionId) { errors.push("packageRevisionId is required"); } if (!descriptor.runtimeProtocolVersion) { errors.push("runtimeProtocolVersion is required"); } const seenExports = new Set(); for (const [index, entry] of descriptor.exports.entries()) { if (!entry.exportId) errors.push(`exports[${index}].exportId is required`); if (!entry.runtimeSymbol) { errors.push(`exports[${index}].runtimeSymbol is required`); } if (seenExports.has(entry.exportId)) { errors.push(`duplicate export: ${entry.exportId}`); } seenExports.add(entry.exportId); } return errors; };