fix(cli): preserve failure semantics across full-output formats

This commit is contained in:
morluto 2026-10-13 03:33:27 +08:00
parent 221c856481
commit 570c40cde5
14 changed files with 494 additions and 82 deletions

1
package-lock.json generated
View File

@ -41,6 +41,7 @@
"smol-toml": "1.9.0",
"stream-chain": "4.2.6",
"stream-json": "3.7.0",
"tokenx": "1.3.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"yaml": "2.9.0",

View File

@ -320,6 +320,7 @@
"smol-toml": "1.9.0",
"stream-chain": "4.2.6",
"stream-json": "3.7.0",
"tokenx": "1.3.0",
"write-file-atomic": "7.0.0",
"ws": "8.21.0",
"yaml": "2.9.0",

View File

@ -24,7 +24,7 @@ export const bundleRuntime = async (root) => {
{
cli: "cli.js",
cliOutput: "cliOutput.js",
streamedJsonOutput: "cli/streamedJsonOutput.js",
cliResultOutput: "cli/cliResultOutput.js",
mcpDoctor: "mcpDoctor.js",
},
],

View File

@ -49,7 +49,7 @@ if (await pathExists(join(root, "src"))) {
"dist/mcp/main.js",
"dist/cli-runtime/cli.js",
"dist/cli-runtime/cliOutput.js",
"dist/cli-runtime/streamedJsonOutput.js",
"dist/cli-runtime/cliResultOutput.js",
"dist/cli-runtime/mcpDoctor.js",
"dist/mcpDoctor.js",
"dist/cli.js",

View File

@ -37,7 +37,7 @@ const runtimeFiles = isMcpMode
: [
"dist/cli-runtime/cli.js",
"dist/cli-runtime/cliOutput.js",
"dist/cli-runtime/streamedJsonOutput.js",
"dist/cli-runtime/cliResultOutput.js",
];
if (!(await compiledRuntimeExists(runtimeFiles))) {
@ -60,8 +60,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
} else {
const { cliConfigurationError, createCli } =
await import("../dist/cli-runtime/cli.js");
const { createStreamedCliJsonOutput } =
await import("../dist/cli-runtime/streamedJsonOutput.js");
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
const {
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
@ -77,7 +77,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
process.exitCode = 1;
} else {
let wroteOutput = false;
const resultOutput = createStreamedCliJsonOutput(args, process.stdout);
const resultOutput = createCliResultOutput(args, process.stdout);
await createCli(process.env, resultOutput).serve(args, {
stdout: (output) => {
if (resultOutput?.handled) {

View File

@ -10,7 +10,7 @@ import { finished } from "node:stream/promises";
import { Cli } from "incur";
import { createStreamedCliJsonOutput } from "../../../dist/cli/streamedJsonOutput.js";
import { createCliResultOutput } from "../../../dist/cli/cliResultOutput.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
// This opt-in format-boundary check writes one temporary file larger than the
@ -79,7 +79,7 @@ try {
destination = createWriteStream(path, { flags: "wx", mode: 0o600 });
const argv = [metadata.command, "--format", format, "--full-output"];
const output = createStreamedCliJsonOutput(argv, destination);
const output = createCliResultOutput(argv, destination);
assert.ok(output);
let unexpectedOutputBytes = 0;
let exitCode = 0;

View File

@ -44,7 +44,7 @@ import type { CliInstance } from "./cli/types.js";
import {
streamCliCommandResults,
type CliResultOutput,
} from "./cli/streamedJsonOutput.js";
} from "./cli/cliResultOutput.js";
/** Describe CLI settings that are invalid before any command runs, as MCP does. */
export const cliConfigurationError = (

View File

@ -1,15 +1,24 @@
import { constants } from "node:buffer";
import type { Writable } from "node:stream";
import { Cli, Filter } from "incur";
import { Cli, Filter, Formatter } from "incur";
import { estimateTokenCount, sliceByTokens } from "tokenx";
import { parseCliOutputArguments } from "../cliOutput.js";
import {
parseCliOutputArguments,
validateCliOutputArguments,
} from "../cliOutput.js";
import { cliOperationFailurePayload } from "../cliLogging.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
projectAnalysisError,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import { jsonOutputParts, writeJsonOutput } from "./jsonOutput.js";
import {
jsonOutputParts,
writeJsonOutput,
writeOutputText,
} from "./jsonOutput.js";
import type { CliInstance } from "./types.js";
/** Result metadata supplied by the command that actually ran. */
@ -100,19 +109,15 @@ const wrapCommand = (
};
};
/** Preserve Incur's JSON controls while streaming the complete result. */
export const createStreamedCliJsonOutput = (
/** Preserve Incur output controls and stream JSON while framing failures. */
export const createCliResultOutput = (
arguments_: readonly string[],
destination: Writable,
): CliResultOutput | undefined => {
const options = parseCliOutputArguments(arguments_);
if (
options.parseError ||
options.tokenWindow ||
(options.format !== "json" && options.format !== "jsonl")
)
if (options.parseError || !validateCliOutputArguments(arguments_).ok)
return undefined;
const format = options.format;
const format = options.format as "json" | "jsonl" | "yaml" | "toon" | "md";
let handled = false;
let failed = false;
let error: AnalysisErrorProjection | undefined;
@ -128,27 +133,53 @@ export const createStreamedCliJsonOutput = (
},
async write(value, metadata) {
if (metadata.format !== format) return false;
const failure = options.fullOutput
? cliOperationFailurePayload(value)
: undefined;
const filtered: unknown = options.filterOutput
? Filter.apply(value, Filter.parse(options.filterOutput))
: value;
const meta = {
command: metadata.command,
duration: metadata.duration,
};
// Leave successful YAML and bare YAML rendering to Incur. The custom
// boundary is needed only for a full failure envelope, whose error
// sentinel otherwise drops category, remediation, and details.
if (format !== "json" && format !== "jsonl" && failure === undefined)
return false;
if (options.tokenCount) {
// Incur/tokenx materialize the formatted value. Measure its UTF-16
// expansion before allowing that allocation, independently of UTF-8 bytes.
// Incur/tokenx materialize JSON. Measure its UTF-16 expansion before
// allowing that allocation, independently of UTF-8 bytes. Other formats
// retain Incur's materializing behavior.
const tokenValue = failure ?? filtered;
let characters = 0;
if (
filtered !== undefined &&
filtered !== null &&
typeof filtered !== "function" &&
typeof filtered !== "symbol"
)
for (const part of jsonOutputParts(filtered, format))
characters += part.length;
characters = Math.max(0, characters - 1);
if (characters <= constants.MAX_STRING_LENGTH) return false;
tokenValue !== undefined &&
tokenValue !== null &&
typeof tokenValue !== "function" &&
typeof tokenValue !== "symbol"
) {
if (format === "json" || format === "jsonl") {
for (const part of jsonOutputParts(
tokenValue,
format,
format === "json",
))
characters += part.length;
characters = Math.max(0, characters - 1);
}
}
if (characters <= constants.MAX_STRING_LENGTH) {
if (failure === undefined) return false;
handled = true;
try {
await writeOutputText(
destination,
`${estimateTokenCount(Formatter.format(failure, format))}\n`,
);
return true;
} catch (cause: unknown) {
failed = true;
throw cause;
}
}
error = projectAnalysisError(
new AnalysisResourceConstraintError(
metadata.command,
@ -165,10 +196,26 @@ export const createStreamedCliJsonOutput = (
),
);
}
const meta = {
command: metadata.command,
duration: metadata.duration,
};
const errorDocument =
options.fullOutput && failure !== undefined
? fullFailureEnvelope(
failure,
meta,
format,
options.tokenLimit,
options.tokenOffset,
)
: undefined;
const document = options.fullOutput
? filtered === undefined
? { ok: true, meta }
: { ok: true, data: filtered, meta }
? failure !== undefined
? errorDocument
: filtered === undefined
? { ok: true, meta }
: { ok: true, data: filtered, meta }
: filtered === undefined ||
filtered === null ||
typeof filtered === "function" ||
@ -179,15 +226,24 @@ export const createStreamedCliJsonOutput = (
// document from Incur's error formatter.
handled = true;
try {
await writeJsonOutput(
error === undefined
? document
: options.fullOutput
? { ok: false, error, meta }
: error,
destination,
format,
);
if (failure !== undefined && format !== "json" && format !== "jsonl") {
// Incur's error sentinel only carries code/message/retryable. Render
// the full caller-visible failure at this output boundary.
await writeOutputText(
destination,
`${Formatter.format(document, format)}\n`,
);
} else {
await writeJsonOutput(
error === undefined
? document
: options.fullOutput
? { ok: false, error, meta }
: error,
destination,
format as "json" | "jsonl",
);
}
} catch (cause: unknown) {
failed = true;
throw cause;
@ -196,3 +252,26 @@ export const createStreamedCliJsonOutput = (
},
};
};
const fullFailureEnvelope = (
failure: unknown,
meta: Readonly<Record<string, unknown>>,
format: "json" | "jsonl" | "yaml" | "toon" | "md",
tokenLimit: number | undefined,
tokenOffset: number | undefined,
): Readonly<Record<string, unknown>> => {
if (tokenLimit === undefined && tokenOffset === undefined)
return { ok: false, error: failure, meta };
const formatted = Formatter.format(failure, format);
const total = estimateTokenCount(formatted);
const offset = tokenOffset ?? 0;
const end = tokenLimit === undefined ? total : offset + tokenLimit;
if (offset === 0 && end >= total) return { ok: false, error: failure, meta };
const actualEnd = Math.min(end, total);
const nextOffset = actualEnd < total ? actualEnd : undefined;
return {
ok: false,
error: `${sliceByTokens(formatted, offset, end)}\n[truncated: showing tokens ${offset}–${actualEnd} of ${total}]`,
meta: { ...meta, ...(nextOffset === undefined ? {} : { nextOffset }) },
};
};

View File

@ -19,13 +19,16 @@ export const writeJsonOutput = async (
for (const part of bufferedJsonParts(
jsonOutputParts(value, format, isTerminal(destination)),
))
await writePart(destination, part);
await writeOutputText(destination, part);
};
const isTerminal = (destination: Writable): boolean =>
"isTTY" in destination && destination.isTTY === true;
const writePart = (destination: Writable, part: string): Promise<void> =>
export const writeOutputText = (
destination: Writable,
part: string,
): Promise<void> =>
new Promise((resolve, reject) => {
const onError = (cause: unknown): void => reject(cause);
destination.once("error", onError);

View File

@ -12,20 +12,64 @@ import type { Logger } from "pino";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Identify caller-visible CLI outcomes that mean the requested operation failed. */
export const isCliOperationFailure = (value: unknown): boolean => {
if (!isRecord(value)) return false;
if (
isRecord(value.error) &&
analysisErrorProjectionSchema.safeParse(value.error).success
)
return true;
if (analysisCliErrorEnvelopeSchema.safeParse(value).success) return true;
if (analysisErrorProjectionSchema.safeParse(value).success) return true;
if (value.healthy === false) return true;
return false;
const loggedFailures = new WeakMap<object, unknown>();
/** Return the canonical failure projection owned by the CLI result boundary. */
const projectCliOperationFailure = (
value: unknown,
): AnalysisErrorProjection | undefined => {
if (!isRecord(value)) return undefined;
const direct = analysisErrorProjectionSchema.safeParse(value);
if (direct.success) return value as AnalysisErrorProjection;
const envelope = analysisCliErrorEnvelopeSchema.safeParse(value);
if (envelope.success) {
const {
error: _label,
input_path: inputPath,
input_reason: inputReason,
...projection
} = value;
const parsed = projection as AnalysisErrorProjection;
if (typeof inputPath !== "string" && typeof inputReason !== "string")
return parsed;
return {
...parsed,
details: {
...parsed.details,
...(typeof inputPath === "string" ? { input_path: inputPath } : {}),
...(typeof inputReason === "string"
? { input_reason: inputReason }
: {}),
},
};
}
if (isRecord(value.error)) {
const nested = analysisErrorProjectionSchema.safeParse(value.error);
if (nested.success) return value.error as AnalysisErrorProjection;
}
return undefined;
};
/** Return the canonical projection or original record classified as failed. */
const inferCliOperationFailure = (value: unknown): unknown | undefined => {
const projection = projectCliOperationFailure(value);
if (projection !== undefined) return projection;
if (isRecord(value) && value.healthy === false) return value;
return undefined;
};
export const cliOperationFailurePayload = (
value: unknown,
): unknown | undefined => {
if (isRecord(value) && loggedFailures.has(value))
return loggedFailures.get(value);
return inferCliOperationFailure(value);
};
/** Identify caller-visible CLI outcomes that mean the requested operation failed. */
export const isCliOperationFailure = (value: unknown): boolean =>
cliOperationFailurePayload(value) !== undefined;
/** Log one CLI command with duration and a stable success or failure status. */
export const logCliCommand = async <Value>(
logger: Logger,
@ -36,7 +80,13 @@ export const logCliCommand = async <Value>(
const startedAt = performance.now();
try {
const value = await execute();
const failed = isCommandFailure?.(value) ?? isCliOperationFailure(value);
const inferredFailure = inferCliOperationFailure(value);
const failed = isCommandFailure?.(value) ?? inferredFailure !== undefined;
if (isRecord(value))
loggedFailures.set(
value,
failed ? (inferredFailure ?? value) : undefined,
);
logger[failed ? "error" : "info"](
{
command,

View File

@ -20,6 +20,8 @@ interface ParsedCliOutputArguments {
readonly parseError: boolean;
readonly tokenCount: boolean;
readonly tokenWindow: boolean;
readonly tokenLimit: number | undefined;
readonly tokenOffset: number | undefined;
}
/** Read effective Incur output controls at the executable boundary. */
@ -30,14 +32,18 @@ export const parseCliOutputArguments = (
let filterOutput: string | undefined;
let fullOutput = false;
let tokenCount = false;
let tokenWindow = false;
let tokenLimit: number | undefined;
let tokenOffset: number | undefined;
const parsed = (parseError: boolean): ParsedCliOutputArguments => ({
filterOutput,
format,
fullOutput,
parseError,
tokenCount,
tokenWindow: parseError ? false : tokenWindow,
tokenWindow:
!parseError && (tokenLimit !== undefined || tokenOffset !== undefined),
tokenLimit: parseError ? undefined : tokenLimit,
tokenOffset: parseError ? undefined : tokenOffset,
});
for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index];
@ -71,7 +77,8 @@ export const parseCliOutputArguments = (
const numericValue = Number(value);
if (!Number.isFinite(numericValue) || value.trim() === "")
return parsed(true);
tokenWindow = true;
if (argument === "--token-limit") tokenLimit = numericValue;
else tokenOffset = numericValue;
index += 1;
}
continue;

View File

@ -32,8 +32,8 @@ describe("executable dispatcher", () => {
"export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n",
),
workspace.write(
"dist/cli-runtime/streamedJsonOutput.js",
"export const createStreamedCliJsonOutput = () => undefined;\n",
"dist/cli-runtime/cliResultOutput.js",
"export const createCliResultOutput = () => undefined;\n",
),
workspace.write(
"dist/cli-runtime/mcpDoctor.js",

View File

@ -2,15 +2,19 @@ import { constants } from "node:buffer";
import { createHash } from "node:crypto";
import { Writable } from "node:stream";
import { Cli } from "incur";
import { Cli, Formatter } from "incur";
import { estimateTokenCount } from "tokenx";
import { parse as parseYaml } from "yaml";
import { describe, expect, it } from "vitest";
import { writeJsonOutput } from "../../../src/cli/jsonOutput.js";
import {
createStreamedCliJsonOutput,
createCliResultOutput,
streamCliCommandResults,
} from "../../../src/cli/streamedJsonOutput.js";
} from "../../../src/cli/cliResultOutput.js";
import { logCliCommand } from "../../../src/cliLogging.js";
import { registerManagedCommands } from "../../../src/cli/managedCommands.js";
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
import { silentLogger } from "../../../src/logger.js";
const collect = async (
@ -157,16 +161,283 @@ describe("incremental CLI JSON", () => {
);
});
describe("streamed CLI result surface", () => {
describe("CLI result output surface", () => {
it.each(["json", "yaml", "toon", "md"] as const)(
"places typed analysis failures in the full-output error envelope (%s)",
async (format) => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const failure = {
error: "Analysis failed",
code: "target_unavailable",
category: "unavailable",
message: "The selected artifact is missing.",
retryable: false,
remediation: { action: "Check that the path exists." },
details: { source: "artifact-open" },
input_path: "missing.dll",
input_reason: "read-failed",
};
const argv = [
"inspect-managed-artifact",
"missing.dll",
"--format",
format,
"--full-output",
"--filter-output",
"message",
];
const output = createCliResultOutput(argv, destination);
if (output === undefined)
throw new Error("Missing structured output surface");
const cli = Cli.create("rea-output-test", { sync: false });
registerManagedCommands(cli, silentLogger, async () => failure);
streamCliCommandResults(cli, output);
let fallback = "";
const previousExitCode = process.exitCode;
try {
await cli.serve(argv, {
stdout: (text) => {
if (!output.handled) fallback += text;
},
});
expect(process.exitCode).toBe(1);
} finally {
process.exitCode = previousExitCode;
}
const text = Buffer.concat(chunks).toString("utf8");
expect(output.handled).toBe(true);
expect(fallback).toBe("");
if (format === "json" || format === "yaml") {
const document = format === "json" ? JSON.parse(text) : parseYaml(text);
expect(document).toMatchObject({
ok: false,
error: {
code: failure.code,
category: failure.category,
message: failure.message,
retryable: false,
remediation: failure.remediation,
details: {
...failure.details,
input_path: failure.input_path,
input_reason: failure.input_reason,
},
},
meta: { command: "inspect-managed-artifact" },
});
expect(() =>
analysisErrorProjectionSchema.parse(document.error),
).not.toThrow();
expect(document).not.toHaveProperty("data");
} else {
expect(text).toContain("false");
expect(text).toContain(failure.code);
expect(text).not.toContain("true");
}
},
);
it.each([
{
name: "custom command failure",
value: {
status: "failed",
message: "Update was not applied.",
remediation: "Check the package manager, then retry.",
details: { stage: "install" },
},
isFailure: () => true,
},
{
name: "unhealthy doctor result",
value: { healthy: false, checks: [{ name: "provider", ok: false }] },
isFailure: undefined,
},
])("preserves $name as a full-output error payload", async (scenario) => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const argv = ["fixture", "--json", "--full-output"];
const output = createCliResultOutput(argv, destination);
if (output === undefined) throw new Error("Missing JSON output surface");
const cli = Cli.create("rea-output-test", { sync: false });
cli.command("fixture", {
run: () =>
logCliCommand(
silentLogger,
"fixture",
async () => scenario.value,
scenario.isFailure,
),
});
streamCliCommandResults(cli, output);
const previousExitCode = process.exitCode;
try {
await cli.serve(argv, { stdout: () => undefined });
expect(process.exitCode).toBe(1);
} finally {
process.exitCode = previousExitCode;
}
expect(JSON.parse(Buffer.concat(chunks).toString("utf8"))).toMatchObject({
ok: false,
error: scenario.value,
});
});
});
describe("full-output token windows", () => {
it("preserves Incur token-window metadata when truncating a TOON failure", async () => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const argv = [
"inspect-managed-artifact",
"missing.dll",
"--format",
"toon",
"--full-output",
"--token-limit",
"3",
];
const output = createCliResultOutput(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 () => ({
code: "target_unavailable",
category: "unavailable",
message: "The selected artifact is missing.",
retryable: false,
remediation: { action: "Check that the path exists." },
details: { source: "artifact-open" },
}));
streamCliCommandResults(cli, output);
const previousExitCode = process.exitCode;
try {
await cli.serve(argv, { stdout: () => undefined });
expect(process.exitCode).toBe(1);
} finally {
process.exitCode = previousExitCode;
}
const text = Buffer.concat(chunks).toString("utf8");
expect(text).toContain("[truncated: showing tokens 0–3 of");
expect(text).toMatch(/nextOffset:?\s*3/);
expect(text).toContain("ok: false");
});
});
describe("logger classification handoff", () => {
it("honors an explicit success for data containing error-shaped fields", async () => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const result = {
error: {
code: "target_unavailable",
category: "unavailable",
message: "Source data describes a missing target.",
retryable: false,
remediation: { action: "Use the value as source data." },
},
source: "captured-metadata",
};
const argv = ["fixture", "--json", "--full-output"];
const output = createCliResultOutput(argv, destination);
if (output === undefined) throw new Error("Missing JSON output surface");
const cli = Cli.create("rea-output-test", { sync: false });
cli.command("fixture", {
run: () =>
logCliCommand(
silentLogger,
"fixture",
async () => result,
() => false,
),
});
streamCliCommandResults(cli, output);
const previousExitCode = process.exitCode;
try {
await cli.serve(argv, { stdout: () => undefined });
expect(process.exitCode).toBe(previousExitCode);
} finally {
process.exitCode = previousExitCode;
}
expect(JSON.parse(Buffer.concat(chunks).toString("utf8"))).toMatchObject({
ok: true,
data: result,
});
});
});
describe("full-output token counts", () => {
it("counts the full failure payload before applying output filters", async () => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const failure = {
code: "target_unavailable",
category: "unavailable",
message: "The selected artifact is missing.",
retryable: false,
remediation: { action: "Check that the path exists." },
details: { source: "artifact-open" },
};
const argv = [
"inspect-managed-artifact",
"missing.dll",
"--json",
"--full-output",
"--token-count",
"--filter-output",
"message",
];
const output = createCliResultOutput(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);
streamCliCommandResults(cli, output);
const previousExitCode = process.exitCode;
try {
await cli.serve(argv, { stdout: () => undefined });
expect(process.exitCode).toBe(1);
} finally {
process.exitCode = previousExitCode;
}
expect(output.handled).toBe(true);
expect(Buffer.concat(chunks).toString("utf8")).toBe(
`${estimateTokenCount(Formatter.format(failure, "json"))}\n`,
);
});
});
describe("CLI result output surface", () => {
it.each([
[],
["--format", "md"],
["--json", "--token-limit", "5"],
["--json", "--format", "invalid"],
])("delegates unsupported output controls %j", (...arguments_) => {
expect(createStreamedCliJsonOutput(arguments_, new Writable())).toBe(
undefined,
);
expect(createCliResultOutput(arguments_, new Writable())).toBe(undefined);
});
it("keeps ownership after a failed write so no second document is emitted", async () => {
@ -176,7 +447,7 @@ describe("streamed CLI result surface", () => {
callback(cause);
},
});
const output = createStreamedCliJsonOutput(["--json"], destination);
const output = createCliResultOutput(["--json"], destination);
if (output === undefined) throw new Error("Missing JSON output surface");
await expect(
output.write(
@ -228,7 +499,7 @@ describe("streamed CLI result 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 = createStreamedCliJsonOutput(argv, destination);
const output = createCliResultOutput(argv, destination);
if (output === undefined) throw new Error("Missing JSON output surface");
streamCliCommandResults(cli, output);
let exitCode = 0;
@ -248,7 +519,7 @@ describe("streamed CLI result surface", () => {
const chunks: Buffer[] = [];
const tokenArgv = [...argv, "--token-count", "--full-output"];
const tokenOutput = createStreamedCliJsonOutput(
const tokenOutput = createCliResultOutput(
tokenArgv,
new Writable({
write(chunk: Buffer, _encoding, callback) {

View File

@ -50,7 +50,7 @@ export const lazyUrl = import.meta.url;`,
'export const generatedPackageMetadata = { version: "1.0.0" };',
"dist/cli.js": 'export { facts, run } from "./main.js";',
"dist/cliOutput.js": 'export { facts } from "./provider/probe λ.js";',
"dist/cli/streamedJsonOutput.js":
"dist/cli/cliResultOutput.js":
'export { facts } from "../provider/probe λ.js";',
"dist/mcpDoctor.js": 'export { run } from "./main.js";',
"dist/main.js": `
@ -98,7 +98,7 @@ export const run = async () => {
[
"--input-type=module",
"--eval",
`const { facts } = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cli.js")).href)}); const output = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliOutput.js")).href)}); const streamed = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "streamedJsonOutput.js")).href)}); process.stdout.write(JSON.stringify(facts === output.facts && facts === streamed.facts));`,
`const { facts } = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cli.js")).href)}); const output = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliOutput.js")).href)}); const streamed = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliResultOutput.js")).href)}); process.stdout.write(JSON.stringify(facts === output.facts && facts === streamed.facts));`,
],
{ cwd: workspace.root },
);
@ -136,7 +136,7 @@ export const run = async () => {
await workspace.write("package.json", '{"type":"module"}');
for (const path of [
"dist/cliOutput.js",
"dist/cli/streamedJsonOutput.js",
"dist/cli/cliResultOutput.js",
"dist/mcpDoctor.js",
])
await workspace.write(path, "export {};\n");