Skip to content

Commit 2b8debf

Browse files
committed
feat: return-based output with OutputConfig on buildCommand
Commands with output config return bare data or [data, {hint}] tuples. The buildCommand wrapper intercepts returns and renders automatically using the config's human/jsonData formatters. Two forms of output config: - "json" (string) — flag injection only (--json, --fields) - { json: true, human: fn, jsonData?: fn } — flag injection + auto-render Migrated 4 Category A commands to return-based pattern: - auth/whoami: returns user, jsonData transform in config - auth/refresh: returns payload, formatRefreshResult in config - issue/explain: returns causes with [data, {hint}] for footer - org/view: returns org with optional [data, {hint}] for detection source Replaces OutputResult branded wrapper with simpler bare-return pattern. Renames detectedFrom → hint (callers compose full message). Fixes #377
1 parent 23b4f98 commit 2b8debf

File tree

9 files changed

+799
-197
lines changed

9 files changed

+799
-197
lines changed

src/commands/auth/refresh.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
import { AuthError } from "../../lib/errors.js";
1616
import { success } from "../../lib/formatters/colors.js";
1717
import { formatDuration } from "../../lib/formatters/human.js";
18-
import { writeOutput } from "../../lib/formatters/index.js";
1918

2019
type RefreshFlags = {
2120
readonly json: boolean;
@@ -31,6 +30,13 @@ type RefreshOutput = {
3130
expiresAt?: string;
3231
};
3332

33+
/** Format refresh result for terminal output */
34+
function formatRefreshResult(data: RefreshOutput): string {
35+
return data.refreshed
36+
? `${success("✓")} Token refreshed successfully. Expires in ${formatDuration(data.expiresIn ?? 0)}.`
37+
: `Token still valid (expires in ${formatDuration(data.expiresIn ?? 0)}).\nUse --force to refresh anyway.`;
38+
}
39+
3440
export const refreshCommand = buildCommand({
3541
docs: {
3642
brief: "Refresh your authentication token",
@@ -52,7 +58,7 @@ Examples:
5258
{"success":true,"refreshed":true,"expiresIn":3600,"expiresAt":"..."}
5359
`.trim(),
5460
},
55-
output: "json",
61+
output: { json: true, human: formatRefreshResult },
5662
parameters: {
5763
flags: {
5864
force: {
@@ -62,9 +68,7 @@ Examples:
6268
},
6369
},
6470
},
65-
async func(this: SentryContext, flags: RefreshFlags): Promise<void> {
66-
const { stdout } = this;
67-
71+
async func(this: SentryContext, flags: RefreshFlags) {
6872
// Env var tokens can't be refreshed
6973
if (isEnvTokenActive()) {
7074
const envVar = getActiveEnvVarName();
@@ -88,7 +92,7 @@ Examples:
8892

8993
const result = await refreshToken({ force: flags.force });
9094

91-
const output: RefreshOutput = {
95+
const payload: RefreshOutput = {
9296
success: true,
9397
refreshed: result.refreshed,
9498
message: result.refreshed
@@ -100,13 +104,6 @@ Examples:
100104
: undefined,
101105
};
102106

103-
writeOutput(stdout, output, {
104-
json: flags.json,
105-
fields: flags.fields,
106-
formatHuman: (data) =>
107-
data.refreshed
108-
? `${success("✓")} Token refreshed successfully. Expires in ${formatDuration(data.expiresIn ?? 0)}.`
109-
: `Token still valid (expires in ${formatDuration(data.expiresIn ?? 0)}).\nUse --force to refresh anyway.`,
110-
});
107+
return payload;
111108
},
112109
});

src/commands/auth/whoami.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { buildCommand } from "../../lib/command.js";
1212
import { isAuthenticated } from "../../lib/db/auth.js";
1313
import { setUserInfo } from "../../lib/db/user.js";
1414
import { AuthError } from "../../lib/errors.js";
15-
import { formatUserIdentity, writeOutput } from "../../lib/formatters/index.js";
15+
import { formatUserIdentity } from "../../lib/formatters/index.js";
1616
import {
1717
applyFreshFlag,
1818
FRESH_ALIASES,
@@ -33,16 +33,24 @@ export const whoamiCommand = buildCommand({
3333
"This calls the Sentry API live (not cached) so the result always reflects " +
3434
"the current token. Works with all token types: OAuth, API tokens, and OAuth App tokens.",
3535
},
36-
output: "json",
36+
output: {
37+
json: true,
38+
human: formatUserIdentity,
39+
jsonData: (user) => ({
40+
id: user.id,
41+
name: user.name ?? null,
42+
username: user.username ?? null,
43+
email: user.email ?? null,
44+
}),
45+
},
3746
parameters: {
3847
flags: {
3948
fresh: FRESH_FLAG,
4049
},
4150
aliases: FRESH_ALIASES,
4251
},
43-
async func(this: SentryContext, flags: WhoamiFlags): Promise<void> {
52+
async func(this: SentryContext, flags: WhoamiFlags) {
4453
applyFreshFlag(flags);
45-
const { stdout } = this;
4654

4755
if (!(await isAuthenticated())) {
4856
throw new AuthError("not_authenticated");
@@ -63,16 +71,6 @@ export const whoamiCommand = buildCommand({
6371
// Cache update failure is non-essential — user identity was already fetched.
6472
}
6573

66-
writeOutput(stdout, user, {
67-
json: flags.json,
68-
fields: flags.fields,
69-
jsonData: {
70-
id: user.id,
71-
name: user.name ?? null,
72-
username: user.username ?? null,
73-
email: user.email ?? null,
74-
},
75-
formatHuman: formatUserIdentity,
76-
});
74+
return user;
7775
},
7876
});

src/commands/issue/explain.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import type { SentryContext } from "../../context.js";
88
import { buildCommand } from "../../lib/command.js";
99
import { ApiError } from "../../lib/errors.js";
10-
import { writeOutput } from "../../lib/formatters/index.js";
1110
import {
1211
formatRootCauseList,
1312
handleSeerApiError,
@@ -59,7 +58,7 @@ export const explainCommand = buildCommand({
5958
" sentry issue explain 123456789 --json\n" +
6059
" sentry issue explain 123456789 --force",
6160
},
62-
output: "json",
61+
output: { json: true, human: formatRootCauseList },
6362
parameters: {
6463
positional: issueIdPositional,
6564
flags: {
@@ -72,13 +71,9 @@ export const explainCommand = buildCommand({
7271
},
7372
aliases: FRESH_ALIASES,
7473
},
75-
async func(
76-
this: SentryContext,
77-
flags: ExplainFlags,
78-
issueArg: string
79-
): Promise<void> {
74+
async func(this: SentryContext, flags: ExplainFlags, issueArg: string) {
8075
applyFreshFlag(flags);
81-
const { stdout, stderr, cwd } = this;
76+
const { stderr, cwd } = this;
8277

8378
// Declare org outside try block so it's accessible in catch for error messages
8479
let resolvedOrg: string | undefined;
@@ -110,13 +105,11 @@ export const explainCommand = buildCommand({
110105
);
111106
}
112107

113-
// Output results
114-
writeOutput(stdout, causes, {
115-
json: flags.json,
116-
fields: flags.fields,
117-
formatHuman: formatRootCauseList,
118-
footer: `To create a plan, run: sentry issue plan ${issueArg}`,
119-
});
108+
// Return data with hint for next step
109+
return [
110+
causes,
111+
{ hint: `To create a plan, run: sentry issue plan ${issueArg}` },
112+
];
120113
} catch (error) {
121114
// Handle API errors with friendly messages
122115
if (error instanceof ApiError) {

src/commands/org/view.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getOrganization } from "../../lib/api-client.js";
99
import { openInBrowser } from "../../lib/browser.js";
1010
import { buildCommand } from "../../lib/command.js";
1111
import { ContextError } from "../../lib/errors.js";
12-
import { formatOrgDetails, writeOutput } from "../../lib/formatters/index.js";
12+
import { formatOrgDetails } from "../../lib/formatters/index.js";
1313
import {
1414
applyFreshFlag,
1515
FRESH_ALIASES,
@@ -35,7 +35,7 @@ export const viewCommand = buildCommand({
3535
" 2. Config defaults\n" +
3636
" 3. SENTRY_DSN environment variable or source code detection",
3737
},
38-
output: "json",
38+
output: { json: true, human: formatOrgDetails },
3939
parameters: {
4040
positional: {
4141
kind: "tuple",
@@ -58,11 +58,7 @@ export const viewCommand = buildCommand({
5858
},
5959
aliases: { ...FRESH_ALIASES, w: "web" },
6060
},
61-
async func(
62-
this: SentryContext,
63-
flags: ViewFlags,
64-
orgSlug?: string
65-
): Promise<void> {
61+
async func(this: SentryContext, flags: ViewFlags, orgSlug?: string) {
6662
applyFreshFlag(flags);
6763
const { stdout, cwd } = this;
6864

@@ -79,11 +75,9 @@ export const viewCommand = buildCommand({
7975

8076
const org = await getOrganization(resolved.org);
8177

82-
writeOutput(stdout, org, {
83-
json: flags.json,
84-
fields: flags.fields,
85-
formatHuman: formatOrgDetails,
86-
detectedFrom: resolved.detectedFrom,
87-
});
78+
const hint = resolved.detectedFrom
79+
? `Detected from ${resolved.detectedFrom}`
80+
: undefined;
81+
return hint ? [org, { hint }] : org;
8882
},
8983
});

src/commands/project/view.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ export const viewCommand = buildCommand({
307307
writeOutput(stdout, firstProject, {
308308
json: false,
309309
formatHuman: (p: SentryProject) => formatProjectDetails(p, firstDsn),
310-
detectedFrom: firstTarget?.detectedFrom,
310+
hint: firstTarget?.detectedFrom
311+
? `Detected from ${firstTarget.detectedFrom}`
312+
: undefined,
311313
});
312314
} else {
313315
writeMultipleProjects(stdout, projects, dsns, targets);

0 commit comments

Comments
 (0)