mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -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>
274 lines
8.2 KiB
JavaScript
274 lines
8.2 KiB
JavaScript
import { Writable } from "node:stream";
|
|
import { access } from "node:fs/promises";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
createCliSessionFrameReader,
|
|
encodeCliSessionFrame,
|
|
} from "./cliCommandSessionFrames.mjs";
|
|
|
|
// One-shot command sequence matches scripts/rea.mjs. Output is captured per
|
|
// command so this process can serve the next invocation without restarting.
|
|
const packageRoot = resolve(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
"../../..",
|
|
);
|
|
const { default: packageJson } = await import(
|
|
new URL("../../../package.json", import.meta.url),
|
|
{ with: { type: "json" } }
|
|
);
|
|
|
|
const prepareCliProcess = () => {
|
|
process.env.REA_PACKAGE_VERSION = packageJson.version;
|
|
// Windows home lookup throws for an empty USERPROFILE. Dependencies resolve
|
|
// home during import, before REA's own resolver can apply its account fallback.
|
|
if (process.platform === "win32" && process.env.USERPROFILE === "") {
|
|
delete process.env.USERPROFILE;
|
|
}
|
|
if (
|
|
process.platform === "android" &&
|
|
process.env.PLAYWRIGHT_BROWSERS_PATH === undefined
|
|
) {
|
|
const cacheRoot =
|
|
process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache");
|
|
process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
|
}
|
|
};
|
|
|
|
const runtimeFiles = [
|
|
"dist/cli-runtime/cli.js",
|
|
"dist/cli-runtime/cliOutput.js",
|
|
"dist/cli-runtime/cliResultOutput.js",
|
|
];
|
|
|
|
prepareCliProcess();
|
|
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
|
process.stderr.write(
|
|
`REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
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 writeFrame = (value) =>
|
|
new Promise((resolveWrite, rejectWrite) => {
|
|
process.stdout.write(encodeCliSessionFrame(value), (error) => {
|
|
if (error) rejectWrite(error);
|
|
else resolveWrite();
|
|
});
|
|
});
|
|
|
|
await writeFrame({ ready: true });
|
|
|
|
let chain = Promise.resolve();
|
|
const reader = createCliSessionFrameReader((value) => {
|
|
chain = chain.then(
|
|
() => handle(value),
|
|
() => handle(value),
|
|
);
|
|
});
|
|
process.stdin.on("data", (chunk) => {
|
|
try {
|
|
reader(chunk);
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
process.stdin.on("end", () => {
|
|
chain.finally(async () => {
|
|
await closeCliJavaScriptResources();
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
async function handle(value) {
|
|
const request = value;
|
|
const savedEnv = { ...process.env };
|
|
const savedCwd = process.cwd();
|
|
try {
|
|
if (
|
|
request === null ||
|
|
typeof request !== "object" ||
|
|
!Array.isArray(request.arguments) ||
|
|
typeof request.cwd !== "string" ||
|
|
request.env === null ||
|
|
typeof request.env !== "object"
|
|
) {
|
|
throw new Error(
|
|
"CLI session command is missing its arguments, cwd, or environment",
|
|
);
|
|
}
|
|
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
Object.assign(process.env, request.env);
|
|
prepareCliProcess();
|
|
process.chdir(request.cwd);
|
|
const result = await runOneShot(request.arguments);
|
|
await writeFrame({
|
|
id: request.id,
|
|
pid: process.pid,
|
|
rss: process.memoryUsage().rss,
|
|
...result,
|
|
});
|
|
} catch (error) {
|
|
const id =
|
|
request !== null && typeof request === "object" ? request.id : undefined;
|
|
await writeFrame({
|
|
id,
|
|
pid: process.pid,
|
|
exitCode: 1,
|
|
stdout: "",
|
|
stderr:
|
|
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
fatal: true,
|
|
});
|
|
process.exit(1);
|
|
} finally {
|
|
for (const key of Object.keys(process.env)) delete process.env[key];
|
|
Object.assign(process.env, savedEnv);
|
|
process.chdir(savedCwd);
|
|
}
|
|
}
|
|
|
|
async function runOneShot(args) {
|
|
const stdoutChunks = [];
|
|
const stderrChunks = [];
|
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
process.stdout.write = captureWrite(stdoutChunks);
|
|
process.stderr.write = captureWrite(stderrChunks);
|
|
const destination = new Writable({
|
|
write(chunk, encoding, callback) {
|
|
process.stdout.write(chunk, encoding, callback);
|
|
},
|
|
});
|
|
destination.isTTY = process.stdout.isTTY === true;
|
|
const previousExitCode = process.exitCode;
|
|
process.exitCode = undefined;
|
|
let explicitExit;
|
|
try {
|
|
const cliArgs = normalizeCliArguments(args);
|
|
const outputOptions = parseCliOutputArguments(cliArgs);
|
|
const outputArguments = outputOptions.validation;
|
|
const configurationError = cliConfigurationError(process.env);
|
|
if (configurationError !== undefined && !isDoctorCommand(cliArgs)) {
|
|
process.stderr.write(`${configurationError}\n`);
|
|
process.exitCode = 1;
|
|
} else if (!outputArguments.ok) {
|
|
process.stdout.write(renderCliOutputArgumentError(outputArguments));
|
|
process.exitCode = 1;
|
|
} else {
|
|
let wroteOutput = false;
|
|
const resultOutput = createCliResultOutput(outputOptions, destination);
|
|
try {
|
|
await createCli(process.env, resultOutput).serve(cliArgs, {
|
|
stdout: (output) => {
|
|
if (resultOutput?.handled) {
|
|
if (resultOutput.failed) process.stderr.write(output);
|
|
return;
|
|
}
|
|
if (output.length > 0) wroteOutput = true;
|
|
if (resultOutput?.captureSdkOutput(output)) return;
|
|
process.stdout.write(output);
|
|
},
|
|
exit: (code) => {
|
|
explicitExit = code;
|
|
},
|
|
});
|
|
} catch (cause) {
|
|
if (wroteOutput) throw cause;
|
|
const recovered = await resultOutput?.recoverFormattingFailure(cause);
|
|
if (!recovered) throw cause;
|
|
process.exitCode = 1;
|
|
}
|
|
if (!wroteOutput && !resultOutput?.handled) {
|
|
const empty = renderEmptyFilteredCliOutput(outputOptions);
|
|
if (empty) process.stdout.write(empty);
|
|
}
|
|
await resultOutput?.finishSdkOutput(
|
|
explicitExit ??
|
|
(typeof process.exitCode === "number" ? process.exitCode : 0),
|
|
);
|
|
}
|
|
} finally {
|
|
process.stdout.write = originalStdoutWrite;
|
|
process.stderr.write = originalStderrWrite;
|
|
}
|
|
const exitCode =
|
|
explicitExit ??
|
|
(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
process.exitCode = previousExitCode;
|
|
return {
|
|
exitCode,
|
|
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
};
|
|
}
|
|
|
|
function captureWrite(chunks) {
|
|
return function patched(chunk, encoding, callback) {
|
|
const encodingName = typeof encoding === "string" ? encoding : "utf8";
|
|
chunks.push(
|
|
Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), encodingName),
|
|
);
|
|
const done = typeof encoding === "function" ? encoding : callback;
|
|
if (typeof done === "function") done();
|
|
return true;
|
|
};
|
|
}
|
|
|
|
function isDoctorCommand(arguments_) {
|
|
const valuedFlags = new Set([
|
|
"--format",
|
|
"--filter-output",
|
|
"--token-limit",
|
|
"--token-offset",
|
|
]);
|
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
const argument = arguments_[index];
|
|
if (argument === "--") return arguments_[index + 1] === "doctor";
|
|
if (valuedFlags.has(argument)) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (argument.startsWith("-")) continue;
|
|
return argument === "doctor";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function compiledRuntimeExists(paths) {
|
|
for (const path of paths) {
|
|
try {
|
|
await access(resolve(packageRoot, path));
|
|
} catch (cause) {
|
|
if (isMissing(cause)) return false;
|
|
throw cause;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isMissing(cause) {
|
|
return (
|
|
typeof cause === "object" &&
|
|
cause !== null &&
|
|
"code" in cause &&
|
|
cause.code === "ENOENT"
|
|
);
|
|
}
|