From 387907af3e07a54e38c618b52819605457af49a2 Mon Sep 17 00:00:00 2001 From: Kevin Cui Date: Mon, 31 Aug 2026 22:47:28 -0400 Subject: [PATCH] feat(connector): color proxy text output like connector run `oo connector proxy` printed its text output unstyled while `oo connector run` highlighted the execution id and result data, so the two commands looked inconsistent when used side by side. Move the shared execution id and result data rendering into _src/application/commands/connector/result-text.ts_ so both commands go through one formatter, and color the proxied HTTP status by class (green for 2xx, yellow for 3xx, red for 4xx and 5xx). The status coloring matters because the CLI itself exits 0 even when the upstream request failed, so a red status is the only at-a-glance signal. Signed-off-by: Kevin Cui --- .../commands/connector/index.cli.test.ts | 133 +++++++++++++++++- src/application/commands/connector/proxy.ts | 38 ++++- .../commands/connector/result-text.ts | 24 ++++ src/application/commands/connector/run.ts | 33 ++--- 4 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 src/application/commands/connector/result-text.ts diff --git a/src/application/commands/connector/index.cli.test.ts b/src/application/commands/connector/index.cli.test.ts index efe1f2b..56bb2b6 100644 --- a/src/application/commands/connector/index.cli.test.ts +++ b/src/application/commands/connector/index.cli.test.ts @@ -25,6 +25,7 @@ import { readTelemetryRowsForTest, } from "../../telemetry/outbox.ts"; import { createTerminalColors } from "../../terminal-colors.ts"; +import { connectorExecutionIdColor } from "./result-text.ts"; import { createConnectorSchemaCacheScope, loadConnectorActionSchema, @@ -786,6 +787,123 @@ describe("connectorCommand CLI", () => { } }); + test("renders connector proxy text output with the same highlighting as connector run", async () => { + const sandbox = await createCliSandbox(); + const colors = createTerminalColors(true); + + try { + await writeAuthFile(sandbox); + + const result = await sandbox.run( + [ + "connector", + "proxy", + "tavily", + "--endpoint", + "/search", + "--method", + "GET", + ], + { + fetcher: async () => createConnectorProxyResponse(200, { + results: [{ title: "OOMOL" }], + }), + stdout: { + hasColors: true, + }, + }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain(`Status: ${colors.green(200)}`); + expect(result.stdout).toContain( + `Execution ID: ${colors.hex(connectorExecutionIdColor)("exec-1")}`, + ); + expect(result.stdout).toContain(colors.bold("Result data:")); + expect(result.stdout).toContain("\u001B[36m{\n"); + expect(result.stdout).toContain("\"title\": \"OOMOL\""); + } + finally { + await sandbox.cleanup(); + } + }); + + test("colors non-success connector proxy HTTP statuses by class", async () => { + const sandbox = await createCliSandbox(); + const colors = createTerminalColors(true); + + try { + await writeAuthFile(sandbox); + + for (const [status, expected] of [ + [301, colors.yellow(301)], + [404, colors.red(404)], + [503, colors.red(503)], + ] as const) { + const result = await sandbox.run( + [ + "connector", + "proxy", + "tavily", + "--endpoint", + "/search", + "--method", + "GET", + ], + { + fetcher: async () => createConnectorProxyResponse(status, null), + stdout: { + hasColors: true, + }, + }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`Status: ${expected}`); + expect(result.stdout).toContain(colors.cyan("null")); + } + } + finally { + await sandbox.cleanup(); + } + }); + + test("keeps connector proxy text output plain without color support", async () => { + const sandbox = await createCliSandbox(); + + try { + await writeAuthFile(sandbox); + + const result = await sandbox.run( + [ + "connector", + "proxy", + "tavily", + "--endpoint", + "/search", + "--method", + "GET", + ], + { + fetcher: async () => createConnectorProxyResponse(404, null), + }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe([ + "Status: 404", + "Execution ID: exec-1", + "Result data:", + "null", + "", + ].join("\n")); + } + finally { + await sandbox.cleanup(); + } + }); + test("normalizes connector proxy method values case-insensitively", async () => { const sandbox = await createCliSandbox(); @@ -3359,7 +3477,7 @@ describe("connectorCommand CLI", () => { expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); - expect(result.stdout).toContain(colors.hex("#59F78D")("exec-1")); + expect(result.stdout).toContain(colors.hex(connectorExecutionIdColor)("exec-1")); expect(result.stdout).toContain(colors.bold("Result data:")); expect(result.stdout).toContain("\u001B[36m{\n"); expect(result.stdout).toContain("\"messageId\": \"message-1\""); @@ -5530,3 +5648,16 @@ function collapseWhitespace(value: string): string { .filter(Boolean) .join(" "); } + +function createConnectorProxyResponse(status: number, data: unknown): Response { + return new Response(JSON.stringify({ + data: { + data, + status, + }, + meta: { + executionId: "exec-1", + service: "tavily", + }, + })); +} diff --git a/src/application/commands/connector/proxy.ts b/src/application/commands/connector/proxy.ts index 185081a..1e7a619 100644 --- a/src/application/commands/connector/proxy.ts +++ b/src/application/commands/connector/proxy.ts @@ -1,15 +1,18 @@ import type { CliCommandDefinition, CliExecutionContext } from "../../contracts/cli.ts"; +import type { TerminalColors } from "../../terminal-colors.ts"; import type { ConnectorProxyResponse } from "./shared.ts"; import { Buffer } from "node:buffer"; import { z } from "zod"; import { CliUserError } from "../../contracts/cli.ts"; import { bucketTelemetryBytes } from "../../telemetry/buckets.ts"; +import { createWriterColors } from "../../terminal-colors.ts"; import { readJsonInputValue } from "../shared/json-input.ts"; import { teamIdentityInputShape, teamOption, } from "../team/identity.ts"; +import { formatConnectorExecutionResultAsText } from "./result-text.ts"; import { resolveConnectorSession } from "./session.ts"; import { runConnectorProxy } from "./shared.ts"; import { recordConnectorFailureTelemetry } from "./telemetry.ts"; @@ -247,12 +250,37 @@ function parseJsonOption(value: string, errorKey: string): unknown { function formatConnectorProxyResponseAsText( response: ConnectorProxyResponse, - context: Pick, + context: Pick, ): string { + const colors = createWriterColors(context.stdout); + return [ - `${context.translator.t("connector.proxy.text.status")}: ${response.data.status}`, - `${context.translator.t("connector.run.text.executionId")}: ${response.meta.executionId}`, - `${context.translator.t("connector.run.text.resultData")}:`, - JSON.stringify(response.data.data, null, 2) ?? "null", + `${context.translator.t("connector.proxy.text.status")}: ${formatConnectorProxyStatus(response.data.status, colors)}`, + formatConnectorExecutionResultAsText( + { + data: response.data.data, + executionId: response.meta.executionId, + }, + colors, + context.translator, + ), ].join("\n"); } + +// The upstream HTTP status is colored by its class so a proxied 4xx/5xx is +// visible at a glance even though the CLI command itself succeeded. +function formatConnectorProxyStatus(status: number, colors: TerminalColors): string { + if (status >= 400) { + return colors.red(status); + } + + if (status >= 300) { + return colors.yellow(status); + } + + if (status >= 200) { + return colors.green(status); + } + + return String(status); +} diff --git a/src/application/commands/connector/result-text.ts b/src/application/commands/connector/result-text.ts new file mode 100644 index 0000000..d67ce71 --- /dev/null +++ b/src/application/commands/connector/result-text.ts @@ -0,0 +1,24 @@ +import type { CliExecutionContext } from "../../contracts/cli.ts"; +import type { TerminalColors } from "../../terminal-colors.ts"; + +export const connectorExecutionIdColor = "#59F78D"; + +/** + * Renders the execution id and result data block shared by every connector + * command that prints a backend execution result as text (`run`, `proxy`). + * Keeping it in one place guarantees the two commands highlight identically. + */ +export function formatConnectorExecutionResultAsText( + result: { + data: unknown; + executionId: string; + }, + colors: TerminalColors, + translator: Pick, +): string { + return [ + `${translator.t("connector.run.text.executionId")}: ${colors.hex(connectorExecutionIdColor)(result.executionId)}`, + colors.bold(`${translator.t("connector.run.text.resultData")}:`), + colors.cyan(JSON.stringify(result.data, null, 2) ?? "null"), + ].join("\n"); +} diff --git a/src/application/commands/connector/run.ts b/src/application/commands/connector/run.ts index 20dd8e5..4b87e8b 100644 --- a/src/application/commands/connector/run.ts +++ b/src/application/commands/connector/run.ts @@ -18,6 +18,7 @@ import { teamIdentityInputShape, teamOption, } from "../team/identity.ts"; +import { formatConnectorExecutionResultAsText } from "./result-text.ts"; import { invalidateConnectorActionSchemaOnNotFound, loadConnectorActionSchema, @@ -30,15 +31,12 @@ import { import { recordConnectorFailureTelemetry } from "./telemetry.ts"; import { validateConnectorActionInput } from "./validation.ts"; -const connectorRunExecutionIdColor = "#59F78D"; - const connectorRunDataErrorKeys = { dataFilePathRequired: "errors.connectorRun.dataFilePathRequired", dataReadFailed: "errors.connectorRun.dataReadFailed", invalidDataJson: "errors.connectorRun.invalidDataJson", } as const; -type ConnectorRunTextContext = Pick; type ConnectorRunTarget = Pick & { actionName: string; }; @@ -287,7 +285,14 @@ export const connectorRunCommand: CliCommandDefinition = { } context.stdout.write( - `${formatConnectorRunResponseAsText(response, context)}\n`, + `${formatConnectorExecutionResultAsText( + { + data: response.data, + executionId: response.meta.executionId, + }, + createWriterColors(context.stdout), + context.translator, + )}\n`, ); }, }; @@ -633,23 +638,3 @@ class ConnectorAsyncLifecycleProgressReporter extends TerminalProgressRenderer { return [""]; } } - -function formatConnectorRunResponseAsText( - response: ConnectorActionRunResponse, - context: ConnectorRunTextContext, -): string { - const colors = createWriterColors(context.stdout); - - return [ - `${context.translator.t("connector.run.text.executionId")}: ${colors.hex(connectorRunExecutionIdColor)(response.meta.executionId)}`, - colors.bold(`${context.translator.t("connector.run.text.resultData")}:`), - formatConnectorRunResultData(response.data, colors), - ].join("\n"); -} - -function formatConnectorRunResultData( - value: unknown, - colors: ReturnType, -): string { - return colors.cyan(JSON.stringify(value, null, 2) ?? "null"); -}