mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
5d2c8c2b78
* feat(cli): add JSON Lines output for row-oriented results (#5487) - Support opt-in JSON Lines format (--format jsonl) for row-oriented results and collection views with clear record boundaries - Preserve stable source identities and stream records deterministically line-by-line - Emit final coverage and limitation metadata as a distinct line with https://rea.dev/schemas/jsonl-coverage-v1.json to prevent confusion with data rows - Distinguish error outputs with https://rea.dev/schemas/jsonl-error-v1.json - Reject non-row-oriented results that lack record boundaries with an unreadable_output error - Document JSON Lines streaming conventions in docs/cli.md - Add boundary tests covering empty results, multiline strings, partial coverage, large fields, and non-row rejection * fix(cli): preserve diagnostics, provenance, and nested collections in JSONL * fix(cli): frame classified failures and reject unrepresented JSONL facets --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
152 lines
4.4 KiB
JavaScript
152 lines
4.4 KiB
JavaScript
import { Writable } from "node:stream";
|
|
import { createInterface } from "node:readline";
|
|
|
|
const { closeCliJavaScriptResources, createCli } =
|
|
await import("../../../dist/cli-runtime/cli.js");
|
|
const { createCliResultOutput } =
|
|
await import("../../../dist/cli-runtime/cliResultOutput.js");
|
|
const {
|
|
cliConfigurationError,
|
|
normalizeCliArguments,
|
|
renderCliOutputArgumentError,
|
|
renderEmptyFilteredCliOutput,
|
|
parseCliOutputArguments,
|
|
} = await import("../../../dist/cli-runtime/cliOutput.js");
|
|
|
|
const MAX_STDOUT_CHARS = 16 * 1024 * 1024;
|
|
|
|
const writeMessage = (message) => {
|
|
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
};
|
|
|
|
// One CLI instance serves every command. A fresh instance per command retains
|
|
// its command registrations after the call returns; production exits instead.
|
|
const outputParts = [];
|
|
let outputCharacters = 0;
|
|
const destination = new Writable({
|
|
write(chunk, _encoding, callback) {
|
|
const text = Buffer.isBuffer(chunk)
|
|
? chunk.toString("utf8")
|
|
: String(chunk);
|
|
outputCharacters += text.length;
|
|
if (outputCharacters > MAX_STDOUT_CHARS) {
|
|
callback(new Error("Compiled CLI stdout exceeded 16 MiB"));
|
|
return;
|
|
}
|
|
outputParts.push(text);
|
|
callback();
|
|
},
|
|
});
|
|
Object.defineProperty(destination, "isTTY", { value: false });
|
|
const resultOutput = createCliResultOutput(
|
|
parseCliOutputArguments(["--json"]),
|
|
destination,
|
|
);
|
|
const cli = createCli(process.env, resultOutput);
|
|
|
|
const resetOutput = () => {
|
|
outputParts.length = 0;
|
|
outputCharacters = 0;
|
|
};
|
|
|
|
const runCommand = async (args) => {
|
|
const previousExitCode = process.exitCode;
|
|
process.exitCode = undefined;
|
|
resetOutput();
|
|
try {
|
|
const cliArgs = normalizeCliArguments(args);
|
|
const outputOptions = parseCliOutputArguments(cliArgs);
|
|
const outputArguments = outputOptions.validation;
|
|
if (!outputArguments.ok)
|
|
return {
|
|
exitCode: 1,
|
|
stdout: renderCliOutputArgumentError(outputArguments),
|
|
};
|
|
const configurationError = cliConfigurationError(process.env);
|
|
if (configurationError !== undefined)
|
|
return { exitCode: 1, stdout: "", error: configurationError };
|
|
|
|
let wroteOutput = false;
|
|
let explicitExit;
|
|
resultOutput?.begin(outputOptions);
|
|
try {
|
|
await cli.serve(cliArgs, {
|
|
stdout: (output) => {
|
|
if (resultOutput?.handled) return;
|
|
if (output.length === 0) return;
|
|
wroteOutput = true;
|
|
if (resultOutput?.captureSdkOutput(output)) return;
|
|
destination.write(output);
|
|
},
|
|
exit: (code) => {
|
|
explicitExit = code;
|
|
},
|
|
});
|
|
} catch (cause) {
|
|
return {
|
|
exitCode: 1,
|
|
stdout: outputParts.join(""),
|
|
error:
|
|
cause instanceof Error
|
|
? (cause.stack ?? cause.message)
|
|
: String(cause),
|
|
};
|
|
}
|
|
if (!wroteOutput && !resultOutput?.handled) {
|
|
const empty = renderEmptyFilteredCliOutput(outputOptions);
|
|
if (empty !== undefined) destination.write(empty);
|
|
}
|
|
await resultOutput?.finishSdkOutput(
|
|
explicitExit ??
|
|
(typeof process.exitCode === "number" ? process.exitCode : 0),
|
|
);
|
|
// logCliCommand reports some failures only through process.exitCode.
|
|
return {
|
|
exitCode:
|
|
explicitExit ??
|
|
(typeof process.exitCode === "number" ? process.exitCode : 0),
|
|
stdout: outputParts.join(""),
|
|
};
|
|
} finally {
|
|
process.exitCode = previousExitCode;
|
|
}
|
|
};
|
|
|
|
const acceptRequest = async (line) => {
|
|
let request;
|
|
try {
|
|
request = JSON.parse(line);
|
|
} catch (cause) {
|
|
writeMessage({
|
|
id: -1,
|
|
exitCode: 1,
|
|
stdout: "",
|
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
});
|
|
return;
|
|
}
|
|
const args = request?.args;
|
|
if (
|
|
typeof request?.id !== "number" ||
|
|
!Array.isArray(args) ||
|
|
args.some((argument) => typeof argument !== "string")
|
|
) {
|
|
writeMessage({
|
|
id: typeof request?.id === "number" ? request.id : -1,
|
|
exitCode: 1,
|
|
stdout: "",
|
|
error: "Compiled CLI session expected an id and string arguments",
|
|
});
|
|
return;
|
|
}
|
|
writeMessage({ id: request.id, ...(await runCommand(args)) });
|
|
};
|
|
|
|
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
writeMessage({ ready: true });
|
|
for await (const line of lines) {
|
|
if (line.trim().length === 0) continue;
|
|
await acceptRequest(line);
|
|
}
|
|
await closeCliJavaScriptResources();
|