#!/usr/bin/env node import { readFile, writeFile } from "node:fs/promises"; import process from "node:process"; import { compileWorkspaceRepository } from "./assembly.js"; import { createGitCapabilityResolver } from "./git-resolver.js"; import { bindingSchema, specializeBindingSchema } from "../bindings/index.js"; import { planEvolution, runtimeContracts, type EvolutionReview, type WorkspaceRevision, } from "../capability-model/index.js"; const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY [--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID] [--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH] [--reviews REVIEW_JSON] [--schemas-out PATH] Resolves a workspace's recursive resource-lock graph, clones every exact resource revision, validates standalone interface/package manifests, and emits the checked workspace plan as JSON.`; const parseArgs = (args: string[]) => { const values = new Map(); for (let index = 0; index < args.length; index += 2) { const key = args[index]; const value = args[index + 1]; if (!key?.startsWith("--") || !value) throw new Error(usage); values.set(key, value); } const rootDirectory = values.get("--root"); const checkoutRoot = values.get("--checkout-root"); if (!rootDirectory || !checkoutRoot) throw new Error(usage); return { rootDirectory, checkoutRoot, graphOut: values.get("--graph-out"), schemasOut: values.get("--schemas-out"), snapshotMap: values.get("--snapshot-map"), workspaceId: values.get("--workspace-id"), workspaceRevisionId: values.get("--workspace-revision-id"), sourceRootCommit: values.get("--source-root-commit"), baseline: values.get("--baseline"), evolutionOut: values.get("--evolution-out"), reviews: values.get("--reviews"), }; }; const main = async () => { if (process.argv.includes("--help") || process.argv.includes("-h")) { process.stdout.write(`${usage}\n`); return; } const options = parseArgs(process.argv.slice(2)); const resolveResource = await createGitCapabilityResolver({ checkoutRoot: options.checkoutRoot, snapshotMap: options.snapshotMap, }); const assembled = await compileWorkspaceRepository({ rootDirectory: options.rootDirectory, workspaceId: options.workspaceId, workspaceRevisionId: options.workspaceRevisionId, sourceRootCommit: options.sourceRootCommit, resolveResource, }); if (options.graphOut) { await writeFile( options.graphOut, `${JSON.stringify( { formatVersion: 1, quixos: assembled.lock.quixos, directResources: [...assembled.directResources.entries()].map(([bindingKey, node]) => { const [kind, binding] = bindingKey.split("\0"); return { kind, binding, resourceKey: node.key, directory: node.directory }; }), resources: assembled.resources.map((node) => ({ key: node.key, kind: node.kind, source: node.source, directory: node.directory, resourceId: node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId, revisionId: node.resource.revision.revisionId, dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({ binding, resourceKey: dependency.key, })), })), }, null, 2, )}\n`, ); } const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) }; if (options.schemasOut) { const schemas: Record = {}; for (const node of assembled.resources.filter((entry) => entry.kind === "package")) { const closure = new Map(); const visit = (entry: typeof node) => { if (closure.has(entry.key)) return; closure.set(entry.key, entry); entry.dependencies.forEach(visit); }; visit(node); schemas[node.resource.revision.revisionId] = specializeBindingSchema( bindingSchema({ resources: [...closure.values()] }), candidate, node.resource.revision.revisionId, ); } await writeFile(options.schemasOut, JSON.stringify(schemas)); } if (options.evolutionOut) { const baseline = options.baseline ? (JSON.parse(await readFile(options.baseline, "utf8")) as WorkspaceRevision) : null; const reviews = options.reviews ? (JSON.parse(await readFile(options.reviews, "utf8")) as EvolutionReview[]) : []; if (!Array.isArray(reviews)) throw new Error("Review file must contain an array"); await writeFile( options.evolutionOut, `${JSON.stringify(planEvolution(baseline, candidate, { reviews }), null, 2)}\n`, ); } process.stdout.write(`${JSON.stringify(candidate, null, 2)}\n`); }; main().catch((error: unknown) => { process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`); process.exitCode = 1; });