Skip to content

Commit 8dcba68

Browse files
committed
refactor: convert Tier 2-3 commands to return-based output and consola
Convert remaining non-streaming commands away from direct stdout/stderr writes, continuing the output convergence started in #382. Return-based output (OutputConfig<T>): - project/view, log/view: pure formatter functions, return { data, hint } - org/list, trace/list: return structured data with human formatters - Add jsonTransform to OutputConfig for custom JSON envelopes (trace/list) - Widen buildListCommand to accept OutputConfig alongside "json" Consola logging: - auth/login, logout, status: logger.withTag("auth.*"), log.info/success/warn - cli/feedback, setup, fix, upgrade: logger.withTag("cli.*") - api.ts: remove stderr: Writer param from 4 helper functions Infrastructure: - OutputConfig<T> gains jsonTransform property for envelope wrapping - ListCommandFunction type allows unknown returns All tests updated to spy on process.stderr.write (consola target) instead of mock context stdout/stderr. 26 files changed across src and test.
1 parent fc90123 commit 8dcba68

File tree

26 files changed

+1681
-1252
lines changed

26 files changed

+1681
-1252
lines changed

src/commands/api.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { OutputError, ValidationError } from "../lib/errors.js";
1212
import { validateEndpoint } from "../lib/input-validation.js";
1313
import { logger } from "../lib/logger.js";
1414
import { getDefaultSdkConfig } from "../lib/sentry-client.js";
15-
import type { Writer } from "../types/index.js";
15+
16+
const log = logger.withTag("api");
1617

1718
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
1819

@@ -309,7 +310,7 @@ export function setNestedValue(
309310

310311
/**
311312
* Auto-correct fields that use ':' instead of '=' as the separator, and warn
312-
* the user on stderr.
313+
* the user via the module logger.
313314
*
314315
* This recovers from a common mistake where users write Sentry search-query
315316
* style syntax (`-F status:resolved`) instead of the required key=value form
@@ -325,14 +326,12 @@ export function setNestedValue(
325326
* downstream parser can throw its normal error.
326327
*
327328
* @param fields - Raw field strings from --field or --raw-field flags
328-
* @param stderr - Writer to emit warnings on (command's stderr)
329329
* @returns New array with corrected field strings (or the original array if no
330330
* corrections were needed)
331331
* @internal Exported for testing
332332
*/
333333
export function normalizeFields(
334-
fields: string[] | undefined,
335-
stderr: Writer
334+
fields: string[] | undefined
336335
): string[] | undefined {
337336
if (!fields || fields.length === 0) {
338337
return fields;
@@ -358,8 +357,8 @@ export function normalizeFields(
358357
const key = field.substring(0, colonIndex);
359358
const value = field.substring(colonIndex + 1);
360359
const corrected = `${key}=${value}`;
361-
stderr.write(
362-
`warning: field '${field}' looks like it uses ':' instead of '=' — interpreting as '${corrected}'\n`
360+
log.warn(
361+
`field '${field}' looks like it uses ':' instead of '=' — interpreting as '${corrected}'`
363362
);
364363
return corrected;
365364
}
@@ -742,10 +741,10 @@ function tryParseJsonField(
742741
* was empty/undefined.
743742
* @internal Exported for testing
744743
*/
745-
export function extractJsonBody(
746-
fields: string[] | undefined,
747-
stderr: Writer
748-
): { body?: Record<string, unknown> | unknown[]; remaining?: string[] } {
744+
export function extractJsonBody(fields: string[] | undefined): {
745+
body?: Record<string, unknown> | unknown[];
746+
remaining?: string[];
747+
} {
749748
if (!fields || fields.length === 0) {
750749
return {};
751750
}
@@ -771,9 +770,8 @@ export function extractJsonBody(
771770

772771
jsonBody = parsed;
773772
const preview = field.length > 60 ? `${field.substring(0, 57)}...` : field;
774-
stderr.write(
775-
`hint: '${preview}' was used as the request body. ` +
776-
"Use --data/-d to pass inline JSON next time.\n"
773+
log.info(
774+
`'${preview}' was used as the request body. Use --data/-d to pass inline JSON next time.`
777775
);
778776
}
779777

@@ -913,28 +911,27 @@ export function resolveEffectiveHeaders(
913911
* Build body and params from field flags, auto-detecting bare JSON bodies.
914912
*
915913
* Runs colon-to-equals normalization, extracts any JSON body passed as a
916-
* field value (with a stderr hint about `--data`), and routes the remaining
914+
* field value (with a logged hint about `--data`), and routes the remaining
917915
* fields to body or query params based on the HTTP method.
918916
*
919917
* @internal Exported for testing
920918
*/
921919
export function buildFromFields(
922920
method: HttpMethod,
923-
flags: Pick<ApiFlags, "field" | "raw-field">,
924-
stderr: Writer
921+
flags: Pick<ApiFlags, "field" | "raw-field">
925922
): {
926923
body?: Record<string, unknown> | unknown[];
927924
params?: Record<string, string | string[]>;
928925
} {
929-
const field = normalizeFields(flags.field, stderr);
930-
let rawField = normalizeFields(flags["raw-field"], stderr);
926+
const field = normalizeFields(flags.field);
927+
let rawField = normalizeFields(flags["raw-field"]);
931928

932929
// Auto-detect bare JSON passed as a field value (common mistake).
933930
// GET requests don't have a body — skip detection so JSON-shaped values
934931
// fall through to query-param routing (which will throw a clear error).
935932
let body: Record<string, unknown> | unknown[] | undefined;
936933
if (method !== "GET") {
937-
const extracted = extractJsonBody(rawField, stderr);
934+
const extracted = extractJsonBody(rawField);
938935
body = extracted.body;
939936
rawField = extracted.remaining;
940937
}
@@ -986,8 +983,7 @@ export function buildFromFields(
986983
*/
987984
export async function resolveBody(
988985
flags: Pick<ApiFlags, "method" | "data" | "input" | "field" | "raw-field">,
989-
stdin: NodeJS.ReadStream & { fd: 0 },
990-
stderr: Writer
986+
stdin: NodeJS.ReadStream & { fd: 0 }
991987
): Promise<{
992988
body?: Record<string, unknown> | unknown[] | string;
993989
params?: Record<string, string | string[]>;
@@ -1026,13 +1022,11 @@ export async function resolveBody(
10261022
return { body: await buildBodyFromInput(flags.input, stdin) };
10271023
}
10281024

1029-
return buildFromFields(flags.method, flags, stderr);
1025+
return buildFromFields(flags.method, flags);
10301026
}
10311027

10321028
// Command Definition
10331029

1034-
const log = logger.withTag("api");
1035-
10361030
/** Log outgoing request details in `> ` curl-verbose style. */
10371031
function logRequest(
10381032
method: string,
@@ -1162,10 +1156,10 @@ export const apiCommand = buildCommand({
11621156
},
11631157
},
11641158
async func(this: SentryContext, flags: ApiFlags, endpoint: string) {
1165-
const { stderr, stdin } = this;
1159+
const { stdin } = this;
11661160

11671161
const normalizedEndpoint = normalizeEndpoint(endpoint);
1168-
const { body, params } = await resolveBody(flags, stdin, stderr);
1162+
const { body, params } = await resolveBody(flags, stdin);
11691163

11701164
const headers =
11711165
flags.header && flags.header.length > 0

src/commands/auth/login.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
import { getDbPath } from "../../lib/db/index.js";
1212
import { setUserInfo } from "../../lib/db/user.js";
1313
import { AuthError } from "../../lib/errors.js";
14-
import { muted, success } from "../../lib/formatters/colors.js";
1514
import { formatUserIdentity } from "../../lib/formatters/human.js";
1615
import { runInteractiveLogin } from "../../lib/interactive-login.js";
16+
import { logger } from "../../lib/logger.js";
1717
import { clearResponseCache } from "../../lib/response-cache.js";
1818

19+
const log = logger.withTag("auth.login");
20+
1921
type LoginFlags = {
2022
readonly token?: string;
2123
readonly timeout: number;
@@ -47,19 +49,17 @@ export const loginCommand = buildCommand({
4749
},
4850
},
4951
async func(this: SentryContext, flags: LoginFlags): Promise<void> {
50-
const { stdout, stderr } = this;
51-
5252
// Check if already authenticated
5353
if (await isAuthenticated()) {
5454
if (isEnvTokenActive()) {
5555
const envVar = getActiveEnvVarName();
56-
stdout.write(
56+
log.info(
5757
`Authentication is provided via ${envVar} environment variable. ` +
58-
`Unset ${envVar} to use OAuth-based login instead.\n`
58+
`Unset ${envVar} to use OAuth-based login instead.`
5959
);
6060
} else {
61-
stdout.write(
62-
"You are already authenticated. Use 'sentry auth logout' first to re-authenticate.\n"
61+
log.info(
62+
"You are already authenticated. Use 'sentry auth logout' first to re-authenticate."
6363
);
6464
}
6565
return;
@@ -104,11 +104,11 @@ export const loginCommand = buildCommand({
104104
// Non-fatal: user info is supplementary. Token remains stored and valid.
105105
}
106106

107-
stdout.write(`${success("✓")} Authenticated with API token\n`);
107+
log.success("Authenticated with API token");
108108
if (user) {
109-
stdout.write(` Logged in as: ${muted(formatUserIdentity(user))}\n`);
109+
log.info(`Logged in as: ${formatUserIdentity(user)}`);
110110
}
111-
stdout.write(` Config saved to: ${getDbPath()}\n`);
111+
log.info(`Config saved to: ${getDbPath()}`);
112112
return;
113113
}
114114

@@ -119,7 +119,8 @@ export const loginCommand = buildCommand({
119119
// Non-fatal: cache directory may not exist
120120
}
121121

122-
// Device Flow OAuth
122+
// Device Flow OAuth — still needs raw stdout/stderr Writers
123+
const { stdout, stderr } = this;
123124
const loginSuccess = await runInteractiveLogin(
124125
stdout,
125126
stderr,

src/commands/auth/logout.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
isEnvTokenActive,
1414
} from "../../lib/db/auth.js";
1515
import { getDbPath } from "../../lib/db/index.js";
16-
import { success } from "../../lib/formatters/colors.js";
16+
import { logger } from "../../lib/logger.js";
17+
18+
const log = logger.withTag("auth.logout");
1719

1820
export const logoutCommand = buildCommand({
1921
docs: {
@@ -25,24 +27,22 @@ export const logoutCommand = buildCommand({
2527
flags: {},
2628
},
2729
async func(this: SentryContext): Promise<void> {
28-
const { stdout } = this;
29-
3030
if (!(await isAuthenticated())) {
31-
stdout.write("Not currently authenticated.\n");
31+
log.info("Not currently authenticated.");
3232
return;
3333
}
3434

3535
if (isEnvTokenActive()) {
3636
const envVar = getActiveEnvVarName();
37-
stdout.write(
37+
log.info(
3838
`Authentication is provided via ${envVar} environment variable.\n` +
39-
`Unset ${envVar} to log out.\n`
39+
`Unset ${envVar} to log out.`
4040
);
4141
return;
4242
}
4343

4444
await clearAuth();
45-
stdout.write(`${success("✓")} Logged out successfully.\n`);
46-
stdout.write(` Credentials removed from: ${getDbPath()}\n`);
45+
log.success("Logged out successfully.");
46+
log.info(`Credentials removed from: ${getDbPath()}`);
4747
},
4848
});

0 commit comments

Comments
 (0)