diff --git a/scripts/rea.mjs b/scripts/rea.mjs index 578d29d62b..30d9c23e27 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -87,15 +87,16 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { renderCliOutputArgumentError, renderEmptyFilteredCliOutput, renderVersionReport, - validateCliOutputArguments, + parseCliOutputArguments, } = await import("../dist/cli-runtime/cliOutput.js"); guardClosedStdout(); const cliArgs = normalizeCliArguments(args); - const outputArguments = validateCliOutputArguments(cliArgs); + const outputOptions = parseCliOutputArguments(cliArgs); + const outputArguments = outputOptions.validation; // `--version` with an explicit format reports the runtime as a document; // plain `--version` stays Incur's bare version string. const versionOutput = renderVersionReport( - cliArgs, + outputOptions, { packageName: packageJson.name, packageVersion: packageJson.version, @@ -129,7 +130,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { const { createCliResultOutput } = await import("../dist/cli-runtime/cliResultOutput.js"); let wroteOutput = false; - const resultOutput = createCliResultOutput(cliArgs, process.stdout); + const resultOutput = createCliResultOutput(outputOptions, process.stdout); try { await createCli(process.env, resultOutput).serve(cliArgs, { stdout: (output) => { @@ -182,7 +183,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1; } if (!wroteOutput && !resultOutput?.handled) - writeStdout(renderEmptyFilteredCliOutput(cliArgs) ?? ""); + writeStdout(renderEmptyFilteredCliOutput(outputOptions) ?? ""); } } catch (cause) { dispatchFailure = cause; diff --git a/scripts/verify/javascript/output.mjs b/scripts/verify/javascript/output.mjs index e9bc9e5883..1191f9a758 100644 --- a/scripts/verify/javascript/output.mjs +++ b/scripts/verify/javascript/output.mjs @@ -10,6 +10,7 @@ import { finished } from "node:stream/promises"; import { Cli } from "incur"; +import { parseCliOutputArguments } from "../../../dist/cliOutput.js"; import { createCliResultOutput } from "../../../dist/cli/cliResultOutput.js"; import { createEvidence } from "../../../dist/domain/evidence.js"; @@ -79,7 +80,10 @@ try { destination = createWriteStream(path, { flags: "wx", mode: 0o600 }); const argv = [metadata.command, "--format", format, "--full-output"]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); assert.ok(output); let unexpectedOutputBytes = 0; let exitCode = 0; diff --git a/src/cli/cliResultOutput.ts b/src/cli/cliResultOutput.ts index 26541a4fdb..9f7d25471c 100644 --- a/src/cli/cliResultOutput.ts +++ b/src/cli/cliResultOutput.ts @@ -4,10 +4,7 @@ import type { Writable } from "node:stream"; import { Filter } from "incur"; import { estimateTokenCount } from "tokenx"; -import { - parseCliOutputArguments, - validateCliOutputArguments, -} from "../cliOutput.js"; +import type { ParsedCliOutputArguments } from "../cliOutput.js"; import { cliOperationFailureIdentity, cliOperationFailurePayload, @@ -103,12 +100,10 @@ const wrapCommand = ( /** Preserve Incur output controls and stream JSON while framing failures. */ export const createCliResultOutput = ( - arguments_: readonly string[], + options: ParsedCliOutputArguments, destination: Writable, ): CliResultOutput | undefined => { - const options = parseCliOutputArguments(arguments_); - if (options.parseError || !validateCliOutputArguments(arguments_).ok) - return undefined; + if (options.parseError || !options.validation.ok) return undefined; const format = options.format as "json" | "jsonl" | "yaml" | "toon" | "md"; let handled = false; let failed = false; diff --git a/src/cliOutput.ts b/src/cliOutput.ts index 11a5523f6f..a8f32cbfdc 100644 --- a/src/cliOutput.ts +++ b/src/cliOutput.ts @@ -178,7 +178,7 @@ export type CliOutputArgumentValidation = readonly message: string; }; -interface ParsedCliOutputArguments { +interface CliOutputControls { readonly filterOutput: string | undefined; readonly format: string; /** Whether `--json` or `--format` selected the format, rather than the default. */ @@ -193,11 +193,19 @@ interface ParsedCliOutputArguments { readonly tokenOffset: number | undefined; } +/** Parsed output controls and the decisions shared by every executable consumer. */ +export interface ParsedCliOutputArguments extends CliOutputControls { + readonly validation: CliOutputArgumentValidation; + readonly versionReportFormat: CliOutputFormat | undefined; +} + /** Read effective Incur output controls at the executable boundary. */ export const parseCliOutputArguments = ( arguments_: readonly string[], ): ParsedCliOutputArguments => { let format: string = "toon"; + let rootVersion = false; + let versionEligible = true; let formatExplicit = false; let filterOutput: string | undefined; let fullOutput = false; @@ -205,21 +213,44 @@ export const parseCliOutputArguments = ( let tokenCount = false; let tokenLimit: number | undefined; let tokenOffset: number | undefined; - const parsed = (parseError: boolean): ParsedCliOutputArguments => ({ - filterOutput, - format, - formatExplicit, - fullOutput, - missingValue, - parseError, - tokenCount, - tokenWindow: - !parseError && (tokenLimit !== undefined || tokenOffset !== undefined), - tokenLimit: parseError ? undefined : tokenLimit, - tokenOffset: parseError ? undefined : tokenOffset, - }); + const parsed = (parseError: boolean): ParsedCliOutputArguments => { + const controls: CliOutputControls = { + filterOutput, + format, + formatExplicit, + fullOutput, + missingValue, + parseError, + tokenCount, + tokenWindow: + !parseError && (tokenLimit !== undefined || tokenOffset !== undefined), + tokenLimit: parseError ? undefined : tokenLimit, + tokenOffset: parseError ? undefined : tokenOffset, + }; + return { + ...controls, + validation: validateCliOutputControls(controls), + versionReportFormat: + rootVersion && + versionEligible && + formatExplicit && + !parseError && + missingValue === undefined && + isCliOutputFormat(format) + ? format + : undefined, + }; + }; for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; + // Only these flags can accompany a formatted root version request. + // Valued output flags consume their data below before the next iteration. + if (argument === "--version") { + const next = arguments_[index + 1]; + if (next === undefined || next.startsWith("-")) rootVersion = true; + else versionEligible = false; + } else if (argument !== "--json" && argument !== "--format") + versionEligible = false; // Incur scans builtins across `--` and reads spaced value forms, which // normalizeCliArguments has produced. Mirror those effective values. if (argument === "--json") { @@ -288,11 +319,13 @@ export const missingCliOutputValueMessage = (flag: string): string => `${flag} requires a value, as in ${flag} or ${flag}=.`; /** Reject output controls that cannot safely produce a complete result. */ -export const validateCliOutputArguments = ( - arguments_: readonly string[], -): CliOutputArgumentValidation => { - const { filterOutput, format, missingValue, parseError, tokenWindow } = - parseCliOutputArguments(arguments_); +const validateCliOutputControls = ({ + filterOutput, + format, + missingValue, + parseError, + tokenWindow, +}: CliOutputControls): CliOutputArgumentValidation => { if (parseError) return { ok: true }; if (filterOutput !== undefined && hasUnclosedFilterSlice(filterOutput)) return { @@ -325,11 +358,11 @@ export const validateCliOutputArguments = ( }; /** Preserve a complete document when Incur filters the entire result away. */ -export const renderEmptyFilteredCliOutput = ( - arguments_: readonly string[], -): string | undefined => { - const { filterOutput, format, parseError } = - parseCliOutputArguments(arguments_); +export const renderEmptyFilteredCliOutput = ({ + filterOutput, + format, + parseError, +}: ParsedCliOutputArguments): string | undefined => { if (parseError) return undefined; if ( filterOutput && @@ -352,48 +385,15 @@ export const renderCliOutputArgumentError = ( return `${JSON.stringify(value)}\n`; }; -/** - * The format of a root `--version` request that selects one explicitly, or - * undefined when Incur should answer it. Only `--version`, `--json` and - * `--format ` may appear, so help keeps precedence, a command keeps - * its own `--version` option, and other output controls keep Incur's plain - * version. Plain `--version` stays the bare version string. - */ -export const versionReportFormat = ( - arguments_: readonly string[], -): CliOutputFormat | undefined => { - let version = false; - for (let index = 0; index < arguments_.length; index += 1) { - const argument = arguments_[index]; - if (argument === "--version") { - const next = arguments_[index + 1]; - // A following value belongs to a command-local `--version` option. - if (next !== undefined && !next.startsWith("-")) return undefined; - version = true; - } else if (argument === "--format") index += 1; - else if (argument !== "--json") return undefined; - } - if (!version) return undefined; - const { format, formatExplicit, missingValue, parseError } = - parseCliOutputArguments(arguments_); - return formatExplicit && - !parseError && - missingValue === undefined && - isCliOutputFormat(format) - ? format - : undefined; -}; - /** * Render the version report for a formatted root `--version` request, or * return undefined so Incur prints the bare version or handles the request. */ export const renderVersionReport = ( - arguments_: readonly string[], + { versionReportFormat: format }: ParsedCliOutputArguments, host: VersionReportHost, stdoutIsTerminal: boolean, ): string | undefined => { - const format = versionReportFormat(arguments_); return format === undefined ? undefined : renderCliDocument(versionReport(host), format, stdoutIsTerminal); diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index bbf5aadabb..d6473e3b6f 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -12,8 +12,7 @@ import { renderCliDocument, renderEmptyFilteredCliOutput, renderVersionReport, - validateCliOutputArguments, - versionReportFormat, + parseCliOutputArguments, } from "../../../src/cliOutput.js"; const CLI_INTEGRATION_TIMEOUT_MS = 60_000; @@ -29,13 +28,13 @@ describe("CLI output argument boundary", () => { it("rejects token windows that would corrupt structured output", () => { for (const format of ["json", "jsonl", "yaml"] as const) { - const validation = validateCliOutputArguments([ + const validation = parseCliOutputArguments([ "providers", "--token-limit", "5", "--format", format, - ]); + ]).validation; expect(validation).toMatchObject({ ok: false, format, @@ -56,16 +55,17 @@ describe("CLI output argument boundary", () => { } } expect( - validateCliOutputArguments([ + parseCliOutputArguments([ "providers", "--token-limit", "5", "--format", "toon", - ]), + ]).validation, ).toEqual({ ok: true }); expect( - validateCliOutputArguments(["providers", "--token-count", "--json"]), + parseCliOutputArguments(["providers", "--token-count", "--json"]) + .validation, ).toEqual({ ok: true }); }); }); @@ -129,15 +129,16 @@ describe("CLI output flag value forms", () => { it("reports a missing value only for a flag the parser did not consume", () => { expect( - validateCliOutputArguments([ + parseCliOutputArguments([ "capabilities", "--json", "--filter-output", "--format", - ]), + ]).validation, ).toEqual({ ok: true }); expect( - validateCliOutputArguments(["capabilities", "--json", "--format"]), + parseCliOutputArguments(["capabilities", "--json", "--format"]) + .validation, ).toMatchObject({ ok: false, code: "MISSING_OUTPUT_VALUE" }); }); }); @@ -151,12 +152,12 @@ describe("unclosed CLI output filter slices", () => { "providers],name[0,1]", ])("rejects %s before the filter parser runs", (filter) => { expect( - validateCliOutputArguments([ + parseCliOutputArguments([ "providers", "--json", "--filter-output", filter, - ]), + ]).validation, ).toMatchObject({ ok: false, code: "INVALID_OUTPUT_FILTER" }); }); @@ -173,7 +174,9 @@ describe("unclosed CLI output filter slices", () => { ["--format", "invalid", "--filter-output", "providers["], ["--token-limit", "invalid", "--filter-output", "providers["], ]) - expect(validateCliOutputArguments(arguments_)).toEqual({ ok: true }); + expect(parseCliOutputArguments(arguments_).validation).toEqual({ + ok: true, + }); }); workspaceCliTest( @@ -356,19 +359,25 @@ describe("CLI output argument boundary", () => { it("renders an explicit empty projection for structured filtered output", () => { for (const format of ["json", "jsonl", "yaml"]) expect( - renderEmptyFilteredCliOutput([ - "providers", - "--format", - format, - "--filter-output", - "missing", - ]), + renderEmptyFilteredCliOutput( + parseCliOutputArguments([ + "providers", + "--format", + format, + "--filter-output", + "missing", + ]), + ), ).toBe("{}\n"); expect( - renderEmptyFilteredCliOutput(["providers", "--filter-output", "missing"]), + renderEmptyFilteredCliOutput( + parseCliOutputArguments(["providers", "--filter-output", "missing"]), + ), ).toBeUndefined(); expect( - renderEmptyFilteredCliOutput(["providers", "--format", "json"]), + renderEmptyFilteredCliOutput( + parseCliOutputArguments(["providers", "--format", "json"]), + ), ).toBeUndefined(); }); }); @@ -626,7 +635,9 @@ describe("version report requests", () => { [["--version", "--format"], undefined], [["--json"], undefined], ] as const)("reads %j as format %s", (arguments_, expected) => { - expect(versionReportFormat(arguments_)).toBe(expected); + expect(parseCliOutputArguments(arguments_).versionReportFormat).toBe( + expected, + ); }); }); @@ -640,8 +651,16 @@ describe("version report rendering", () => { }; it("indents JSON for a terminal and compacts it when piped", () => { - const terminal = renderVersionReport(["--version", "--json"], host, true); - const piped = renderVersionReport(["--version", "--json"], host, false); + const terminal = renderVersionReport( + parseCliOutputArguments(["--version", "--json"]), + host, + true, + ); + const piped = renderVersionReport( + parseCliOutputArguments(["--version", "--json"]), + host, + false, + ); expect(terminal?.split("\n").length).toBeGreaterThan(2); expect(piped?.trimEnd().split("\n")).toHaveLength(1); @@ -649,6 +668,8 @@ describe("version report rendering", () => { }); it("leaves plain --version to Incur", () => { - expect(renderVersionReport(["--version"], host, true)).toBeUndefined(); + expect( + renderVersionReport(parseCliOutputArguments(["--version"]), host, true), + ).toBeUndefined(); }); }); diff --git a/tests/boundary/cli/cliTokenWindowOutput.test.ts b/tests/boundary/cli/cliTokenWindowOutput.test.ts index 48e16bcafe..09a9a9be38 100644 --- a/tests/boundary/cli/cliTokenWindowOutput.test.ts +++ b/tests/boundary/cli/cliTokenWindowOutput.test.ts @@ -4,6 +4,7 @@ import { Cli, Formatter } from "incur"; import { estimateTokenCount } from "tokenx"; import { describe, expect, it } from "vitest"; +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; import { createCliResultOutput, streamCliCommandResults, @@ -34,7 +35,10 @@ const renderWindow = async ( "--token-offset", String(offset), ]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing TOON output surface"); const handled = await output.write(value, { command: "fixture", @@ -191,7 +195,12 @@ describe("token windows through Incur output policies", () => { }, }); const output = createCliResultOutput( - ["fixture", "--filter-output", "missing", ...windowFlags], + parseCliOutputArguments([ + "fixture", + "--filter-output", + "missing", + ...windowFlags, + ]), destination, )!; const handled = await output.write( @@ -249,7 +258,10 @@ describe("token windows through Incur output policies", () => { callback(); }, }); - const output = createCliResultOutput(argv, destination)!; + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + )!; const cli = Cli.create("policy-test", { sync: false }); if (grouped) { const group = Cli.create("group", { diff --git a/tests/boundary/cli/dispatcher.test.ts b/tests/boundary/cli/dispatcher.test.ts index 5e67cc37f6..d1a47feaea 100644 --- a/tests/boundary/cli/dispatcher.test.ts +++ b/tests/boundary/cli/dispatcher.test.ts @@ -8,7 +8,7 @@ import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js"; import type { TestWorkspace } from "../../support/workspace/workspaceFixture.js"; const CLI_OUTPUT_RUNTIME = - "export const cliConfigurationError = () => undefined; export const normalizeCliArguments = (args) => args; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined; export const renderVersionReport = () => undefined;\n"; + "export const cliConfigurationError = () => undefined; export const normalizeCliArguments = (args) => args; export const parseCliOutputArguments = () => ({ validation: { ok: true } }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined; export const renderVersionReport = () => undefined;\n"; const installCliRuntime = async ( workspace: TestWorkspace, diff --git a/tests/boundary/cli/filteredArrayOutput.test.ts b/tests/boundary/cli/filteredArrayOutput.test.ts index c9ae33ffa2..5de47b4142 100644 --- a/tests/boundary/cli/filteredArrayOutput.test.ts +++ b/tests/boundary/cli/filteredArrayOutput.test.ts @@ -4,6 +4,7 @@ import { Cli, Formatter } from "incur"; import { estimateTokenCount } from "tokenx"; import { expect, it } from "vitest"; +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; import { createCliResultOutput, streamCliCommandResults, @@ -28,7 +29,7 @@ const runFilteredCli = async ( const args = ["rows", "--format", format, "--filter-output", filter]; const chunks: Buffer[] = []; const output = createCliResultOutput( - args, + parseCliOutputArguments(args), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -75,7 +76,7 @@ it.each( ]; const chunks: Buffer[] = []; const output = createCliResultOutput( - args, + parseCliOutputArguments(args), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -128,7 +129,7 @@ it.each(["json", "jsonl"] as const)( ]; const chunks: Buffer[] = []; const output = createCliResultOutput( - args, + parseCliOutputArguments(args), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -161,7 +162,7 @@ it.each(["selected", "toString", "constructor"])( const before = structuredClone(value); const chunks: Buffer[] = []; const output = createCliResultOutput( - ["--json", "--filter-output", filter], + parseCliOutputArguments(["--json", "--filter-output", filter]), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -187,7 +188,12 @@ it("finishes JSONL after a bounded large record before a missing selection", asy const value = [{ selected: "x".repeat(80 * 1024) }, {}]; const chunks: Buffer[] = []; const output = createCliResultOutput( - ["--format", "jsonl", "--filter-output", "selected"], + parseCliOutputArguments([ + "--format", + "jsonl", + "--filter-output", + "selected", + ]), new Writable({ highWaterMark: 1, write(chunk: Buffer, _encoding, callback) { diff --git a/tests/boundary/cli/filteredFailureOutput.test.ts b/tests/boundary/cli/filteredFailureOutput.test.ts index 35988e676a..33724d6713 100644 --- a/tests/boundary/cli/filteredFailureOutput.test.ts +++ b/tests/boundary/cli/filteredFailureOutput.test.ts @@ -5,6 +5,7 @@ import { Cli, z } from "incur"; import { parse as parseYaml } from "yaml"; import { expect, it } from "vitest"; +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; import { createCliResultOutput, streamCliCommandResults, @@ -21,7 +22,10 @@ const runCli = async (arguments_: readonly string[]) => { callback(); }, }); - const output = createCliResultOutput(arguments_, destination); + const output = createCliResultOutput( + parseCliOutputArguments(arguments_), + destination, + ); if (output === undefined) throw new Error("Missing CLI result output owner"); const cli = Cli.create("filtered-failure-test", { sync: false }); cli.command("trace-application-feature", { @@ -178,7 +182,10 @@ it("keeps successful scalar filtering available", async () => { }, }); const argv = ["open-control", "--json", "--filter-output", "open"]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing CLI result output owner"); const cli = Cli.create("filtered-failure-test", { sync: false }); cli.command("open-control", { run: () => ({ open: false }) }); diff --git a/tests/boundary/cli/formattingRecovery.test.ts b/tests/boundary/cli/formattingRecovery.test.ts index 698f931a70..dc4f6ad4f8 100644 --- a/tests/boundary/cli/formattingRecovery.test.ts +++ b/tests/boundary/cli/formattingRecovery.test.ts @@ -3,6 +3,7 @@ import { Writable } from "node:stream"; import { Cli } from "incur"; import { describe, expect, it } from "vitest"; +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; import { createCliResultOutput, streamCliCommandResults, @@ -29,7 +30,10 @@ describe("CLI formatting stage admission", () => { { cta: { commands: [{ command: "next" }] } }, ), }); - const output = createCliResultOutput(["fixture"], new Writable()); + const output = createCliResultOutput( + parseCliOutputArguments(["fixture"]), + new Writable(), + ); if (output === undefined) throw new Error("Missing output owner"); if (wrapped) streamCliCommandResults(cli, output); let reply = ""; @@ -75,7 +79,10 @@ describe("CLI formatting stage admission", () => { }); it("declines errors before a result or from a different failure class", async () => { - const output = createCliResultOutput([], new Writable()); + const output = createCliResultOutput( + parseCliOutputArguments([]), + new Writable(), + ); expect(output).toBeDefined(); expect( await output?.recoverFormattingFailure( @@ -99,7 +106,10 @@ describe("CLI formatting stage admission", () => { throw new RangeError("Invalid string length"); }, }); - const output = createCliResultOutput([], new Writable()); + const output = createCliResultOutput( + parseCliOutputArguments([]), + new Writable(), + ); if (output === undefined) throw new Error("Missing output owner"); streamCliCommandResults(cli, output); let text = ""; @@ -124,7 +134,7 @@ describe("CLI formatting recovery outcomes", () => { const cause = new RangeError("Invalid string length"); const chunks: Buffer[] = []; const output = createCliResultOutput( - ["--format", "md"], + parseCliOutputArguments(["--format", "md"]), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -154,7 +164,7 @@ describe("CLI formatting recovery outcomes", () => { it("retains ownership if the recovery document cannot be written", async () => { const cause = new RangeError("Invalid string length"); const output = createCliResultOutput( - [], + parseCliOutputArguments([]), new Writable({ write(_chunk, _encoding, callback) { callback(cause); @@ -171,7 +181,7 @@ describe("CLI formatting recovery outcomes", () => { it("does not replace a failed destination, even for the same RangeError", async () => { const cause = new RangeError("Invalid string length"); const output = createCliResultOutput( - ["--json"], + parseCliOutputArguments(["--json"]), new Writable({ write(_chunk, _encoding, callback) { callback(cause); @@ -189,7 +199,7 @@ describe("CLI formatting recovery outcomes", () => { it("retains the original failure identity and ignores large details on recovery", async () => { const chunks: Buffer[] = []; const output = createCliResultOutput( - [], + parseCliOutputArguments([]), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); diff --git a/tests/boundary/cli/jsonOutput.test.ts b/tests/boundary/cli/jsonOutput.test.ts index b0525f16a4..67f46650f4 100644 --- a/tests/boundary/cli/jsonOutput.test.ts +++ b/tests/boundary/cli/jsonOutput.test.ts @@ -8,6 +8,7 @@ import { parse as parseYaml } from "yaml"; import { describe, expect, it } from "vitest"; import { writeJsonOutput } from "../../../src/cli/jsonOutput.js"; +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; import { createCliResultOutput, streamCliCommandResults, @@ -171,7 +172,7 @@ describe("CLI command output wrapping", () => { }, }); const output = createCliResultOutput( - ["--format", "json", "--full-output"], + parseCliOutputArguments(["--format", "json", "--full-output"]), destination, ); if (output === undefined) @@ -234,7 +235,10 @@ describe("CLI result output surface", () => { "--filter-output", "message", ]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing structured output surface"); const cli = Cli.create("rea-output-test", { sync: false }); @@ -311,7 +315,10 @@ describe("CLI result output surface", () => { }, }); const argv = ["fixture", "--json", "--full-output"]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing JSON output surface"); const cli = Cli.create("rea-output-test", { sync: false }); cli.command("fixture", { @@ -356,7 +363,10 @@ describe("full-output token windows", () => { "--token-limit", "3", ]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing TOON output surface"); const cli = Cli.create("rea-output-test", { sync: false }); registerManagedCommands(cli, silentLogger, async () => ({ @@ -402,7 +412,10 @@ describe("logger classification handoff", () => { source: "captured-metadata", }; const argv = ["fixture", "--json", "--full-output"]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing JSON output surface"); const cli = Cli.create("rea-output-test", { sync: false }); cli.command("fixture", { @@ -455,7 +468,10 @@ describe("full-output token counts", () => { "--filter-output", "message", ]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing JSON output surface"); const cli = Cli.create("rea-output-test", { sync: false }); registerManagedCommands(cli, silentLogger, async () => failure); @@ -479,7 +495,12 @@ describe("CLI result output surface", () => { ["--json", "--token-limit", "5"], ["--json", "--format", "invalid"], ])("delegates unsupported output controls %j", (...arguments_) => { - expect(createCliResultOutput(arguments_, new Writable())).toBe(undefined); + expect( + createCliResultOutput( + parseCliOutputArguments(arguments_), + new Writable(), + ), + ).toBe(undefined); }); it("keeps ownership after a failed write so no second document is emitted", async () => { @@ -489,7 +510,10 @@ describe("CLI result output surface", () => { callback(cause); }, }); - const output = createCliResultOutput(["--json"], destination); + const output = createCliResultOutput( + parseCliOutputArguments(["--json"]), + destination, + ); if (output === undefined) throw new Error("Missing JSON output surface"); await expect( output.write( @@ -541,7 +565,10 @@ describe("CLI result output surface", () => { // A terminal destination exercises the indented encoding past the limit. Object.defineProperty(destination, "isTTY", { value: true }); const argv = ["inspect-managed-members", "fixture.dll", "--json"]; - const output = createCliResultOutput(argv, destination); + const output = createCliResultOutput( + parseCliOutputArguments(argv), + destination, + ); if (output === undefined) throw new Error("Missing JSON output surface"); streamCliCommandResults(cli, output); let exitCode = 0; @@ -562,7 +589,7 @@ describe("CLI result output surface", () => { const chunks: Buffer[] = []; const tokenArgv = [...argv, "--token-count", "--full-output"]; const tokenOutput = createCliResultOutput( - tokenArgv, + parseCliOutputArguments(tokenArgv), new Writable({ write(chunk: Buffer, _encoding, callback) { chunks.push(chunk); @@ -623,7 +650,12 @@ describe("streamed CLI filtered selections", () => { }, }); const output = createCliResultOutput( - ["--format", format, "--filter-output", filter], + parseCliOutputArguments([ + "--format", + format, + "--filter-output", + filter, + ]), destination, ); if (output === undefined) throw new Error("Missing JSON output surface"); diff --git a/tests/support/cli/cliCommandSessionWorker.mjs b/tests/support/cli/cliCommandSessionWorker.mjs index ba8c152786..13da0a2b4d 100644 --- a/tests/support/cli/cliCommandSessionWorker.mjs +++ b/tests/support/cli/cliCommandSessionWorker.mjs @@ -59,7 +59,7 @@ const { normalizeCliArguments, renderCliOutputArgumentError, renderEmptyFilteredCliOutput, - validateCliOutputArguments, + parseCliOutputArguments, } = await import("../../../dist/cli-runtime/cliOutput.js"); const writeFrame = (value) => @@ -162,7 +162,8 @@ async function runOneShot(args) { let explicitExit; try { const cliArgs = normalizeCliArguments(args); - const outputArguments = validateCliOutputArguments(cliArgs); + const outputOptions = parseCliOutputArguments(cliArgs); + const outputArguments = outputOptions.validation; const configurationError = cliConfigurationError(process.env); if (configurationError !== undefined && !isDoctorCommand(cliArgs)) { process.stderr.write(`${configurationError}\n`); @@ -172,7 +173,7 @@ async function runOneShot(args) { process.exitCode = 1; } else { let wroteOutput = false; - const resultOutput = createCliResultOutput(cliArgs, destination); + const resultOutput = createCliResultOutput(outputOptions, destination); try { await createCli(process.env, resultOutput).serve(cliArgs, { stdout: (output) => { @@ -194,7 +195,7 @@ async function runOneShot(args) { process.exitCode = 1; } if (!wroteOutput && !resultOutput?.handled) { - const empty = renderEmptyFilteredCliOutput(cliArgs); + const empty = renderEmptyFilteredCliOutput(outputOptions); if (empty) process.stdout.write(empty); } } diff --git a/tests/support/cli/compiledCliWorker.mjs b/tests/support/cli/compiledCliWorker.mjs index fdaac82cb6..9dfdd17de6 100644 --- a/tests/support/cli/compiledCliWorker.mjs +++ b/tests/support/cli/compiledCliWorker.mjs @@ -10,7 +10,7 @@ const { normalizeCliArguments, renderCliOutputArgumentError, renderEmptyFilteredCliOutput, - validateCliOutputArguments, + parseCliOutputArguments, } = await import("../../../dist/cli-runtime/cliOutput.js"); const MAX_STDOUT_CHARS = 16 * 1024 * 1024; @@ -38,7 +38,10 @@ const destination = new Writable({ }, }); Object.defineProperty(destination, "isTTY", { value: false }); -const resultOutput = createCliResultOutput(["--json"], destination); +const resultOutput = createCliResultOutput( + parseCliOutputArguments(["--json"]), + destination, +); const cli = createCli(process.env, resultOutput); const resetOutput = () => { @@ -52,7 +55,8 @@ const runCommand = async (args) => { resetOutput(); try { const cliArgs = normalizeCliArguments(args); - const outputArguments = validateCliOutputArguments(cliArgs); + const outputOptions = parseCliOutputArguments(cliArgs); + const outputArguments = outputOptions.validation; if (!outputArguments.ok) return { exitCode: 1, @@ -87,7 +91,7 @@ const runCommand = async (args) => { }; } if (!wroteOutput && !resultOutput?.handled) { - const empty = renderEmptyFilteredCliOutput(cliArgs); + const empty = renderEmptyFilteredCliOutput(outputOptions); if (empty !== undefined) destination.write(empty); } // logCliCommand reports some failures only through process.exitCode.