Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 132 additions & 1 deletion src/application/commands/connector/index.cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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();

Expand Down Expand Up @@ -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\"");
Expand Down Expand Up @@ -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",
},
}));
}
38 changes: 33 additions & 5 deletions src/application/commands/connector/proxy.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -247,12 +250,37 @@ function parseJsonOption(value: string, errorKey: string): unknown {

function formatConnectorProxyResponseAsText(
response: ConnectorProxyResponse,
context: Pick<CliExecutionContext, "translator">,
context: Pick<CliExecutionContext, "stdout" | "translator">,
): 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);
}
24 changes: 24 additions & 0 deletions src/application/commands/connector/result-text.ts
Original file line number Diff line number Diff line change
@@ -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<CliExecutionContext["translator"], "t">,
): 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");
}
33 changes: 9 additions & 24 deletions src/application/commands/connector/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
teamIdentityInputShape,
teamOption,
} from "../team/identity.ts";
import { formatConnectorExecutionResultAsText } from "./result-text.ts";
import {
invalidateConnectorActionSchemaOnNotFound,
loadConnectorActionSchema,
Expand All @@ -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<CliExecutionContext, "stdout" | "translator">;
type ConnectorRunTarget = Pick<ConnectorRunInput, "serviceName"> & {
actionName: string;
};
Expand Down Expand Up @@ -287,7 +285,14 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
}

context.stdout.write(
`${formatConnectorRunResponseAsText(response, context)}\n`,
`${formatConnectorExecutionResultAsText(
{
data: response.data,
executionId: response.meta.executionId,
},
createWriterColors(context.stdout),
context.translator,
)}\n`,
);
},
};
Expand Down Expand Up @@ -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<typeof createWriterColors>,
): string {
return colors.cyan(JSON.stringify(value, null, 2) ?? "null");
}