import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

export const RUNTIME_RELEASE_SCHEMA_VERSION = 1;

const MODULE_PATH = "modules/workstation/claude";
const MANIFEST_PATH = "modules/workstation/manifest/claude.json";
const TOOLS_PATH = "modules/buildbox/devtools.json";
const NON_RUNTIME_ENTRIES = new Set(["docs", "tests"]);
const RUNTIME_PAYLOAD_POLICY = new Map([
  ["skills", "asset"], ["hooks", "runtime-code"], ["agents", "asset"], ["commands", "asset"],
  ["output-styles", "asset"], ["lib", "runtime-code"], ["bin", "runtime-code"],
  ["workflows/lib", "runtime-code"], ["workflows/security-sweep.js", "runtime-code"],
  ["systemd", "config"], ["scripts", "runtime-code"], ["command-assets", "asset"],
  ["security-gate", "runtime-code"], ["statusline-command.sh", "runtime-code"],
  ["statusline-cost.sh", "runtime-code"], ["statusline-router.sh", "runtime-code"],
  ["tools.json", "config"], ["build-priority.conf", "config"], ["build-remote.json", "config"],
  ["buildbox-hosts.json", "config"], ["cloudflare-token-targets.json", "config"],
  ["local-gate.json", "config"], ["claudex-anthropic-settings.json", "config"],
  ["coding_best_practices.md", "config"], ["CLAUDE.md", "config"], ["mcp.json", "config"],
  ["settings.json", "config"],
]);
const NON_RUNTIME_CODE_SEGMENTS = new Set(["__tests__", "docs", "fixtures", "test", "testdata", "tests"]);
const FORBIDDEN_SEGMENTS = new Set([
  ".git",
  ".cache",
  "cache",
  "caches",
  "credentials",
  "credentials.json",
  "logs",
  "sessions",
]);
const HASH_RE = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
const SHA256_RE = /^[0-9a-f]{64}$/;
const SAFE_COMPONENT_RE = /^[^\0-\x1f\x7f/\\]+$/;
const RELEASE_KEYS = [
  "artifactSha256",
  "manifestSha256",
  "schemaVersion",
  "sourceCommit",
  "sourceTree",
  "toolVersions",
  "unitSha256",
];

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function sortedRecord(record) {
  return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
}

export function canonicalJson(value) {
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
  if (isRecord(value)) {
    return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
  }
  if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
  if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
  throw new Error(`value is not canonical JSON: ${typeof value}`);
}

function git(repoRoot, args, encoding = "utf8") {
  try {
    return execFileSync("git", ["-C", repoRoot, ...args], {
      encoding,
      maxBuffer: 128 * 1024 * 1024,
      stdio: ["ignore", "pipe", "pipe"],
    });
  } catch (error) {
    const detail = Buffer.isBuffer(error?.stderr) ? error.stderr.toString("utf8") : String(error?.stderr ?? "");
    throw new Error(`Git ${args[0]} failed${detail.trim() ? `: ${detail.trim()}` : ""}`);
  }
}

function assertClean(repoRoot) {
  const status = git(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
  if (status.length !== 0) throw new Error("dirty Git tree; runtime release requires committed source");
}

function committedJson(repoRoot, commit, path) {
  const bytes = git(repoRoot, ["show", `${commit}:${path}`], "buffer");
  try {
    return JSON.parse(bytes.toString("utf8"));
  } catch {
    throw new Error(`invalid committed JSON: ${path}`);
  }
}

function validateRelativePath(path, label) {
  if (typeof path !== "string" || path === "" || path.startsWith("/")) throw new Error(`invalid ${label}: ${path}`);
  const parts = path.split("/");
  if (parts.some((part) => part === "." || part === ".." || !SAFE_COMPONENT_RE.test(part))) {
    throw new Error(`invalid ${label}: ${path}`);
  }
  return parts;
}

function assertRuntimePath(path) {
  const parts = validateRelativePath(path, "runtime path");
  const forbidden = parts.find((part) => FORBIDDEN_SEGMENTS.has(part.toLowerCase()) || part.toLowerCase().endsWith(".log"));
  if (forbidden) throw new Error(`forbidden runtime path: ${path}`);
}

function releaseManifest(raw) {
  if (!isRecord(raw) || !Array.isArray(raw.entries) || raw.home !== "~/.claude") {
    throw new Error("runtime manifest schema mismatch");
  }
  const keys = Object.keys(raw).sort();
  if (keys.join("\0") !== "entries\0home") throw new Error("runtime manifest schema mismatch");
  const seen = new Set();
  const entries = [];
  for (const rawEntry of raw.entries) {
    if (!isRecord(rawEntry)) throw new Error("runtime manifest entry schema mismatch");
    const entryKeys = Object.keys(rawEntry).sort();
    if (entryKeys.some((key) => !["path", "source", "strategy"].includes(key))) {
      throw new Error("runtime manifest entry schema mismatch");
    }
    if (rawEntry.strategy !== "symlink" && rawEntry.strategy !== "copy") {
      throw new Error(`invalid runtime manifest strategy: ${rawEntry.strategy}`);
    }
    if (rawEntry.source !== undefined && rawEntry.source !== "deploy") {
      throw new Error(`invalid runtime manifest source: ${rawEntry.source}`);
    }
    validateRelativePath(rawEntry.path, "manifest path");
    if (seen.has(rawEntry.path)) throw new Error(`duplicate runtime manifest path: ${rawEntry.path}`);
    seen.add(rawEntry.path);
    if (NON_RUNTIME_ENTRIES.has(rawEntry.path)) continue;
    if (!RUNTIME_PAYLOAD_POLICY.has(rawEntry.path)) {
      throw new Error(`runtime manifest path has no payload policy: ${rawEntry.path}`);
    }
    assertRuntimePath(rawEntry.path);
    const entry = { path: rawEntry.path, strategy: rawEntry.strategy };
    if (rawEntry.source !== undefined) entry.source = rawEntry.source;
    entries.push(entry);
  }
  entries.sort((a, b) => a.path.localeCompare(b.path));
  return { schemaVersion: 1, home: raw.home, entries };
}

function toolVersions(raw) {
  if (!isRecord(raw) || !Array.isArray(raw.tools) || !Array.isArray(raw.vendor)) {
    throw new Error("tool manifest schema mismatch");
  }
  const versions = {};
  for (const [items, field] of [[raw.tools, "ref"], [raw.vendor, "version"]]) {
    for (const item of items) {
      if (!isRecord(item) || typeof item.name !== "string" || typeof item[field] !== "string" || !item.name || !item[field]) {
        throw new Error(`tool manifest ${field} mismatch`);
      }
      if (Object.hasOwn(versions, item.name)) throw new Error(`duplicate tool name: ${item.name}`);
      versions[item.name] = item[field];
    }
  }
  return sortedRecord(versions);
}

function excludedCodePath(manifestEntry, path) {
  if (RUNTIME_PAYLOAD_POLICY.get(manifestEntry) !== "runtime-code") return false;
  const relative = path === manifestEntry ? "" : path.slice(manifestEntry.length + 1);
  const parts = relative.split("/");
  if (parts.some((part) => NON_RUNTIME_CODE_SEGMENTS.has(part))) return true;
  const name = parts.at(-1);
  return name.startsWith("test-") || name.includes(".test.") || name.includes(".spec.");
}

function parseTree(repoRoot, commit, manifest) {
  const files = new Map();
  for (const entry of manifest.entries) {
    const sourcePath = `${MODULE_PATH}/${entry.path}`;
    const output = git(repoRoot, ["ls-tree", "-r", "-z", "--full-tree", commit, "--", sourcePath], "buffer");
    let count = 0;
    for (const record of output.toString("utf8").split("\0")) {
      if (!record) continue;
      const tab = record.indexOf("\t");
      const fields = record.slice(0, tab).split(" ");
      if (tab < 0 || fields.length !== 3 || fields[1] !== "blob") throw new Error(`unreadable Git tree entry: ${entry.path}`);
      const [mode, , oid] = fields;
      const source = record.slice(tab + 1);
      if (source !== sourcePath && !source.startsWith(`${sourcePath}/`)) throw new Error(`nondeterministic Git path: ${source}`);
      const path = source.slice(MODULE_PATH.length + 1);
      if (excludedCodePath(entry.path, path)) continue;
      assertRuntimePath(path);
      if (files.has(path)) continue;
      if (mode !== "100644" && mode !== "100755" && mode !== "120000") throw new Error(`unsupported Git mode ${mode}: ${path}`);
      const content = git(repoRoot, ["cat-file", "blob", oid], "buffer");
      if (mode === "120000") validateSymlink(path, content);
      files.set(path, { path, mode, content });
      count += 1;
    }
    if (count === 0) throw new Error(`committed runtime input missing: ${entry.path}`);
  }
  return files;
}

function validateSymlink(path, content) {
  const target = content.toString("utf8");
  if (!target || target.startsWith("/") || target.includes("\0") || target.includes("\\")) throw new Error(`unsafe runtime symlink: ${path}`);
  const resolved = resolve("/runtime", dirname(path), target);
  if (resolved !== "/runtime" && !resolved.startsWith("/runtime/")) throw new Error(`escaping runtime symlink: ${path}`);
}

function writeString(header, offset, length, value, label) {
  const bytes = Buffer.from(value);
  if (bytes.length > length) throw new Error(`${label} exceeds tar field size`);
  bytes.copy(header, offset);
}

function writeOctal(header, offset, length, value) {
  const octal = value.toString(8).padStart(length - 1, "0");
  if (octal.length !== length - 1) throw new Error("tar numeric field overflow");
  writeString(header, offset, length, `${octal}\0`, "tar numeric field");
}

function splitTarPath(path) {
  const bytes = Buffer.byteLength(path);
  if (bytes <= 100) return { name: path, prefix: "" };
  for (let index = path.lastIndexOf("/"); index > 0; index = path.lastIndexOf("/", index - 1)) {
    const prefix = path.slice(0, index);
    const name = path.slice(index + 1);
    if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) return { name, prefix };
  }
  throw new Error(`runtime path exceeds USTAR limits: ${path}`);
}

function tarHeader({ path, mode, size, type, link = "" }) {
  const header = Buffer.alloc(512);
  const { name, prefix } = splitTarPath(path);
  writeString(header, 0, 100, name, "tar name");
  writeOctal(header, 100, 8, mode);
  writeOctal(header, 108, 8, 0);
  writeOctal(header, 116, 8, 0);
  writeOctal(header, 124, 12, size);
  writeOctal(header, 136, 12, 0);
  header.fill(0x20, 148, 156);
  writeString(header, 156, 1, type, "tar type");
  writeString(header, 157, 100, link, "tar link");
  writeString(header, 257, 6, "ustar\0", "tar magic");
  writeString(header, 263, 2, "00", "tar version");
  writeOctal(header, 329, 8, 0);
  writeOctal(header, 337, 8, 0);
  writeString(header, 345, 155, prefix, "tar prefix");
  const checksum = [...header].reduce((sum, byte) => sum + byte, 0).toString(8).padStart(6, "0");
  writeString(header, 148, 8, `${checksum}\0 `, "tar checksum");
  return header;
}

function createTar(files) {
  const entries = new Map(files);
  for (const path of files.keys()) {
    const parts = path.split("/");
    for (let index = 1; index < parts.length; index += 1) {
      const directory = `${parts.slice(0, index).join("/")}/`;
      if (!entries.has(directory)) entries.set(directory, { path: directory, mode: "040000", content: Buffer.alloc(0) });
    }
  }
  const chunks = [];
  for (const entry of [...entries.values()].sort((a, b) => a.path.localeCompare(b.path))) {
    if (entry.mode === "040000") {
      chunks.push(tarHeader({ path: entry.path, mode: 0o755, size: 0, type: "5" }));
      continue;
    }
    if (entry.mode === "120000") {
      chunks.push(tarHeader({ path: entry.path, mode: 0o777, size: 0, type: "2", link: entry.content.toString("utf8") }));
      continue;
    }
    const mode = entry.mode === "100755" ? 0o755 : 0o644;
    chunks.push(tarHeader({ path: entry.path, mode, size: entry.content.length, type: "0" }), entry.content);
    const padding = (512 - (entry.content.length % 512)) % 512;
    if (padding) chunks.push(Buffer.alloc(padding));
  }
  chunks.push(Buffer.alloc(1024));
  return Buffer.concat(chunks);
}

function tarString(header, offset, length, label) {
  const field = header.subarray(offset, offset + length);
  const nul = field.indexOf(0);
  const content = nul < 0 ? field : field.subarray(0, nul);
  if (nul >= 0 && field.subarray(nul).some((byte) => byte !== 0)) throw new Error(`invalid tar ${label}`);
  const value = content.toString("utf8");
  if (!Buffer.from(value).equals(content)) throw new Error(`invalid UTF-8 tar ${label}`);
  return value;
}

function tarOctal(header, offset, length, label) {
  const field = header.subarray(offset, offset + length).toString("ascii");
  if (!new RegExp(`^[0-7]{${length - 1}}\\0$`).test(field)) throw new Error(`invalid tar ${label}`);
  const value = Number.parseInt(field.slice(0, -1), 8);
  if (!Number.isSafeInteger(value)) throw new Error(`invalid tar ${label}`);
  return value;
}

function validateTarSymlink(path, target) {
  if (!target || target.startsWith("/") || target.includes("\0") || target.includes("\\")) {
    throw new Error(`unsafe runtime artifact symlink: ${path}`);
  }
  const resolved = resolve("/", dirname(path), target);
  if (resolved !== "/runtime" && !resolved.startsWith("/runtime/")) {
    throw new Error(`escaping runtime artifact symlink: ${path}`);
  }
}

function parseTar(artifact) {
  const entries = new Map();
  const entryTypes = new Map();
  let previousPath = "";
  let terminated = false;
  for (let offset = 0; offset + 512 <= artifact.length;) {
    const header = artifact.subarray(offset, offset + 512);
    if (header.every((byte) => byte === 0)) {
      if (offset + 1024 !== artifact.length || !artifact.subarray(offset).every((byte) => byte === 0)) {
        throw new Error("invalid runtime artifact terminator or trailing bytes");
      }
      terminated = true;
      break;
    }
    if (!header.subarray(257, 263).equals(Buffer.from("ustar\0")) || !header.subarray(263, 265).equals(Buffer.from("00"))) {
      throw new Error("invalid runtime artifact USTAR header");
    }
    const storedChecksum = header.subarray(148, 156).toString("ascii");
    if (!/^[0-7]{6}\0 $/.test(storedChecksum)) throw new Error("invalid tar checksum field");
    const checksumHeader = Buffer.from(header);
    checksumHeader.fill(0x20, 148, 156);
    const actualChecksum = [...checksumHeader].reduce((sum, byte) => sum + byte, 0);
    if (Number.parseInt(storedChecksum.slice(0, 6), 8) !== actualChecksum) throw new Error("tar checksum mismatch");

    const name = tarString(header, 0, 100, "name");
    const prefix = tarString(header, 345, 155, "prefix");
    const path = prefix ? `${prefix}/${name}` : name;
    const type = String.fromCharCode(header[156]);
    if (!new Set(["0", "2", "5"]).has(type)) throw new Error(`unsupported runtime artifact tar type: ${type}`);
    if (!name || (type === "5") !== path.endsWith("/")) throw new Error(`invalid runtime artifact path: ${path}`);
    const normalizedPath = type === "5" ? path.slice(0, -1) : path;
    validateRelativePath(normalizedPath, "runtime artifact path");
    if (normalizedPath !== "runtime" && !normalizedPath.startsWith("runtime/")) {
      throw new Error(`runtime artifact entry outside runtime/: ${path}`);
    }
    if (previousPath && previousPath.localeCompare(path) >= 0) throw new Error(`duplicate or unsorted runtime artifact entry: ${path}`);
    previousPath = path;
    const parts = normalizedPath.split("/");
    for (let index = 1; index < parts.length; index += 1) {
      const parent = `${parts.slice(0, index).join("/")}/`;
      if (entryTypes.get(parent) !== "5") throw new Error(`invalid runtime artifact parent: ${path}`);
    }

    const mode = tarOctal(header, 100, 8, "mode");
    const uid = tarOctal(header, 108, 8, "uid");
    const gid = tarOctal(header, 116, 8, "gid");
    const size = tarOctal(header, 124, 12, "size");
    const mtime = tarOctal(header, 136, 12, "mtime");
    const link = tarString(header, 157, 100, "link");
    const uname = tarString(header, 265, 32, "uname");
    const gname = tarString(header, 297, 32, "gname");
    const devMajor = tarOctal(header, 329, 8, "device major");
    const devMinor = tarOctal(header, 337, 8, "device minor");
    const expectedMode = type === "5" ? 0o755 : type === "2" ? 0o777 : mode;
    if (![0o644, 0o755].includes(mode) && type === "0") throw new Error(`invalid regular-file mode: ${path}`);
    if (mode !== expectedMode || uid !== 0 || gid !== 0 || mtime !== 0 || uname || gname || devMajor !== 0 || devMinor !== 0) {
      throw new Error(`nondeterministic runtime artifact metadata: ${path}`);
    }
    if (type !== "0" && size !== 0) throw new Error(`non-regular runtime artifact entry has content: ${path}`);
    if (type === "2") validateTarSymlink(normalizedPath, link);
    else if (link) throw new Error(`unexpected tar link target: ${path}`);
    if (!header.equals(tarHeader({ path, mode, size, type, link }))) {
      throw new Error(`non-canonical runtime artifact header: ${path}`);
    }

    offset += 512;
    const paddedSize = Math.ceil(size / 512) * 512;
    if (offset + paddedSize > artifact.length) throw new Error("truncated runtime artifact");
    const content = artifact.subarray(offset, offset + size);
    if (!artifact.subarray(offset + size, offset + paddedSize).every((byte) => byte === 0)) {
      throw new Error(`non-zero runtime artifact padding: ${path}`);
    }
    entries.set(path, type === "0" ? content : type === "2" ? link : null);
    entryTypes.set(path, type);
    offset += paddedSize;
  }
  if (!terminated) throw new Error("runtime artifact lacks canonical terminator");
  return entries;
}

function validateStringRecord(value, label, valuePattern = null) {
  if (!isRecord(value)) throw new Error(`${label} must be an object`);
  const result = {};
  for (const [key, item] of Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) {
    if (!key || typeof item !== "string" || !item || (valuePattern && !valuePattern.test(item))) {
      throw new Error(`invalid ${label} entry: ${key}`);
    }
    result[key] = item;
  }
  return result;
}

export function validateRuntimeReleaseV1(value) {
  if (!isRecord(value) || Object.keys(value).sort().join("\0") !== RELEASE_KEYS.join("\0")) {
    throw new Error("RuntimeReleaseV1 descriptor schema mismatch");
  }
  if (value.schemaVersion !== RUNTIME_RELEASE_SCHEMA_VERSION) throw new Error("invalid RuntimeReleaseV1 schemaVersion");
  if (!HASH_RE.test(value.sourceCommit)) throw new Error("invalid RuntimeReleaseV1 sourceCommit");
  if (!HASH_RE.test(value.sourceTree)) throw new Error("invalid RuntimeReleaseV1 sourceTree");
  if (!SHA256_RE.test(value.artifactSha256)) throw new Error("invalid RuntimeReleaseV1 artifactSha256");
  if (!SHA256_RE.test(value.manifestSha256)) throw new Error("invalid RuntimeReleaseV1 manifestSha256");
  const descriptor = {
    schemaVersion: value.schemaVersion,
    sourceCommit: value.sourceCommit,
    sourceTree: value.sourceTree,
    artifactSha256: value.artifactSha256,
    manifestSha256: value.manifestSha256,
    toolVersions: validateStringRecord(value.toolVersions, "toolVersions"),
    unitSha256: validateStringRecord(value.unitSha256, "unitSha256", SHA256_RE),
  };
  return descriptor;
}

export function buildRuntimeRelease({ repoRoot, outputDir }) {
  if (!repoRoot || !outputDir) throw new Error("repoRoot and outputDir are required");
  const root = resolve(repoRoot);
  assertClean(root);
  const sourceCommit = git(root, ["rev-parse", "HEAD"]).trim();
  const sourceTree = git(root, ["rev-parse", `${sourceCommit}^{tree}`]).trim();
  if (!HASH_RE.test(sourceCommit) || !HASH_RE.test(sourceTree)) throw new Error("invalid Git source identity");

  const manifest = releaseManifest(committedJson(root, sourceCommit, MANIFEST_PATH));
  const manifestBytes = Buffer.from(`${canonicalJson(manifest)}\n`);
  const files = parseTree(root, sourceCommit, manifest);
  files.set("runtime-manifest.json", { path: "runtime-manifest.json", mode: "100644", content: manifestBytes });
  const archiveFiles = new Map([...files].map(([path, entry]) => [
    `runtime/${path}`,
    { ...entry, path: `runtime/${path}` },
  ]));
  const artifact = createTar(archiveFiles);
  const artifactSha256 = sha256(artifact);
  const unitSha256 = {};
  for (const entry of files.values()) {
    if (entry.path.startsWith("systemd/") && entry.mode !== "120000") unitSha256[entry.path] = sha256(entry.content);
  }
  const descriptor = validateRuntimeReleaseV1({
    schemaVersion: RUNTIME_RELEASE_SCHEMA_VERSION,
    sourceCommit,
    sourceTree,
    artifactSha256,
    manifestSha256: sha256(manifestBytes),
    toolVersions: toolVersions(committedJson(root, sourceCommit, TOOLS_PATH)),
    unitSha256: sortedRecord(unitSha256),
  });

  if (git(root, ["rev-parse", "HEAD"]).trim() !== sourceCommit) {
    throw new Error("Git HEAD moved while building runtime release");
  }
  assertClean(root);
  mkdirSync(outputDir, { recursive: true });
  const artifactPath = join(outputDir, `runtime-release-${artifactSha256}.tar`);
  const descriptorPath = join(outputDir, `runtime-release-${artifactSha256}.json`);
  writeFileSync(artifactPath, artifact, { flag: "wx", mode: 0o644 });
  writeFileSync(descriptorPath, `${canonicalJson(descriptor)}\n`, { flag: "wx", mode: 0o644 });
  return { descriptor, artifactPath, descriptorPath };
}

export function readRuntimeReleaseDescriptor(path) {
  const descriptorBytes = readFileSync(path);
  let parsed;
  try {
    parsed = JSON.parse(descriptorBytes.toString("utf8"));
  } catch {
    throw new Error("invalid RuntimeReleaseV1 descriptor JSON");
  }
  const descriptor = validateRuntimeReleaseV1(parsed);
  const canonical = Buffer.from(`${canonicalJson(descriptor)}\n`);
  if (!descriptorBytes.equals(canonical)) throw new Error("RuntimeReleaseV1 descriptor is not canonical");
  return descriptor;
}

export function verifyRuntimeReleaseArtifact({ descriptor, artifactPath }) {
  const validated = validateRuntimeReleaseV1(descriptor);
  const artifact = readFileSync(artifactPath);
  const actualArtifactSha256 = sha256(artifact);
  if (actualArtifactSha256 !== validated.artifactSha256) {
    throw new Error(`artifact digest mismatch: expected ${validated.artifactSha256}, got ${actualArtifactSha256}`);
  }
  const files = parseTar(artifact);
  if ([...files.keys()].some((path) => !path.startsWith("runtime/"))) {
    throw new Error("runtime artifact has entries outside runtime/");
  }
  const manifest = files.get("runtime/runtime-manifest.json");
  if (!Buffer.isBuffer(manifest) || sha256(manifest) !== validated.manifestSha256) {
    throw new Error("runtime manifest digest mismatch");
  }
  for (const [path, expected] of Object.entries(validated.unitSha256)) {
    const unit = files.get(`runtime/${path}`);
    if (!Buffer.isBuffer(unit) || sha256(unit) !== expected) throw new Error(`systemd unit digest mismatch: ${path}`);
  }
  return validated;
}

export function verifyRuntimeRelease({ descriptorPath, artifactPath }) {
  const descriptor = readRuntimeReleaseDescriptor(descriptorPath);
  return verifyRuntimeReleaseArtifact({ descriptor, artifactPath });
}

const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
if (invokedPath === fileURLToPath(import.meta.url)) {
  const [command, ...args] = process.argv.slice(2);
  if (command === "build" && args.length === 2) {
    const result = buildRuntimeRelease({ repoRoot: args[0], outputDir: args[1] });
    process.stdout.write(`${canonicalJson({ artifactPath: result.artifactPath, descriptorPath: result.descriptorPath, release: result.descriptor })}\n`);
  } else if (command === "verify" && args.length === 2) {
    process.stdout.write(`${canonicalJson(verifyRuntimeRelease({ descriptorPath: args[0], artifactPath: args[1] }))}\n`);
  } else {
    process.stderr.write("usage: runtime-release.mjs build <repo-root> <output-dir> | verify <descriptor> <artifact>\n");
    process.exitCode = 64;
  }
}
