fix: bound cli dump previews from yaml

This commit is contained in:
Codex
2026-07-01 03:02:16 +00:00
parent a2ed11575d
commit 2210b66eee
7 changed files with 327 additions and 45 deletions
+42 -13
View File
@@ -23,8 +23,9 @@ const CLI_OUTPUT_CONFIG_PATH = rootPath("config", "unidesk-cli.yaml");
const EMERGENCY_OUTPUT_DUMP_THRESHOLD_BYTES = 10 * 1024;
const EMERGENCY_OUTPUT_DUMP_DIR = join(tmpdir(), "unidesk-cli-output");
interface CliOutputPolicy {
export interface CliOutputPolicy {
maxStdoutBytes: number;
maxPreviewLines: number;
dumpDir: string;
includePreview: boolean;
warning: string;
@@ -71,6 +72,10 @@ export function emitError(command: string, error: unknown): void {
safeStdoutWrite(renderEnvelope(command, envelope));
}
export function readCliOutputPolicy(): CliOutputPolicy {
return cliOutputPolicy();
}
function normalizeErrorPayload(command: string, error: unknown): Record<string, unknown> {
const message = error instanceof Error ? error.message : String(error);
const parsed = parseStructuredErrorMessage(command, message);
@@ -141,16 +146,18 @@ function shouldSuppressStack(prefix: string): boolean {
function renderEnvelope<T>(command: string, envelope: JsonEnvelope<T>): string {
const fullText = `${JSON.stringify(envelope, null, 2)}\n`;
if (!shouldDumpLargeOutput(command, fullText, envelope)) return fullText;
const policy = cliOutputPolicy();
const trigger = outputDumpTrigger(fullText, policy, "json");
if (trigger === null) return fullText;
const dump = dumpLargeOutput(command, fullText, "json", policy);
const compactPayload = {
outputTruncated: true,
reason: "stdout-json-exceeded-threshold",
reason: trigger.reason,
warning: policy.warning,
message: "Full JSON output was written to a temporary file; stdout contains only file metadata and a compact summary.",
disclosurePolicy: disclosurePolicy(policy),
trigger,
dump,
summary: summarizeEnvelope(envelope),
};
@@ -160,29 +167,47 @@ function renderEnvelope<T>(command: string, envelope: JsonEnvelope<T>): string {
return `${JSON.stringify(compactEnvelope, null, 2)}\n`;
}
function shouldDumpLargeOutput(command: string, text: string, envelope: JsonEnvelope<unknown>): boolean {
void command;
void envelope;
if (process.env.UNIDESK_CLI_OUTPUT_DUMP_DISABLED === "1") return false;
const threshold = cliOutputPolicy().maxStdoutBytes;
return Buffer.byteLength(text, "utf8") > threshold;
function outputDumpTrigger(text: string, policy: CliOutputPolicy, content: "json" | "text"): Record<string, unknown> | null {
if (process.env.UNIDESK_CLI_OUTPUT_DUMP_DISABLED === "1") return null;
const bytes = Buffer.byteLength(text, "utf8");
const lines = countLines(text);
if (bytes > policy.maxStdoutBytes) {
return {
reason: `stdout-${content}-bytes-exceeded-threshold`,
thresholdBytes: policy.maxStdoutBytes,
observedBytes: bytes,
thresholdLines: policy.maxPreviewLines,
observedLines: lines,
};
}
if (lines > policy.maxPreviewLines) {
return {
reason: `stdout-${content}-lines-exceeded-threshold`,
thresholdBytes: policy.maxStdoutBytes,
observedBytes: bytes,
thresholdLines: policy.maxPreviewLines,
observedLines: lines,
};
}
return null;
}
function renderTextOutput(command: string, text: string): string {
const fullText = text.endsWith("\n") ? text : `${text}\n`;
if (process.env.UNIDESK_CLI_OUTPUT_DUMP_DISABLED === "1") return fullText;
const policy = cliOutputPolicy();
if (Buffer.byteLength(fullText, "utf8") <= policy.maxStdoutBytes) return fullText;
const trigger = outputDumpTrigger(fullText, policy, "text");
if (trigger === null) return fullText;
const dump = dumpLargeOutput(command, fullText, "txt", policy);
const payload: JsonEnvelope<Record<string, unknown>> = {
ok: true,
command,
data: {
outputTruncated: true,
reason: "stdout-text-exceeded-threshold",
reason: trigger.reason,
warning: policy.warning,
message: "Full text output was written to a temporary file; stdout contains only file metadata.",
disclosurePolicy: disclosurePolicy(policy),
trigger,
dump,
},
};
@@ -200,6 +225,7 @@ function dumpLargeOutput(command: string, text: string, extension: "json" | "txt
path,
configPath: policy.configPath,
thresholdBytes: policy.maxStdoutBytes,
thresholdLines: policy.maxPreviewLines,
bytes: Buffer.byteLength(text, "utf8"),
chars: text.length,
lines: countLines(text),
@@ -292,6 +318,7 @@ function disclosurePolicy(policy: CliOutputPolicy): Record<string, unknown> {
return {
configPath: policy.configPath,
maxStdoutBytes: policy.maxStdoutBytes,
maxPreviewLines: policy.maxPreviewLines,
dumpDir: policy.dumpDir,
includePreview: policy.includePreview,
recommendation: "Prefer k8s-style concise summaries/tables by default; expose full data through explicit --full/--raw/id-specific drill-down commands instead of large stdout.",
@@ -520,6 +547,7 @@ function cliOutputPolicy(): CliOutputPolicy {
const output = objectField(root, "output", CLI_OUTPUT_CONFIG_RELATIVE_PATH);
cachedOutputPolicy = {
maxStdoutBytes: positiveIntegerField(output, "maxStdoutBytes", `${CLI_OUTPUT_CONFIG_RELATIVE_PATH}.output`),
maxPreviewLines: positiveIntegerField(output, "maxPreviewLines", `${CLI_OUTPUT_CONFIG_RELATIVE_PATH}.output`),
dumpDir: absolutePathField(output, "dumpDir", `${CLI_OUTPUT_CONFIG_RELATIVE_PATH}.output`),
includePreview: booleanField(output, "includePreview", `${CLI_OUTPUT_CONFIG_RELATIVE_PATH}.output`),
warning: stringField(output, "warning", `${CLI_OUTPUT_CONFIG_RELATIVE_PATH}.output`),
@@ -530,6 +558,7 @@ function cliOutputPolicy(): CliOutputPolicy {
const message = error instanceof Error ? error.message : String(error);
cachedOutputPolicy = {
maxStdoutBytes: EMERGENCY_OUTPUT_DUMP_THRESHOLD_BYTES,
maxPreviewLines: 240,
dumpDir: EMERGENCY_OUTPUT_DUMP_DIR,
includePreview: false,
warning: "CLI output policy YAML could not be loaded; emergency dump guard is active for one-off drill-down only. Fix config/unidesk-cli.yaml, then improve the noisy command itself to print concise tables/summaries and id-specific progressive disclosure instead of repeatedly depending on dump extraction.",