2026-09-05 12:33:52 -07:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
2026-09-10 18:27:41 -07:00
|
|
|
import { readFile, writeFile } from "node:fs/promises";
|
2026-09-05 12:33:52 -07:00
|
|
|
import process from "node:process";
|
|
|
|
|
import { compileWorkspaceRepository } from "./assembly.js";
|
|
|
|
|
import { createGitCapabilityResolver } from "./git-resolver.js";
|
2026-09-27 15:53:34 -07:00
|
|
|
import { loadQueryGenerator } from "./query-generator.js";
|
2026-10-01 01:37:46 -07:00
|
|
|
import { loadCheckedResources } from "./checked-resources.js";
|
|
|
|
|
import { profile } from "./profile.js";
|
2026-09-30 22:03:17 -07:00
|
|
|
import { compileExecutionPlan, packageBuildContexts } from "../execution-world/index.js";
|
2026-09-16 00:12:39 -07:00
|
|
|
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
|
2026-09-15 15:23:24 -07:00
|
|
|
import {
|
|
|
|
|
planEvolution,
|
|
|
|
|
runtimeContracts,
|
|
|
|
|
type EvolutionReview,
|
|
|
|
|
type WorkspaceRevision,
|
|
|
|
|
} from "../capability-model/index.js";
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
|
|
|
|
|
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
|
2026-09-10 18:27:41 -07:00
|
|
|
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
|
2026-09-30 22:03:17 -07:00
|
|
|
[--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH] [--build-contexts-out PATH]
|
2026-10-01 01:37:46 -07:00
|
|
|
[--query-generator BUILD_MODULE_PATH] [--checked-resources NIX_MAP]
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
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<string, string>();
|
|
|
|
|
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"),
|
2026-09-16 00:12:39 -07:00
|
|
|
schemasOut: values.get("--schemas-out"),
|
2026-09-19 23:05:52 -07:00
|
|
|
executionPlanOut: values.get("--execution-plan-out"),
|
2026-09-30 22:03:17 -07:00
|
|
|
buildContextsOut: values.get("--build-contexts-out"),
|
2026-09-05 12:33:52 -07:00
|
|
|
snapshotMap: values.get("--snapshot-map"),
|
|
|
|
|
workspaceId: values.get("--workspace-id"),
|
|
|
|
|
workspaceRevisionId: values.get("--workspace-revision-id"),
|
|
|
|
|
sourceRootCommit: values.get("--source-root-commit"),
|
2026-09-10 18:27:41 -07:00
|
|
|
baseline: values.get("--baseline"),
|
|
|
|
|
evolutionOut: values.get("--evolution-out"),
|
|
|
|
|
reviews: values.get("--reviews"),
|
2026-09-27 15:53:34 -07:00
|
|
|
queryGenerator: values.get("--query-generator"),
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResources: values.get("--checked-resources"),
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
2026-10-01 01:37:46 -07:00
|
|
|
const assemblyStarted = performance.now();
|
2026-09-05 12:33:52 -07:00
|
|
|
const assembled = await compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: options.rootDirectory,
|
|
|
|
|
workspaceId: options.workspaceId,
|
|
|
|
|
workspaceRevisionId: options.workspaceRevisionId,
|
|
|
|
|
sourceRootCommit: options.sourceRootCommit,
|
|
|
|
|
resolveResource,
|
2026-09-27 15:53:34 -07:00
|
|
|
generateQueries: await loadQueryGenerator(options.queryGenerator),
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResource: await loadCheckedResources(options.checkedResources),
|
2026-09-05 12:33:52 -07:00
|
|
|
});
|
2026-10-01 01:37:46 -07:00
|
|
|
profile("workspace-assembly", assemblyStarted, { resources: assembled.resources.length });
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
if (options.graphOut) {
|
2026-09-15 15:23:24 -07:00
|
|
|
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,
|
2026-09-22 13:51:32 -07:00
|
|
|
...(node.resource.kind === "build"
|
|
|
|
|
? {}
|
|
|
|
|
: {
|
|
|
|
|
resourceId:
|
|
|
|
|
node.resource.kind === "interface"
|
|
|
|
|
? node.resource.revision.interfaceId
|
|
|
|
|
: node.resource.revision.packageId,
|
|
|
|
|
revisionId: node.resource.revision.revisionId,
|
|
|
|
|
}),
|
2026-09-15 15:23:24 -07:00
|
|
|
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
|
|
|
|
binding,
|
|
|
|
|
resourceKey: dependency.key,
|
|
|
|
|
})),
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
)}\n`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
2026-09-10 18:27:41 -07:00
|
|
|
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
|
2026-09-30 22:03:17 -07:00
|
|
|
if (options.executionPlanOut || options.buildContextsOut) {
|
2026-10-01 01:37:46 -07:00
|
|
|
const executionStarted = performance.now();
|
2026-09-30 22:03:17 -07:00
|
|
|
const plan = compileExecutionPlan(assembled.workspace);
|
|
|
|
|
if (options.executionPlanOut) await writeFile(options.executionPlanOut, `${JSON.stringify(plan, null, 2)}\n`);
|
|
|
|
|
if (options.buildContextsOut)
|
|
|
|
|
await writeFile(options.buildContextsOut, JSON.stringify(packageBuildContexts(plan.definition.bindingSchema)));
|
2026-10-01 01:37:46 -07:00
|
|
|
profile("execution-plan-and-contexts", executionStarted);
|
2026-09-19 23:05:52 -07:00
|
|
|
}
|
2026-09-16 00:12:39 -07:00
|
|
|
if (options.schemasOut) {
|
2026-10-01 01:37:46 -07:00
|
|
|
const schemasStarted = performance.now();
|
2026-09-16 00:12:39 -07:00
|
|
|
const schemas: Record<string, unknown> = {};
|
2026-09-22 13:51:32 -07:00
|
|
|
for (const node of assembled.capabilityResources.filter((entry) => entry.kind === "package")) {
|
2026-09-16 00:12:39 -07:00
|
|
|
const closure = new Map<string, typeof node>();
|
|
|
|
|
const visit = (entry: typeof node) => {
|
2026-09-22 13:51:32 -07:00
|
|
|
if (closure.has(entry.key) || entry.kind === "build") return;
|
2026-09-16 00:12:39 -07:00
|
|
|
closure.set(entry.key, entry);
|
|
|
|
|
entry.dependencies.forEach(visit);
|
|
|
|
|
};
|
|
|
|
|
visit(node);
|
2026-09-22 13:51:32 -07:00
|
|
|
if (node.resource.kind !== "package") continue;
|
2026-09-16 00:12:39 -07:00
|
|
|
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
|
|
|
|
|
bindingSchema({ resources: [...closure.values()] }),
|
|
|
|
|
candidate,
|
|
|
|
|
node.resource.revision.revisionId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
await writeFile(options.schemasOut, JSON.stringify(schemas));
|
2026-10-01 01:37:46 -07:00
|
|
|
profile("package-schemas", schemasStarted);
|
2026-09-16 00:12:39 -07:00
|
|
|
}
|
2026-09-10 18:27:41 -07:00
|
|
|
if (options.evolutionOut) {
|
2026-09-15 15:23:24 -07:00
|
|
|
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[]) : [];
|
2026-09-10 18:27:41 -07:00
|
|
|
if (!Array.isArray(reviews)) throw new Error("Review file must contain an array");
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
options.evolutionOut,
|
|
|
|
|
`${JSON.stringify(planEvolution(baseline, candidate, { reviews }), null, 2)}\n`,
|
|
|
|
|
);
|
2026-09-10 18:27:41 -07:00
|
|
|
}
|
|
|
|
|
process.stdout.write(`${JSON.stringify(candidate, null, 2)}\n`);
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
main().catch((error: unknown) => {
|
2026-09-15 15:23:24 -07:00
|
|
|
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
2026-09-05 12:33:52 -07:00
|
|
|
process.exitCode = 1;
|
|
|
|
|
});
|