refactor(cli): share process environment preparation
Give the launcher and shared command worker one lightweight owner for package-version binding, Windows empty-home handling and Android browser-cache defaults. Preserve per-command preparation and environment restoration, package the owner explicitly, and retain its package and JavaScript recovery CI scopes. Validation: dispatcher/version/shared-session, home fallback, evaluator/MCP/process cleanup and CI scope tests passed. Functional archive/install/repack, installed static CLI analysis and MCP checks passed; no real Codex/model trial or Windows/Termux runtime qualification was performed.
This commit is contained in:
1 parent
3ad465a666
commit
c2caae88f8
9 files changed
+72
-43
No files matched your search
@@ -69,6 +69,7 @@
|
|||||||
"bridge/android/ReaJadxBridge.java",
|
"bridge/android/ReaJadxBridge.java",
|
||||||
"bridge/mitmproxy/capture.py",
|
"bridge/mitmproxy/capture.py",
|
||||||
"scripts/rea.mjs",
|
"scripts/rea.mjs",
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
"scripts/rea-android-workspace-guardian.mjs",
|
"scripts/rea-android-workspace-guardian.mjs",
|
||||||
"scripts/electron-active-hook.cjs",
|
"scripts/electron-active-hook.cjs",
|
||||||
"scripts/electron-active-hook-boundaries.cjs",
|
"scripts/electron-active-hook-boundaries.cjs",
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const SCOPES = {
|
|||||||
"scripts/prepack.mjs",
|
"scripts/prepack.mjs",
|
||||||
"scripts/prepare.mjs",
|
"scripts/prepare.mjs",
|
||||||
"scripts/rea.mjs",
|
"scripts/rea.mjs",
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
"install.sh",
|
"install.sh",
|
||||||
"src/config/**",
|
"src/config/**",
|
||||||
"src/application/Setup*.ts",
|
"src/application/Setup*.ts",
|
||||||
@@ -377,6 +378,7 @@ export const SCOPES = {
|
|||||||
"src/contracts/toolContracts.ts",
|
"src/contracts/toolContracts.ts",
|
||||||
"src/contracts/toolEffects.ts",
|
"src/contracts/toolEffects.ts",
|
||||||
"scripts/rea.mjs",
|
"scripts/rea.mjs",
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
"src/process/**",
|
"src/process/**",
|
||||||
"src/artifacts/SafeOutputTree.ts",
|
"src/artifacts/SafeOutputTree.ts",
|
||||||
"scripts/verify/javascript/recovery.mjs",
|
"scripts/verify/javascript/recovery.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
/** Prepare the selected process environment before importing CLI or MCP dependencies. */
|
||||||
|
export const prepareCliProcessEnvironment = (
|
||||||
|
environment,
|
||||||
|
platform,
|
||||||
|
packageVersion,
|
||||||
|
) => {
|
||||||
|
environment.REA_PACKAGE_VERSION = packageVersion;
|
||||||
|
// Windows home lookup throws for an empty USERPROFILE. Dependencies resolve
|
||||||
|
// home during import, before REA's own resolver can apply its account fallback.
|
||||||
|
if (platform === "win32" && environment.USERPROFILE === "") {
|
||||||
|
delete environment.USERPROFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playwright's registry assumes a desktop host while choosing its cache.
|
||||||
|
// Termux needs an explicit path before the registry initializes.
|
||||||
|
if (
|
||||||
|
platform === "android" &&
|
||||||
|
environment.PLAYWRIGHT_BROWSERS_PATH === undefined
|
||||||
|
) {
|
||||||
|
const cacheRoot =
|
||||||
|
environment.XDG_CACHE_HOME ?? join(environment.HOME ?? ".", ".cache");
|
||||||
|
environment.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -45,6 +45,7 @@ if (await pathExists(join(root, "src"))) {
|
|||||||
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
|
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
|
||||||
} else {
|
} else {
|
||||||
for (const path of [
|
for (const path of [
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
"dist/main.js",
|
"dist/main.js",
|
||||||
"dist/mcp/main.js",
|
"dist/mcp/main.js",
|
||||||
"dist/cli-runtime/cli.js",
|
"dist/cli-runtime/cli.js",
|
||||||
|
|||||||
+8
-22
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { access } from "node:fs/promises";
|
import { access } from "node:fs/promises";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { prepareCliProcessEnvironment } from "./lib/cli-process-environment.mjs";
|
||||||
|
|
||||||
// Route production MCP before importing Incur. Incur owns registration helpers
|
// Route production MCP before importing Incur. Incur owns registration helpers
|
||||||
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
|
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
|
||||||
// or, with `--http`, over streamable HTTP.
|
// or, with `--http`, over streamable HTTP.
|
||||||
@@ -11,7 +13,11 @@ const args = process.argv.slice(2);
|
|||||||
const { default: packageJson } = await import("../package.json", {
|
const { default: packageJson } = await import("../package.json", {
|
||||||
with: { type: "json" },
|
with: { type: "json" },
|
||||||
});
|
});
|
||||||
process.env.REA_PACKAGE_VERSION = packageJson.version;
|
prepareCliProcessEnvironment(
|
||||||
|
process.env,
|
||||||
|
process.platform,
|
||||||
|
packageJson.version,
|
||||||
|
);
|
||||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const builtinArgumentRuntime = "dist/cli-runtime/cliBuiltinArguments.js";
|
const builtinArgumentRuntime = "dist/cli-runtime/cliBuiltinArguments.js";
|
||||||
const builtinArguments = (await compiledRuntimeExists([builtinArgumentRuntime]))
|
const builtinArguments = (await compiledRuntimeExists([builtinArgumentRuntime]))
|
||||||
@@ -36,26 +42,6 @@ const isMcpHttpMode =
|
|||||||
const SIGPIPE_EXIT_STATUS = 141;
|
const SIGPIPE_EXIT_STATUS = 141;
|
||||||
// Set once the reader of CLI output has closed the pipe.
|
// Set once the reader of CLI output has closed the pipe.
|
||||||
let stdoutClosed = false;
|
let stdoutClosed = false;
|
||||||
// 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 = [
|
const runtimeFiles = [
|
||||||
builtinArgumentRuntime,
|
builtinArgumentRuntime,
|
||||||
...(isMcpMode || isMcpHttpMode
|
...(isMcpMode || isMcpHttpMode
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export async function verifyPackagePack({ root, workspace }) {
|
|||||||
throw new Error("package did not expose both rea command entry points");
|
throw new Error("package did not expose both rea command entry points");
|
||||||
if (packedManifest.mcpName !== "io.github.morluto/rea")
|
if (packedManifest.mcpName !== "io.github.morluto/rea")
|
||||||
throw new Error("package did not retain its MCP Registry ownership marker");
|
throw new Error("package did not retain its MCP Registry ownership marker");
|
||||||
|
if (!packedFiles.includes("package/scripts/lib/cli-process-environment.mjs"))
|
||||||
|
throw new Error("package omitted the CLI process environment owner");
|
||||||
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
|
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"package included authored skill sources instead of only the generated bundle",
|
"package included authored skill sources instead of only the generated bundle",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const installCliRuntime = async (
|
|||||||
cliSource: string,
|
cliSource: string,
|
||||||
resultOutput = "export const createCliResultOutput = () => undefined;\n",
|
resultOutput = "export const createCliResultOutput = () => undefined;\n",
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await workspace.mkdir("scripts");
|
await workspace.mkdir("scripts/lib");
|
||||||
await workspace.mkdir("dist");
|
await workspace.mkdir("dist");
|
||||||
// The bundle's entrypoints and shared chunks form one emitted runtime.
|
// The bundle's entrypoints and shared chunks form one emitted runtime.
|
||||||
await cp("dist/cli-runtime", workspace.path("dist/cli-runtime"), {
|
await cp("dist/cli-runtime", workspace.path("dist/cli-runtime"), {
|
||||||
@@ -23,6 +23,10 @@ const installCliRuntime = async (
|
|||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
copyFile("scripts/rea.mjs", workspace.path("scripts/rea.mjs")),
|
copyFile("scripts/rea.mjs", workspace.path("scripts/rea.mjs")),
|
||||||
|
copyFile(
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
|
workspace.path("scripts/lib/cli-process-environment.mjs"),
|
||||||
|
),
|
||||||
workspace.write(
|
workspace.write(
|
||||||
"package.json",
|
"package.json",
|
||||||
JSON.stringify({ type: "module", version: "1.0.0" }),
|
JSON.stringify({ type: "module", version: "1.0.0" }),
|
||||||
@@ -289,8 +293,13 @@ describe("executable dispatcher", () => {
|
|||||||
"explains how to restore a missing compiled runtime",
|
"explains how to restore a missing compiled runtime",
|
||||||
async ({ processes, workspace }) => {
|
async ({ processes, workspace }) => {
|
||||||
const scripts = await workspace.mkdir("scripts");
|
const scripts = await workspace.mkdir("scripts");
|
||||||
|
await workspace.mkdir("scripts/lib");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")),
|
copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")),
|
||||||
|
copyFile(
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
|
workspace.path("scripts/lib/cli-process-environment.mjs"),
|
||||||
|
),
|
||||||
workspace.write(
|
workspace.write(
|
||||||
"package.json",
|
"package.json",
|
||||||
JSON.stringify({ type: "module", version: "1.0.0" }),
|
JSON.stringify({ type: "module", version: "1.0.0" }),
|
||||||
|
|||||||
@@ -1256,6 +1256,16 @@ it.each([
|
|||||||
package: false,
|
package: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"scripts/lib/cli-process-environment.mjs",
|
||||||
|
{
|
||||||
|
code: true,
|
||||||
|
package: true,
|
||||||
|
runtime: true,
|
||||||
|
javascript_recovery: true,
|
||||||
|
readiness: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"tests/conformance/readiness/report/reconstructionReadiness.ts",
|
"tests/conformance/readiness/report/reconstructionReadiness.ts",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Writable } from "node:stream";
|
import { Writable } from "node:stream";
|
||||||
import { access } from "node:fs/promises";
|
import { access } from "node:fs/promises";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { prepareCliProcessEnvironment } from "../../../scripts/lib/cli-process-environment.mjs";
|
||||||
import {
|
import {
|
||||||
createCliSessionFrameReader,
|
createCliSessionFrameReader,
|
||||||
encodeCliSessionFrame,
|
encodeCliSessionFrame,
|
||||||
@@ -19,23 +20,6 @@ const { default: packageJson } = await import(
|
|||||||
{ with: { type: "json" } }
|
{ 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 = [
|
const runtimeFiles = [
|
||||||
"dist/cli-runtime/cli.js",
|
"dist/cli-runtime/cli.js",
|
||||||
"dist/cli-runtime/cliOutput.js",
|
"dist/cli-runtime/cliOutput.js",
|
||||||
@@ -43,7 +27,11 @@ const runtimeFiles = [
|
|||||||
"dist/cli-runtime/cliResultOutput.js",
|
"dist/cli-runtime/cliResultOutput.js",
|
||||||
];
|
];
|
||||||
|
|
||||||
prepareCliProcess();
|
prepareCliProcessEnvironment(
|
||||||
|
process.env,
|
||||||
|
process.platform,
|
||||||
|
packageJson.version,
|
||||||
|
);
|
||||||
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||||
process.stderr.write(
|
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`,
|
`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`,
|
||||||
@@ -118,7 +106,11 @@ async function handle(value) {
|
|||||||
}
|
}
|
||||||
for (const key of Object.keys(process.env)) delete process.env[key];
|
for (const key of Object.keys(process.env)) delete process.env[key];
|
||||||
Object.assign(process.env, request.env);
|
Object.assign(process.env, request.env);
|
||||||
prepareCliProcess();
|
prepareCliProcessEnvironment(
|
||||||
|
process.env,
|
||||||
|
process.platform,
|
||||||
|
packageJson.version,
|
||||||
|
);
|
||||||
process.chdir(request.cwd);
|
process.chdir(request.cwd);
|
||||||
const result = await runOneShot(request.arguments);
|
const result = await runOneShot(request.arguments);
|
||||||
await writeFrame({
|
await writeFrame({
|
||||||
|
|||||||
Reference in new issue
Block a user