fix(cli): report an invalid REA_LOG_LEVEL as configuration (#2140)

createCli parsed REA_LOG_LEVEL with a throwing schema, so an invalid or
uppercase value crashed every command, including doctor and --help, with
a ZodError stack trace. Parse it into a ConfigurationError result and have
the executable print the same configuration message as the MCP adapter
and exit 1 before building the CLI.

Fixes #2135

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-12 06:07:37 -07:00 committed by GitHub
parent 4f46eac453
commit 3159732597
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 64 additions and 8 deletions

View File

@ -58,7 +58,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
process.stdout.write(result.output);
process.exitCode = result.exitCode;
} else {
const { createCli } = await import("../dist/cli-runtime/cli.js");
const { cliConfigurationError, createCli } =
await import("../dist/cli-runtime/cli.js");
const { createStreamedCliJsonOutput } =
await import("../dist/cli-runtime/streamedJsonOutput.js");
const {
@ -67,7 +68,11 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
validateCliOutputArguments,
} = await import("../dist/cli-runtime/cliOutput.js");
const outputArguments = validateCliOutputArguments(args);
if (!outputArguments.ok) {
const configurationError = cliConfigurationError(process.env);
if (configurationError !== undefined) {
process.stderr.write(`${configurationError}\n`);
process.exitCode = 1;
} else if (!outputArguments.ok) {
process.stdout.write(renderCliOutputArgumentError(outputArguments));
process.exitCode = 1;
} else {

View File

@ -8,6 +8,7 @@ import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsComman
import { Cli } from "incur";
import { createLogger, parseLogLevel } from "./logger.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
import { PRODUCT_IDENTITY } from "./identity.js";
import { npxRegistrationCommand } from "./application/ClientRegistrationIdentity.js";
import { registerSetupCommands } from "./cli/setupCommands.js";
@ -41,6 +42,16 @@ import {
type CliResultOutput,
} from "./cli/streamedJsonOutput.js";
/** Describe CLI settings that are invalid before any command runs, as MCP does. */
export const cliConfigurationError = (
selectedEnvironment: Readonly<Record<string, string | undefined>>,
): string | undefined => {
const value = selectedEnvironment.REA_LOG_LEVEL;
if (value === undefined) return undefined;
const level = parseLogLevel(value);
return level.ok ? undefined : projectAnalysisError(level.error).message;
};
/**
* Build the one-shot Incur CLI without starting Hopper at import time.
* Analysis commands acquire and close their own sessions; bare `mcp` and
@ -54,11 +65,14 @@ export const createCli = (
): CliInstance => {
const environment = snapshotEnvironment(selectedEnvironment);
const analysis = createDirectAnalysis(environment);
// The executable reports an invalid level through cliConfigurationError.
const level =
environment.REA_LOG_LEVEL === undefined
? undefined
: parseLogLevel(environment.REA_LOG_LEVEL);
const logger = createLogger(
"cli",
environment.REA_LOG_LEVEL === undefined
? "silent"
: parseLogLevel(environment.REA_LOG_LEVEL),
level?.ok === true ? level.value : "silent",
);
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
version: PRODUCT_IDENTITY.packageVersion,

View File

@ -1,6 +1,9 @@
import pino, { type Logger } from "pino";
import { z } from "zod";
import { ConfigurationError } from "./domain/configurationErrors.js";
import { err, ok, type Result } from "./domain/result.js";
/** Supported structured log levels at the process boundary. */
export type LogLevel = z.infer<typeof logLevelSchema>;
@ -9,8 +12,21 @@ const logLevelSchema = z
.default("info");
/** Parse the optional REA_LOG_LEVEL environment variable. */
export const parseLogLevel = (value: unknown): LogLevel =>
logLevelSchema.parse(value);
export const parseLogLevel = (
value: unknown,
): Result<LogLevel, ConfigurationError> => {
const parsed = logLevelSchema.safeParse(value);
if (parsed.success) return ok(parsed.data);
return err(
new ConfigurationError("Invalid REA environment configuration", {
cause: parsed.error,
settings: parsed.error.issues.map(({ message }) => ({
setting: "REA_LOG_LEVEL",
constraint: message,
})),
}),
);
};
/** Create a JSON logger on stderr so protocol and CLI result stdout stay valid. */
export const createLogger = (mode: "mcp" | "cli", level: LogLevel): Logger =>

View File

@ -0,0 +1,21 @@
import { expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
for (const level of ["loud", "INFO"])
cliTest(
`reports REA_LOG_LEVEL=${level} as invalid configuration`,
async ({ cli }) => {
for (const arguments_ of [["doctor", "--json"], ["--help"]]) {
const result = await cli.run({
arguments: arguments_,
environment: { REA_LOG_LEVEL: level },
});
expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toBe(
'REA configuration is invalid: REA_LOG_LEVEL: Invalid option: expected one of "trace"|"debug"|"info"|"warn"|"error"|"fatal"|"silent".\n',
);
}
},
);

View File

@ -25,7 +25,7 @@ describe("executable dispatcher", () => {
),
workspace.write(
"dist/cli-runtime/cli.js",
'export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n',
'export const cliConfigurationError = () => undefined; export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n',
),
workspace.write(
"dist/cli-runtime/cliOutput.js",