Skip to content

Commit 23b4f98

Browse files
authored
refactor: converge Tier 1 commands to writeOutput helper (#376)
## Phase 2: `writeOutput` convergence Builds on #373 (`output: "json"` centralization). Enhance `writeOutput` with two new options and migrate 3 Tier 1 commands to use it. ### New options on `writeOutput` - **`footer?: string`** — muted hint after human output, suppressed in JSON mode. Replaces separate `writeFooter()` calls. - **`jsonData?: J`** — separate data object for JSON when the serialized shape differs from the human formatter's input. ### Commands migrated | Command | What changed | |---------|-------------| | `auth/whoami` | `jsonData` narrows user to `{id, name, username, email}` for JSON | | `auth/refresh` | Inline `formatHuman` replaces 3-branch if/else | | `issue/explain` | `footer` replaces separate `writeFooter()` call | Commands with multi-part output or divergent data assembly (`issue/view`, `event/view`, `trace/view`, `project/create`, `log/view`) are intentionally left as-is — they don't fit the single-formatter pattern. ### Tests 16 new tests in `test/lib/formatters/output.test.ts` cover JSON mode, human mode, footer, detectedFrom, and jsonData divergence.
1 parent 081b94c commit 23b4f98

File tree

5 files changed

+314
-48
lines changed

5 files changed

+314
-48
lines changed

src/commands/auth/refresh.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ 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 { writeJson } from "../../lib/formatters/index.js";
18+
import { writeOutput } from "../../lib/formatters/index.js";
1919

2020
type RefreshFlags = {
2121
readonly json: boolean;
@@ -100,17 +100,13 @@ Examples:
100100
: undefined,
101101
};
102102

103-
if (flags.json) {
104-
writeJson(stdout, output, flags.fields);
105-
} else if (result.refreshed) {
106-
stdout.write(
107-
`${success("✓")} Token refreshed successfully. Expires in ${formatDuration(result.expiresIn ?? 0)}.\n`
108-
);
109-
} else {
110-
stdout.write(
111-
`Token still valid (expires in ${formatDuration(result.expiresIn ?? 0)}).\n` +
112-
"Use --force to refresh anyway.\n"
113-
);
114-
}
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+
});
115111
},
116112
});

src/commands/auth/whoami.ts

Lines changed: 12 additions & 16 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, writeJson } from "../../lib/formatters/index.js";
15+
import { formatUserIdentity, writeOutput } from "../../lib/formatters/index.js";
1616
import {
1717
applyFreshFlag,
1818
FRESH_ALIASES,
@@ -63,20 +63,16 @@ export const whoamiCommand = buildCommand({
6363
// Cache update failure is non-essential — user identity was already fetched.
6464
}
6565

66-
if (flags.json) {
67-
writeJson(
68-
stdout,
69-
{
70-
id: user.id,
71-
name: user.name ?? null,
72-
username: user.username ?? null,
73-
email: user.email ?? null,
74-
},
75-
flags.fields
76-
);
77-
return;
78-
}
79-
80-
stdout.write(`${formatUserIdentity(user)}\n`);
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+
});
8177
},
8278
});

src/commands/issue/explain.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import type { SentryContext } from "../../context.js";
88
import { buildCommand } from "../../lib/command.js";
99
import { ApiError } from "../../lib/errors.js";
10-
import { writeFooter, writeJson } from "../../lib/formatters/index.js";
10+
import { writeOutput } from "../../lib/formatters/index.js";
1111
import {
1212
formatRootCauseList,
1313
handleSeerApiError,
@@ -111,17 +111,12 @@ export const explainCommand = buildCommand({
111111
}
112112

113113
// Output results
114-
if (flags.json) {
115-
writeJson(stdout, causes, flags.fields);
116-
return;
117-
}
118-
119-
// Human-readable output
120-
stdout.write(`${formatRootCauseList(causes)}\n`);
121-
writeFooter(
122-
stdout,
123-
`To create a plan, run: sentry issue plan ${issueArg}`
124-
);
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+
});
125120
} catch (error) {
126121
// Handle API errors with friendly messages
127122
if (error instanceof ApiError) {

src/lib/formatters/output.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import type { Writer } from "../../types/index.js";
99
import { muted } from "./colors.js";
1010
import { writeJson } from "./json.js";
1111

12+
/**
13+
* Options for {@link writeOutput} when JSON and human data share the same type.
14+
*
15+
* Most commands fetch data and then either serialize it to JSON or format it
16+
* for the terminal — use this form when the same object works for both paths.
17+
*/
1218
type WriteOutputOptions<T> = {
1319
/** Output JSON format instead of human-readable */
1420
json: boolean;
@@ -18,23 +24,52 @@ type WriteOutputOptions<T> = {
1824
formatHuman: (data: T) => string;
1925
/** Optional source description if data was auto-detected */
2026
detectedFrom?: string;
27+
/** Footer hint shown after human output (suppressed in JSON mode) */
28+
footer?: string;
29+
};
30+
31+
/**
32+
* Options for {@link writeOutput} when JSON needs a different data shape.
33+
*
34+
* Some commands build a richer or narrower object for JSON than the one
35+
* the human formatter receives. Supply `jsonData` to decouple the two.
36+
*
37+
* @typeParam T - Type of data used by the human formatter
38+
* @typeParam J - Type of data serialized to JSON (defaults to T)
39+
*/
40+
type WriteOutputDivergentOptions<T, J> = WriteOutputOptions<T> & {
41+
/**
42+
* Separate data object to serialize when `json: true`.
43+
* When provided, `data` is only used by `formatHuman` and
44+
* `jsonData` is passed to `writeJson`.
45+
*/
46+
jsonData: J;
2147
};
2248

2349
/**
2450
* Write formatted output to stdout based on output format.
25-
* Handles the common JSON vs human-readable pattern used across commands.
2651
*
27-
* @param stdout - Writer to output to
28-
* @param data - Data to output
29-
* @param options - Output options including format and formatters
52+
* Handles the common JSON-vs-human pattern used across commands:
53+
* - JSON mode: serialize data (or `jsonData` if provided) with optional field filtering
54+
* - Human mode: call `formatHuman`, then optionally print `detectedFrom` and `footer`
55+
*
56+
* When JSON and human paths need different data shapes, pass `jsonData`:
57+
* ```ts
58+
* writeOutput(stdout, fullUser, {
59+
* json: true,
60+
* jsonData: { id: fullUser.id, email: fullUser.email },
61+
* formatHuman: formatUserIdentity,
62+
* });
63+
* ```
3064
*/
31-
export function writeOutput<T>(
65+
export function writeOutput<T, J = T>(
3266
stdout: Writer,
3367
data: T,
34-
options: WriteOutputOptions<T>
68+
options: WriteOutputOptions<T> | WriteOutputDivergentOptions<T, J>
3569
): void {
3670
if (options.json) {
37-
writeJson(stdout, data, options.fields);
71+
const jsonPayload = "jsonData" in options ? options.jsonData : data;
72+
writeJson(stdout, jsonPayload, options.fields);
3873
return;
3974
}
4075

@@ -44,6 +79,10 @@ export function writeOutput<T>(
4479
if (options.detectedFrom) {
4580
stdout.write(`\nDetected from ${options.detectedFrom}\n`);
4681
}
82+
83+
if (options.footer) {
84+
writeFooter(stdout, options.footer);
85+
}
4786
}
4887

4988
/**

0 commit comments

Comments
 (0)