fix(cli): accept --flag=value for global output flags (#3811)

* fix(cli): accept --flag=value for global output flags

`rea <command> --format=json` failed with "Unknown flag: --format".
`--filter-output=...`, `--token-limit=...` and `--token-offset=...`
failed the same way. Incur reads these global flags only in spaced form,
so the equals form fell through to the command parser. Command options
already accept both forms, and `rea mcp doctor` parses `--format=` itself.

A valued output flag with no value (`--format` last, or `--format=`)
also reported the flag as unknown with code UNKNOWN.

normalizeCliOutputArguments, in the module that owns output-argument
interpretation, now splits the equals forms of the four valued output
flags. The entry point passes the normalized argv to rea's output
validation and to Incur, so both read the same effective values. A
missing value now fails with MISSING_OUTPUT_VALUE and names the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): detect missing output values while parsing output flags

The missing-value check scanned argv again after parsing, so it also
checked tokens the parser had already consumed as values. For example,
`--filter-output --format` selects the field `--format`, yet the second
scan reported `--format` as missing a value. parseCliOutputArguments
now records the first valued flag it skips for lack of a value, in the
same pass that consumes values.

Cover normalizeCliOutputArguments directly: values containing `=`, empty
values, and command options that only share a prefix. Export it in the
dispatcher test's runtime stub as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-14 20:22:02 -07:00
1 parent 847b7328cf
commit 0781716788
5 files changed
+137 -25

No files matched your search

+3 -1
View File
@@ -314,7 +314,9 @@ diagnostics appear inside the report when `--full-output` is selected. Log
truncation alone does not change the diagnostic health or exit status.
The default terminal format is TOON. Use `--json` when saving results for a
JSON consumer. `--json` is indented on a terminal and compact when piped or
JSON consumer. Valued output flags (`--format`, `--filter-output`,
`--token-limit`, `--token-offset`) accept `--flag value` or `--flag=value`; a
missing value fails with `MISSING_OUTPUT_VALUE` before the command runs. `--json` is indented on a terminal and compact when piped or
redirected, which keeps large results smaller for agents and scripts; pipe
through `jq .` for indented files. Output selection and formatting do not change
operation status.
+6 -4
View File
@@ -74,12 +74,14 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
const {
normalizeCliOutputArguments,
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
validateCliOutputArguments,
} = await import("../dist/cli-runtime/cliOutput.js");
guardClosedStdout();
const outputArguments = validateCliOutputArguments(args);
const cliArgs = normalizeCliOutputArguments(args);
const outputArguments = validateCliOutputArguments(cliArgs);
const configurationError = cliConfigurationError(process.env);
if (configurationError !== undefined) {
process.stderr.write(`${configurationError}\n`);
@@ -89,8 +91,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
process.exitCode = 1;
} else {
let wroteOutput = false;
const resultOutput = createCliResultOutput(args, process.stdout);
await createCli(process.env, resultOutput).serve(args, {
const resultOutput = createCliResultOutput(cliArgs, process.stdout);
await createCli(process.env, resultOutput).serve(cliArgs, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed && !stdoutClosed)
@@ -104,7 +106,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
exit: (code) => process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : code),
});
if (!wroteOutput && !resultOutput?.handled)
writeStdout(renderEmptyFilteredCliOutput(args) ?? "");
writeStdout(renderEmptyFilteredCliOutput(cliArgs) ?? "");
}
}
+47 -7
View File
@@ -3,13 +3,40 @@ const UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE =
type StructuredOutputFormat = "json" | "jsonl" | "yaml";
const INCUR_OUTPUT_FORMATS = new Set(["toon", "json", "yaml", "md", "jsonl"]);
/** Global output flags that take a value. */
const VALUED_OUTPUT_FLAGS = new Set([
"--format",
"--filter-output",
"--token-limit",
"--token-offset",
]);
/**
* Split `--flag=value` forms of the valued output flags. Incur reads these
* flags only in spaced form, so `--format=json` otherwise reaches the command
* parser and fails as an unknown flag. Command options accept both forms.
*/
export const normalizeCliOutputArguments = (
arguments_: readonly string[],
): string[] =>
arguments_.flatMap((argument) => {
const separator = argument.indexOf("=");
if (separator < 0) return [argument];
const flag = argument.slice(0, separator);
return VALUED_OUTPUT_FLAGS.has(flag)
? [flag, argument.slice(separator + 1)]
: [argument];
});
export type CliOutputArgumentValidation =
| { readonly ok: true }
| {
readonly ok: false;
readonly format: StructuredOutputFormat;
readonly code: "UNSUPPORTED_OUTPUT_COMBINATION" | "INVALID_OUTPUT_FILTER";
readonly code:
| "UNSUPPORTED_OUTPUT_COMBINATION"
| "INVALID_OUTPUT_FILTER"
| "MISSING_OUTPUT_VALUE";
readonly message: string;
};
@@ -17,6 +44,8 @@ interface ParsedCliOutputArguments {
readonly filterOutput: string | undefined;
readonly format: string;
readonly fullOutput: boolean;
/** First valued output flag that Incur skips because it has no value. */
readonly missingValue: string | undefined;
readonly parseError: boolean;
readonly tokenCount: boolean;
readonly tokenWindow: boolean;
@@ -31,6 +60,7 @@ export const parseCliOutputArguments = (
let format: string = "toon";
let filterOutput: string | undefined;
let fullOutput = false;
let missingValue: string | undefined;
let tokenCount = false;
let tokenLimit: number | undefined;
let tokenOffset: number | undefined;
@@ -38,6 +68,7 @@ export const parseCliOutputArguments = (
filterOutput,
format,
fullOutput,
missingValue,
parseError,
tokenCount,
tokenWindow:
@@ -47,8 +78,8 @@ export const parseCliOutputArguments = (
});
for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index];
// Incur scans builtins across `--` and accepts only spaced value forms.
// Mirror those effective values at the executable output boundary.
// Incur scans builtins across `--` and reads spaced value forms, which
// normalizeCliOutputArguments has produced. Mirror those effective values.
if (argument === "--json") {
format = "json";
continue;
@@ -61,14 +92,14 @@ export const parseCliOutputArguments = (
if (!INCUR_OUTPUT_FORMATS.has(value)) return parsed(true);
format = value;
index += 1;
}
} else missingValue ??= argument;
continue;
}
if (argument === "--filter-output") {
if (arguments_[index + 1]) {
filterOutput = arguments_[index + 1];
index += 1;
}
} else missingValue ??= argument;
continue;
}
if (argument === "--token-limit" || argument === "--token-offset") {
@@ -80,7 +111,7 @@ export const parseCliOutputArguments = (
if (argument === "--token-limit") tokenLimit = numericValue;
else tokenOffset = numericValue;
index += 1;
}
} else missingValue ??= argument;
continue;
}
if (argument === "--full-output") fullOutput = true;
@@ -111,7 +142,7 @@ const hasUnclosedFilterSlice = (expression: string): boolean => {
export const validateCliOutputArguments = (
arguments_: readonly string[],
): CliOutputArgumentValidation => {
const { filterOutput, format, parseError, tokenWindow } =
const { filterOutput, format, missingValue, parseError, tokenWindow } =
parseCliOutputArguments(arguments_);
if (parseError) return { ok: true };
if (filterOutput !== undefined && hasUnclosedFilterSlice(filterOutput))
@@ -132,6 +163,15 @@ export const validateCliOutputArguments = (
code: "UNSUPPORTED_OUTPUT_COMBINATION",
message: UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE,
};
// Incur skips a valued flag without a value and then reports the flag
// itself as unknown. Name the missing value instead.
if (missingValue !== undefined)
return {
ok: false,
format: format === "yaml" || format === "jsonl" ? format : "json",
code: "MISSING_OUTPUT_VALUE",
message: `${missingValue} requires a value, as in ${missingValue} <value> or ${missingValue}=<value>.`,
};
return { ok: true };
};
+80 -12
View File
@@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest";
import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js";
import {
normalizeCliOutputArguments,
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
validateCliOutputArguments,
@@ -58,6 +59,50 @@ describe("CLI output argument boundary", () => {
});
});
describe("CLI output flag value forms", () => {
it("splits only the equals forms of valued output flags", () => {
expect(
normalizeCliOutputArguments([
"inspect-sqlite-database",
"--table=a=b",
"--format=json",
"--filter-output=a.b,c[0,1]",
"--token-limit=5",
"--token-offset=",
"--json",
"--format-hint=x",
]),
).toEqual([
"inspect-sqlite-database",
"--table=a=b",
"--format",
"json",
"--filter-output",
"a.b,c[0,1]",
"--token-limit",
"5",
"--token-offset",
"",
"--json",
"--format-hint=x",
]);
});
it("reports a missing value only for a flag the parser did not consume", () => {
expect(
validateCliOutputArguments([
"capabilities",
"--json",
"--filter-output",
"--format",
]),
).toEqual({ ok: true });
expect(
validateCliOutputArguments(["capabilities", "--json", "--format"]),
).toMatchObject({ ok: false, code: "MISSING_OUTPUT_VALUE" });
});
});
describe("unclosed CLI output filter slices", () => {
it.each([
"providers[",
@@ -153,7 +198,7 @@ describe("CLI output valued-flag parsing", () => {
async ({ cli }) => {
for (const arguments_ of [
["providers", "--", "--json", "--token-limit", "5"],
["providers", "--json", "--format=toon", "--token-limit", "5"],
["providers", "--format=json", "--token-limit=5"],
]) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
@@ -183,22 +228,45 @@ describe("CLI output valued-flag parsing", () => {
);
workspaceCliTest(
"does not treat empty or equals-style output values as active builtins",
"names missing output values and honors equals-style output values",
async ({ cli }) => {
for (const arguments_ of [
["capabilities", "--json", "--format", ""],
["capabilities", "--json", "--filter-output", ""],
["capabilities", "--json", "--token-limit", ""],
["capabilities", "--json", "--token-limit=5"],
]) {
for (const [arguments_, flag] of [
[["capabilities", "--json", "--format", ""], "--format"],
[["capabilities", "--json", "--filter-output", ""], "--filter-output"],
[["capabilities", "--json", "--token-limit", ""], "--token-limit"],
[["capabilities", "--json", "--token-offset="], "--token-offset"],
[["capabilities", "--format"], "--format"],
] as const) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
expect(result.json).toBeDefined();
expect(result.stdout).not.toContain("[truncated:");
expect(JSON.stringify(result.json)).not.toContain(
"UNSUPPORTED_OUTPUT_COMBINATION",
);
expect(result.json).toMatchObject({
ok: false,
error: {
code: "MISSING_OUTPUT_VALUE",
message: expect.stringContaining(`${flag} requires a value`),
},
});
}
const equalsForms = await cli.run({
arguments: [
"capabilities",
"--format=json",
"--filter-output=capabilities[0,1]",
],
});
expect(equalsForms.exitCode).toBe(0);
expect(equalsForms.json).toMatchObject({
capabilities: [expect.any(Object)],
});
const equalsTokenWindow = await cli.run({
arguments: ["capabilities", "--json", "--token-limit=5"],
});
expect(equalsTokenWindow.exitCode).toBe(1);
expect(equalsTokenWindow.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
const validTokenWindow = await cli.run({
arguments: ["capabilities", "--json", "--token-limit", "5"],
});
+1 -1
View File
@@ -29,7 +29,7 @@ describe("executable dispatcher", () => {
),
workspace.write(
"dist/cli-runtime/cliOutput.js",
"export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n",
"export const normalizeCliOutputArguments = (args) => args; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n",
),
workspace.write(
"dist/cli-runtime/cliResultOutput.js",