Skip to content

Commit 5e76d00

Browse files
committed
refactor: remove stdout/stderr plumbing from commands
Commands no longer receive or pass stdout/stderr Writer references. Interactive and diagnostic output now routes through the project's consola logger (→ stderr), keeping stdout reserved for structured command output. Changes: - org-list.ts: remove stdout from HandlerContext and DispatchOptions - list-command.ts, project/list.ts, issue/list.ts, trace/logs.ts: stop threading stdout to dispatchOrgScopedList - issue/list.ts: convert partial-failure stderr.write to logger.warn - log/list.ts: convert follow-mode banner and onDiagnostic to logger - auth/login.ts, interactive-login.ts: remove stdout/stderr params, use logger for all UI output (QR code, URLs, progress dots) - clipboard.ts: remove stdout param from setupCopyKeyListener - trial/start.ts: use logger for billing URL and QR code display - help.ts: printCustomHelp returns string, caller writes to process.stdout - formatters/log.ts: rename displayTraceLogs → formatTraceLogs (returns string) - formatters/output.ts: extract formatFooter helper from writeFooter Remaining stdout usage: - auth/token.ts: intentional raw stdout for pipe compatibility - help.ts: process.stdout.write for help text (like git --help) - trace/logs.ts: process.stdout.write (pending OutputConfig migration)
1 parent 13047a5 commit 5e76d00

File tree

21 files changed

+278
-283
lines changed

21 files changed

+278
-283
lines changed

AGENTS.md

Lines changed: 20 additions & 50 deletions
Large diffs are not rendered by default.

src/bin.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,7 @@ const autoAuthMiddleware: ErrorMiddleware = async (next, args) => {
103103
: "Authentication required. Starting login flow...\n\n"
104104
);
105105

106-
const loginSuccess = await runInteractiveLogin(
107-
process.stdout,
108-
process.stderr,
109-
process.stdin
110-
);
106+
const loginSuccess = await runInteractiveLogin(process.stdin);
111107

112108
if (loginSuccess) {
113109
process.stderr.write("\nRetrying command...\n\n");

src/commands/auth/login.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const loginCommand = buildCommand({
104104
},
105105
},
106106
},
107-
// biome-ignore lint/correctness/useYield: void generator — writes to stdout directly, will be migrated to yield pattern later
107+
// biome-ignore lint/correctness/useYield: void generator — all output goes to stderr via logger, will be migrated to yield pattern later
108108
async *func(this: SentryContext, flags: LoginFlags) {
109109
// Check if already authenticated and handle re-authentication
110110
if (await isAuthenticated()) {
@@ -168,15 +168,9 @@ export const loginCommand = buildCommand({
168168
// Non-fatal: cache directory may not exist
169169
}
170170

171-
const { stdout, stderr } = this;
172-
const loginSuccess = await runInteractiveLogin(
173-
stdout,
174-
stderr,
175-
process.stdin,
176-
{
177-
timeout: flags.timeout * 1000,
178-
}
179-
);
171+
const loginSuccess = await runInteractiveLogin(process.stdin, {
172+
timeout: flags.timeout * 1000,
173+
});
180174

181175
if (!loginSuccess) {
182176
// Error already displayed by runInteractiveLogin - just set exit code

src/commands/help.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,9 @@ export const helpCommand = buildCommand({
3232
// biome-ignore lint/complexity/noBannedTypes: Stricli requires empty object for commands with no flags
3333
// biome-ignore lint/correctness/useYield: void generator — delegates to Stricli help system
3434
async *func(this: SentryContext, _flags: {}, ...commandPath: string[]) {
35-
const { stdout } = this;
36-
3735
// No args: show branded help
3836
if (commandPath.length === 0) {
39-
await printCustomHelp(stdout);
37+
process.stdout.write(await printCustomHelp());
4038
return;
4139
}
4240

src/commands/issue/list.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {
3838
} from "../../lib/errors.js";
3939
import {
4040
type IssueTableRow,
41-
muted,
4241
shouldAutoCompact,
4342
writeIssueTable,
4443
} from "../../lib/formatters/index.js";
@@ -57,6 +56,7 @@ import {
5756
parseCursorFlag,
5857
targetPatternExplanation,
5958
} from "../../lib/list-command.js";
59+
import { logger } from "../../lib/logger.js";
6060
import {
6161
dispatchOrgScopedList,
6262
jsonTransformListResult,
@@ -871,7 +871,6 @@ async function handleOrgAllIssues(
871871

872872
/** Options for {@link handleResolvedTargets}. */
873873
type ResolvedTargetsOptions = {
874-
stderr: Writer;
875874
parsed: ReturnType<typeof parseOrgProjectArg>;
876875
flags: ListFlags;
877876
cwd: string;
@@ -890,7 +889,7 @@ type ResolvedTargetsOptions = {
890889
async function handleResolvedTargets(
891890
options: ResolvedTargetsOptions
892891
): Promise<IssueListResult> {
893-
const { stderr, parsed, flags, cwd, setContext } = options;
892+
const { parsed, flags, cwd, setContext } = options;
894893

895894
const { targets, footer, skippedSelfHosted, detectedDsns } =
896895
await resolveTargetsFromParsedArg(parsed, cwd);
@@ -1094,10 +1093,8 @@ async function handleResolvedTargets(
10941093
const failedNames = failures
10951094
.map(({ target: t }) => `${t.org}/${t.project}`)
10961095
.join(", ");
1097-
stderr.write(
1098-
muted(
1099-
`\nNote: Failed to fetch issues from ${failedNames}. Showing results from ${validResults.length} project(s).\n`
1100-
)
1096+
logger.warn(
1097+
`Failed to fetch issues from ${failedNames}. Showing results from ${validResults.length} project(s).`
11011098
);
11021099
}
11031100

@@ -1318,7 +1315,7 @@ export const listCommand = buildListCommand("issue", {
13181315
},
13191316
async *func(this: SentryContext, flags: ListFlags, target?: string) {
13201317
applyFreshFlag(flags);
1321-
const { stdout, stderr, cwd, setContext } = this;
1318+
const { cwd, setContext } = this;
13221319

13231320
const parsed = parseOrgProjectArg(target);
13241321

@@ -1341,13 +1338,11 @@ export const listCommand = buildListCommand("issue", {
13411338
handleResolvedTargets({
13421339
...ctx,
13431340
flags,
1344-
stderr,
13451341
setContext,
13461342
});
13471343

13481344
const result = (await dispatchOrgScopedList({
13491345
config: issueListMeta,
1350-
stdout,
13511346
cwd,
13521347
flags,
13531348
parsed,

src/commands/log/list.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ import {
3333
FRESH_FLAG,
3434
TARGET_PATTERN_NOTE,
3535
} from "../../lib/list-command.js";
36+
import { logger } from "../../lib/logger.js";
3637
import {
3738
resolveOrg,
3839
resolveOrgProjectFromArg,
3940
} from "../../lib/resolve-target.js";
4041
import { validateTraceId } from "../../lib/trace-id.js";
4142
import { getUpdateNotification } from "../../lib/version-check.js";
42-
import type { Writer } from "../../types/index.js";
4343

4444
type ListFlags = {
4545
readonly limit: number;
@@ -227,13 +227,13 @@ function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
227227
*
228228
* Unlike the old callback-based approach, this does NOT include
229229
* stdout/stderr. All stdout output flows through yielded chunks;
230-
* stderr diagnostics use the `onDiagnostic` callback.
230+
* diagnostics are reported via the `onDiagnostic` callback.
231231
*/
232232
type FollowGeneratorConfig<T extends LogLike> = {
233233
flags: ListFlags;
234234
/** Whether to show the trace-ID column in table output */
235235
includeTrace: boolean;
236-
/** Report diagnostic/error messages (caller writes to stderr) */
236+
/** Report diagnostic/error messages (caller logs via logger) */
237237
onDiagnostic: (message: string) => void;
238238
/**
239239
* Fetch logs with the given time window.
@@ -321,8 +321,8 @@ async function fetchPoll<T extends LogLike>(
321321
* - `data` chunks contain raw log arrays for JSONL serialization
322322
*
323323
* The generator handles SIGINT via AbortController for clean shutdown.
324-
* It never touches stdout/stderr directly — all output flows through
325-
* yielded chunks and the `onDiagnostic` callback.
324+
* It never touches stdout directly — all data output flows through
325+
* yielded chunks and diagnostics use the `onDiagnostic` callback.
326326
*
327327
* @throws {AuthError} if the API returns an authentication error
328328
*/
@@ -455,25 +455,20 @@ async function executeTraceSingleFetch(
455455
}
456456

457457
/**
458-
* Write the follow-mode banner to stderr. Suppressed in JSON mode.
458+
* Write the follow-mode banner via logger. Suppressed in JSON mode.
459459
* Includes poll interval, Ctrl+C hint, and update notification.
460460
*/
461-
function writeFollowBanner(
462-
stderr: Writer,
463-
flags: ListFlags,
464-
bannerText: string
465-
): void {
461+
function writeFollowBanner(flags: ListFlags, bannerText: string): void {
466462
if (flags.json) {
467463
return;
468464
}
469465
const pollInterval = flags.follow ?? DEFAULT_POLL_INTERVAL;
470-
stderr.write(`${bannerText} (poll interval: ${pollInterval}s)\n`);
471-
stderr.write("Press Ctrl+C to stop.\n");
466+
logger.info(`${bannerText} (poll interval: ${pollInterval}s)`);
467+
logger.info("Press Ctrl+C to stop.");
472468
const notification = getUpdateNotification();
473469
if (notification) {
474-
stderr.write(notification);
470+
logger.info(notification);
475471
}
476-
stderr.write("\n");
477472
}
478473

479474
// ---------------------------------------------------------------------------
@@ -631,23 +626,18 @@ export const listCommand = buildListCommand("log", {
631626
setContext([org], []);
632627

633628
if (flags.follow) {
634-
const { stderr } = this;
635629
const traceId = flags.trace;
636630

637-
// Banner (stderr, suppressed in JSON mode)
638-
writeFollowBanner(
639-
stderr,
640-
flags,
641-
`Streaming logs for trace ${traceId}...`
642-
);
631+
// Banner (suppressed in JSON mode)
632+
writeFollowBanner(flags, `Streaming logs for trace ${traceId}...`);
643633

644634
// Track IDs of logs seen without timestamp_precise so they are
645635
// shown once but not duplicated on subsequent polls.
646636
const seenWithoutTs = new Set<string>();
647637
const generator = generateFollowLogs({
648638
flags,
649639
includeTrace: false,
650-
onDiagnostic: (msg) => stderr.write(msg),
640+
onDiagnostic: (msg) => logger.warn(msg),
651641
fetch: (statsPeriod) =>
652642
listTraceLogs(org, traceId, {
653643
query: flags.query,
@@ -704,14 +694,12 @@ export const listCommand = buildListCommand("log", {
704694
setContext([org], [project]);
705695

706696
if (flags.follow) {
707-
const { stderr } = this;
708-
709-
writeFollowBanner(stderr, flags, "Streaming logs...");
697+
writeFollowBanner(flags, "Streaming logs...");
710698

711699
const generator = generateFollowLogs({
712700
flags,
713701
includeTrace: true,
714-
onDiagnostic: (msg) => stderr.write(msg),
702+
onDiagnostic: (msg) => logger.warn(msg),
715703
fetch: (statsPeriod, afterTimestamp) =>
716704
listLogs(org, project, {
717705
query: flags.query,

src/commands/project/list.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,13 +594,12 @@ export const listCommand = buildListCommand("project", {
594594
},
595595
async *func(this: SentryContext, flags: ListFlags, target?: string) {
596596
applyFreshFlag(flags);
597-
const { stdout, cwd } = this;
597+
const { cwd } = this;
598598

599599
const parsed = parseOrgProjectArg(target);
600600

601601
const result = await dispatchOrgScopedList({
602602
config: projectListMeta,
603-
stdout,
604603
cwd,
605604
flags,
606605
parsed,

src/commands/trace/logs.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { validateLimit } from "../../lib/arg-parsing.js";
1010
import { openInBrowser } from "../../lib/browser.js";
1111
import { buildCommand } from "../../lib/command.js";
1212
import { ContextError } from "../../lib/errors.js";
13-
import { displayTraceLogs } from "../../lib/formatters/index.js";
13+
import { formatTraceLogs } from "../../lib/formatters/index.js";
1414
import {
1515
applyFreshFlag,
1616
FRESH_ALIASES,
@@ -176,10 +176,10 @@ export const logsCommand = buildCommand({
176176
q: "query",
177177
},
178178
},
179-
// biome-ignore lint/correctness/useYield: void generator — writes to stdout directly, will be migrated to yield pattern later
179+
// biome-ignore lint/correctness/useYield: void generator — early returns for web mode
180180
async *func(this: SentryContext, flags: LogsFlags, ...args: string[]) {
181181
applyFreshFlag(flags);
182-
const { stdout, cwd, setContext } = this;
182+
const { cwd, setContext } = this;
183183

184184
const { traceId, orgArg } = parsePositionalArgs(args);
185185

@@ -206,16 +206,17 @@ export const logsCommand = buildCommand({
206206
query: flags.query,
207207
});
208208

209-
displayTraceLogs({
210-
stdout,
211-
logs,
212-
traceId,
213-
limit: flags.limit,
214-
asJson: flags.json,
215-
fields: flags.fields,
216-
emptyMessage:
217-
`No logs found for trace ${traceId} in the last ${flags.period}.\n\n` +
218-
`Try a longer period: sentry trace logs --period 30d ${traceId}\n`,
219-
});
209+
process.stdout.write(
210+
formatTraceLogs({
211+
logs,
212+
traceId,
213+
limit: flags.limit,
214+
asJson: flags.json,
215+
fields: flags.fields,
216+
emptyMessage:
217+
`No logs found for trace ${traceId} in the last ${flags.period}.\n\n` +
218+
`Try a longer period: sentry trace logs --period 30d ${traceId}\n`,
219+
})
220+
);
220221
},
221222
});

src/commands/trial/start.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,7 @@ export const startCommand = buildCommand({
143143

144144
// Plan trial: no API to start it — open billing page instead
145145
if (parsed.name === "plan") {
146-
const planResult = await handlePlanTrial(
147-
orgSlug,
148-
this.stdout,
149-
flags.json ?? false
150-
);
146+
const planResult = await handlePlanTrial(orgSlug, flags.json ?? false);
151147
yield commandOutput(planResult);
152148
return;
153149
}
@@ -181,19 +177,19 @@ export const startCommand = buildCommand({
181177
/**
182178
* Show URL + QR code and prompt to open browser if interactive.
183179
*
180+
* Display text goes to stderr via consola — stdout is reserved for
181+
* structured command output.
182+
*
184183
* @returns true if browser was opened, false otherwise
185184
*/
186-
async function promptOpenBillingUrl(
187-
url: string,
188-
stdout: { write: (s: string) => unknown }
189-
): Promise<boolean> {
185+
async function promptOpenBillingUrl(url: string): Promise<boolean> {
190186
const log = logger.withTag("trial");
191187

192-
stdout.write(`\n ${url}\n\n`);
188+
log.log(`\n ${url}\n`);
193189

194190
// Show QR code so mobile/remote users can scan
195191
const qr = await generateQRCode(url);
196-
stdout.write(`${qr}\n`);
192+
log.log(qr);
197193

198194
// Prompt to open browser if interactive TTY
199195
if (isatty(0) && isatty(1)) {
@@ -236,7 +232,6 @@ type PlanTrialResult = {
236232
*/
237233
async function handlePlanTrial(
238234
orgSlug: string,
239-
stdout: { write: (s: string) => unknown },
240235
json: boolean
241236
): Promise<PlanTrialResult> {
242237
const log = logger.withTag("trial");
@@ -269,7 +264,7 @@ async function handlePlanTrial(
269264
log.info(
270265
`The ${currentPlan} → Business plan trial must be activated in the Sentry UI.`
271266
);
272-
opened = await promptOpenBillingUrl(url, stdout);
267+
opened = await promptOpenBillingUrl(url);
273268
}
274269

275270
return {

0 commit comments

Comments
 (0)