mirror of
https://github.com/morluto/rea.git
synced 2026-10-21 13:22:28 -05:00
* perf(javascript): reuse the source-analysis worker within a process Each analyze-javascript-application call started a new Node worker. That startup, not fixture IO or the shared CLI session, dominated the slow loader-review and factory-completion files. Park one healthy worker on the artifact resource scope and on the CLI process. Stop it when the scope, session, or one-shot process ends. Do not reuse after failure, cancellation, or a different heap or timeout. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(cli): close the JavaScript worker when dispatch throws Minimal dispatcher runtimes now export a no-op closeCliJavaScriptResources, and CLI dispatch plus output rendering always await that cleanup. A dispatch failure and a cleanup failure are both reported. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(artifacts): report close-hook failures with retained cleanup When retained cleanup and the scope close hook both fail, the close error keeps both causes and both resource labels. The pending owners and the hook stay registered so the next close retries each path. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): stop a replaced analysis worker before the next one starts A heap or timeout change now closes the parked worker before constructing its replacement. The scope retains that process only when the close fails. Co-authored-by: morluto <morluto@users.noreply.github.com> * style: format worker-reuse close and launch helpers Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(cli): report a failed JavaScript worker shutdown before exit Share one in-flight close and drop the CLI scope only after it succeeds, so a later caller retries a worker that is still owned. The one-shot exit callback writes that failure and exits non-zero, while a closed stdout pipe still exits 141. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): keep a worker parked during an overlapping limit change Claim the scope slot only when it is still empty after the replaced worker closes. A concurrent analysis that parked another healthy worker keeps that process, and the extra worker is closed through its own port. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(artifacts): run later close hooks when an earlier hook fails Run every registered close hook on each close. One failure is returned unchanged, and several failures are aggregated so a later hook is not skipped. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): fail a limit change when the previous worker stays alive A failed close used to be retained and then ignored, so MCP could return a successful analysis while the old process and its replacement were both owned. The request now fails before the replacement is constructed, and the still-live worker stays owned for a later close. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
258 lines
9.2 KiB
JavaScript
Executable File
258 lines
9.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { access } from "node:fs/promises";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
// Route production MCP before importing Incur. Incur owns registration helpers
|
|
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
|
|
// or, with `--http`, over streamable HTTP.
|
|
const args = process.argv.slice(2);
|
|
const { default: packageJson } = await import("../package.json", {
|
|
with: { type: "json" },
|
|
});
|
|
process.env.REA_PACKAGE_VERSION = packageJson.version;
|
|
const isMcpEntry = args[0] === "--mcp" || args[0] === "mcp";
|
|
const isMcpMode = isMcpEntry && args.length === 1;
|
|
const isMcpDoctorMode = args[0] === "mcp" && args[1] === "doctor";
|
|
// Only flags may follow the entry: `mcp add --http` stays Incur's `mcp add`,
|
|
// while `mcp -h --http` reaches the HTTP entry's own help.
|
|
const isMcpHttpMode =
|
|
isMcpEntry &&
|
|
(args[1]?.startsWith("-") ?? false) &&
|
|
args.slice(1).includes("--http");
|
|
/** Exit status of a writer terminated by SIGPIPE (128 + 13). */
|
|
const SIGPIPE_EXIT_STATUS = 141;
|
|
// Set once the reader of CLI output has closed the pipe.
|
|
let stdoutClosed = false;
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Playwright's registry assumes one of its three desktop host platforms when
|
|
// it chooses the browser cache directory. Termux reports `android`, although
|
|
// its Node runtime and filesystem are POSIX-compatible. Keep the cache path
|
|
// explicit so REA can start on Termux. Browser launch still requires a
|
|
// caller-supplied Android-compatible Chromium executable.
|
|
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 =
|
|
isMcpMode || isMcpHttpMode
|
|
? ["dist/mcp/main.js"]
|
|
: isMcpDoctorMode
|
|
? ["dist/mcp/main.js", "dist/cli-runtime/mcpDoctor.js"]
|
|
: [
|
|
"dist/cli-runtime/cli.js",
|
|
"dist/cli-runtime/cliOutput.js",
|
|
"dist/cli-runtime/cliResultOutput.js",
|
|
];
|
|
|
|
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.exitCode = 1;
|
|
} else if (isMcpMode) {
|
|
const { runEntrypoint } = await import("../dist/mcp/main.js");
|
|
await runEntrypoint();
|
|
} else if (isMcpHttpMode) {
|
|
const { runMcpHttpEntrypoint } = await import("../dist/mcp/main.js");
|
|
await runMcpHttpEntrypoint(args.slice(1));
|
|
} else if (isMcpDoctorMode) {
|
|
guardClosedStdout();
|
|
const { runProductionMcpDoctorCli } =
|
|
await import("../dist/cli-runtime/mcpDoctor.js");
|
|
const result = await runProductionMcpDoctorCli(args.slice(2), {
|
|
dispatcherPath: fileURLToPath(import.meta.url),
|
|
packageRoot,
|
|
});
|
|
writeStdout(result.output);
|
|
process.exitCode = result.exitCode;
|
|
} else {
|
|
const { cliConfigurationError, closeCliJavaScriptResources, createCli } =
|
|
await import("../dist/cli-runtime/cli.js");
|
|
const { createCliResultOutput } =
|
|
await import("../dist/cli-runtime/cliResultOutput.js");
|
|
const {
|
|
normalizeCliArguments,
|
|
renderCliOutputArgumentError,
|
|
renderEmptyFilteredCliOutput,
|
|
validateCliOutputArguments,
|
|
} = await import("../dist/cli-runtime/cliOutput.js");
|
|
guardClosedStdout();
|
|
const cliArgs = normalizeCliArguments(args);
|
|
const outputArguments = validateCliOutputArguments(cliArgs);
|
|
const configurationError = cliConfigurationError(process.env);
|
|
let dispatchFailure;
|
|
// Incur can exit from the callback below before this function returns.
|
|
// That callback owns the status, including a closed-pipe exit of 141.
|
|
let exiting = false;
|
|
try {
|
|
// Doctor is the readiness report for this failure. Other commands still stop
|
|
// here so they do not run with an unusable log level.
|
|
if (configurationError !== undefined && !isDoctorCommand(cliArgs)) {
|
|
process.stderr.write(`${configurationError}\n`);
|
|
process.exitCode = 1;
|
|
} else if (!outputArguments.ok) {
|
|
writeStdout(renderCliOutputArgumentError(outputArguments));
|
|
process.exitCode = 1;
|
|
} else {
|
|
let wroteOutput = false;
|
|
const resultOutput = createCliResultOutput(cliArgs, process.stdout);
|
|
try {
|
|
await createCli(process.env, resultOutput).serve(cliArgs, {
|
|
stdout: (output) => {
|
|
if (resultOutput?.handled) {
|
|
if (resultOutput.failed && !stdoutClosed)
|
|
process.stderr.write(output);
|
|
return;
|
|
}
|
|
if (output.length > 0) wroteOutput = true;
|
|
writeStdout(output);
|
|
},
|
|
// A command failure caused by the closed pipe keeps the pipe's status.
|
|
// Worker shutdown is shared with the dispatch finally below. A failed
|
|
// shutdown is reported here because this callback is what exits.
|
|
exit: (code) => {
|
|
exiting = true;
|
|
const status = stdoutClosed ? SIGPIPE_EXIT_STATUS : code;
|
|
void Promise.resolve()
|
|
.then(() => closeCliJavaScriptResources())
|
|
.then(
|
|
() => {
|
|
process.exit(status);
|
|
},
|
|
(cleanup) => {
|
|
process.stderr.write(
|
|
`Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`,
|
|
);
|
|
process.exit(
|
|
stdoutClosed
|
|
? SIGPIPE_EXIT_STATUS
|
|
: status === 0
|
|
? 1
|
|
: status,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
});
|
|
} catch (cause) {
|
|
if (wroteOutput) throw cause;
|
|
const recovered = await resultOutput
|
|
?.recoverFormattingFailure(cause)
|
|
.catch((writeFailure) => {
|
|
// Recovery writes outside Incur's command boundary. A reader that
|
|
// declined this document keeps the same quiet SIGPIPE outcome.
|
|
if (!stdoutClosed) throw writeFailure;
|
|
return true;
|
|
});
|
|
if (!recovered) throw cause;
|
|
process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1;
|
|
}
|
|
if (!wroteOutput && !resultOutput?.handled)
|
|
writeStdout(renderEmptyFilteredCliOutput(cliArgs) ?? "");
|
|
}
|
|
} catch (cause) {
|
|
dispatchFailure = cause;
|
|
} finally {
|
|
// Serve and output rendering can throw after a worker is parked. Stop it
|
|
// on that path too, and keep the dispatch failure when both fail.
|
|
try {
|
|
await closeCliJavaScriptResources();
|
|
} catch (cleanup) {
|
|
// The exit callback reports this same shared failure and then exits.
|
|
if (!exiting) {
|
|
dispatchFailure =
|
|
dispatchFailure === undefined
|
|
? cleanup
|
|
: new AggregateError(
|
|
[dispatchFailure, cleanup],
|
|
"CLI dispatch failed and the JavaScript analysis worker remained owned",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (!exiting && dispatchFailure !== undefined) throw dispatchFailure;
|
|
}
|
|
|
|
/** True when the first positional command is `doctor`, after global output flags. */
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* A reader that closes the pipe early, as `rea ... | head` does, has declined
|
|
* the rest of the output. Stop writing without reporting a failure, and exit
|
|
* with the status a SIGPIPE-terminated writer reports. MCP stdio keeps its own
|
|
* client lifecycle.
|
|
*/
|
|
function cleanupFailureText(cleanup) {
|
|
if (cleanup instanceof Error) return cleanup.stack ?? cleanup.message;
|
|
return String(cleanup);
|
|
}
|
|
|
|
function guardClosedStdout() {
|
|
process.stdout.on("error", (cause) => {
|
|
if (cause?.code === "EPIPE") {
|
|
stdoutClosed = true;
|
|
process.exitCode = SIGPIPE_EXIT_STATUS;
|
|
return;
|
|
}
|
|
// Other failures stay unhandled unless a pending JSON write reports them.
|
|
if (process.stdout.listenerCount("error") === 1) throw cause;
|
|
});
|
|
}
|
|
|
|
function writeStdout(output) {
|
|
if (!stdoutClosed) process.stdout.write(output);
|
|
}
|
|
|
|
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"
|
|
);
|
|
}
|