refactor(cli): share parsed output controls across consumers
This commit is contained in:
1 parent
4526862728
commit
8d8d85ba22
13 files changed
+228
-135
No files matched your search
+6
-5
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+58
-58
@@ -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} <value> or ${flag}=<value>.`;
|
||||
|
||||
/** 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 <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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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", {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }) });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user