fix: bound cli dump previews from yaml
This commit is contained in:
+42
-13
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user