Skip to content

Commit 013629e

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 formatter. Two forms of output config: - "json" (string) — flag injection only (--json, --fields) - { json: true, human: fn } — flag injection + auto-render Migrated 4 Category A commands to return-based pattern: - auth/whoami: returns user - 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 Eliminated jsonData — one canonical data object for JSON and human output. Normalized JSON shapes for consistent jq ergonomics: - issue/view: always includes event (null when absent) - log/view: always emits array - project/view: always emits array Renames detectedFrom → hint (callers compose full message). Removes OutputResult/output()/isOutputResult()/renderOutputResult() and WriteOutputDivergentOptions/jsonData from all output APIs.
1 parent 23b4f98 commit 013629e

File tree

14 files changed

+756
-305
lines changed

14 files changed

+756
-305
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: 7 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,18 @@ 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+
},
3740
parameters: {
3841
flags: {
3942
fresh: FRESH_FLAG,
4043
},
4144
aliases: FRESH_ALIASES,
4245
},
43-
async func(this: SentryContext, flags: WhoamiFlags): Promise<void> {
46+
async func(this: SentryContext, flags: WhoamiFlags) {
4447
applyFreshFlag(flags);
45-
const { stdout } = this;
4648

4749
if (!(await isAuthenticated())) {
4850
throw new AuthError("not_authenticated");
@@ -63,16 +65,6 @@ export const whoamiCommand = buildCommand({
6365
// Cache update failure is non-essential — user identity was already fetched.
6466
}
6567

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-
});
68+
return user;
7769
},
7870
});

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/issue/view.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,7 @@ export const viewCommand = buildCommand({
154154
const trace = spanTreeResult?.success
155155
? { traceId: spanTreeResult.traceId, spans: spanTreeResult.spans }
156156
: null;
157-
const output = event ? { issue, event, trace } : { issue, trace };
158-
writeJson(stdout, output, flags.fields);
157+
writeJson(stdout, { issue, event: event ?? null, trace }, flags.fields);
159158
return;
160159
}
161160

src/commands/log/view.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,7 @@ export const viewCommand = buildCommand({
383383
warnMissingIds(logIds, logs);
384384

385385
if (flags.json) {
386-
// Single ID: output single object for backward compatibility
387-
// Multiple IDs: output array
388-
if (logIds.length === 1 && logs.length === 1) {
389-
writeJson(stdout, logs[0], flags.fields);
390-
} else {
391-
writeJson(stdout, logs, flags.fields);
392-
}
386+
writeJson(stdout, logs, flags.fields);
393387
return;
394388
}
395389

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: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,14 @@ export const viewCommand = buildCommand({
284284
throw buildContextError();
285285
}
286286

287-
// JSON output - array if multiple, single object if one
287+
// JSON output - always an array for consistent shape
288288
if (flags.json) {
289289
// Add dsn field to JSON output
290290
const projectsWithDsn = projects.map((p, i) => ({
291291
...p,
292292
dsn: dsns[i] ?? null,
293293
}));
294-
writeJson(
295-
stdout,
296-
projectsWithDsn.length === 1 ? projectsWithDsn[0] : projectsWithDsn,
297-
flags.fields
298-
);
294+
writeJson(stdout, projectsWithDsn, flags.fields);
299295
return;
300296
}
301297

@@ -307,7 +303,9 @@ export const viewCommand = buildCommand({
307303
writeOutput(stdout, firstProject, {
308304
json: false,
309305
formatHuman: (p: SentryProject) => formatProjectDetails(p, firstDsn),
310-
detectedFrom: firstTarget?.detectedFrom,
306+
hint: firstTarget?.detectedFrom
307+
? `Detected from ${firstTarget.detectedFrom}`
308+
: undefined,
311309
});
312310
} else {
313311
writeMultipleProjects(stdout, projects, dsns, targets);

0 commit comments

Comments
 (0)