Files
quixos-protocol/src/capability-language/cli.ts
T

96 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import process from "node:process";
import { compileCapabilitySource } from "./parser.js";
import { capabilityId } from "../capability-model/types.js";
import { compileWorkspaceRevision } from "../capability-model/validation.js";
const usage = `usage: quixos-capability-compile [--check]
[--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] <workspace.qx | ->
Parses, lowers, and validates a Quixos capability workspace. By default the
checked semantic workspace revision is written as JSON. --check emits no JSON.
The identity overrides instantiate a checked built-in/template assembly for one
real workspace; both must be supplied together.`;
const parseArgs = (args: string[]) => {
let checkOnly = false;
let workspaceId: string | undefined;
let workspaceRevisionId: string | undefined;
let sourceRootCommit: string | undefined;
const positional: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const argument = args[index]!;
if (argument === "--check") checkOnly = true;
else if (argument === "--workspace-id") workspaceId = args[++index];
else if (argument === "--workspace-revision-id") workspaceRevisionId = args[++index];
else if (argument === "--source-root-commit") sourceRootCommit = args[++index];
else if (argument === "-") positional.push(argument);
else if (argument.startsWith("-")) throw new Error(usage);
else positional.push(argument);
}
if (positional.length !== 1 || Boolean(workspaceId) !== Boolean(workspaceRevisionId)) {
throw new Error(usage);
}
return { checkOnly, workspaceId, workspaceRevisionId, sourceRootCommit, fileName: positional[0]! };
};
const main = async () => {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
process.stdout.write(`${usage}\n`);
return;
}
const {
checkOnly,
workspaceId,
workspaceRevisionId,
sourceRootCommit,
fileName,
} = parseArgs(args);
const source =
fileName === "-"
? await new Promise<string>((resolve, reject) => {
const chunks: Buffer[] = [];
process.stdin.on("data", (chunk: Buffer) => chunks.push(chunk));
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
process.stdin.on("error", reject);
})
: await readFile(fileName, "utf8");
const result = compileCapabilitySource(source, fileName);
if (!result.ok) {
for (const diagnostic of result.diagnostics) {
const location = diagnostic.line
? `${diagnostic.fileName}:${diagnostic.line}:${diagnostic.column + 1}`
: `${diagnostic.fileName}${diagnostic.path ? `:${diagnostic.path}` : ""}`;
process.stderr.write(
`${location}: ${diagnostic.phase} ${diagnostic.code}: ${diagnostic.message}\n`,
);
}
process.exitCode = 1;
return;
}
const workspace = workspaceId && workspaceRevisionId
? {
...result.workspace,
workspaceId: capabilityId.workspace(workspaceId),
id: capabilityId.workspaceRevision(workspaceRevisionId),
...(sourceRootCommit ? { sourceRootCommit } : {}),
}
: { ...result.workspace, ...(sourceRootCommit ? { sourceRootCommit } : {}) };
const instantiated = compileWorkspaceRevision(workspace);
if (!instantiated.ok) {
throw new Error(instantiated.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
}
if (!checkOnly) {
process.stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
}
};
main().catch((error: unknown) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});