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"); -}