Skip to content

Commit 13047a5

Browse files
committed
refactor: brand CommandOutput with Symbol and move hints to generator return
Three improvements to the command framework's output system: 1. **Brand CommandOutput with Symbol discriminant** - Add COMMAND_OUTPUT_BRAND Symbol to CommandOutput type - Add commandOutput() factory function for creating branded values - Replace duck-typing ('data' in v) with Symbol check in isCommandOutput() - Prevents false positives from raw API responses with 'data' property 2. **Move hints from yield to generator return value** - Add CommandReturn type ({ hint?: string }) for generator return values - Switch from for-await-of to manual .next() iteration to capture return - renderCommandOutput no longer handles hints; wrapper renders post-loop - All commands: yield commandOutput(data) + return { hint } 3. **Eliminate noExplicitAny suppressions in command.ts** - wrappedFunc params: any → Record<string, unknown> / unknown[] - Final Stricli cast: as any → as unknown as StricliBuilderArgs<CONTEXT> - OutputConfig<any> kept with improved variance explanation - renderCommandOutput config param: kept any with contravariance docs All 24 command files migrated to use commandOutput() helper. Tests updated for branded outputs and hint-on-return pattern. 1083 tests pass, 0 fail, 9356 assertions across 38 files.
1 parent ddddcf0 commit 13047a5

File tree

29 files changed

+285
-204
lines changed

29 files changed

+285
-204
lines changed

src/commands/api.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { SentryContext } from "../context.js";
99
import { buildSearchParams, rawApiRequest } from "../lib/api-client.js";
1010
import { buildCommand } from "../lib/command.js";
1111
import { OutputError, ValidationError } from "../lib/errors.js";
12+
import { commandOutput } from "../lib/formatters/output.js";
1213
import { validateEndpoint } from "../lib/input-validation.js";
1314
import { logger } from "../lib/logger.js";
1415
import { getDefaultSdkConfig } from "../lib/sentry-client.js";
@@ -1168,14 +1169,12 @@ export const apiCommand = buildCommand({
11681169

11691170
// Dry-run mode: preview the request that would be sent
11701171
if (flags["dry-run"]) {
1171-
yield {
1172-
data: {
1173-
method: flags.method,
1174-
url: resolveRequestUrl(normalizedEndpoint, params),
1175-
headers: resolveEffectiveHeaders(headers, body),
1176-
body: body ?? null,
1177-
},
1178-
};
1172+
yield commandOutput({
1173+
method: flags.method,
1174+
url: resolveRequestUrl(normalizedEndpoint, params),
1175+
headers: resolveEffectiveHeaders(headers, body),
1176+
body: body ?? null,
1177+
});
11791178
return;
11801179
}
11811180

@@ -1211,7 +1210,7 @@ export const apiCommand = buildCommand({
12111210
throw new OutputError(response.body);
12121211
}
12131212

1214-
yield { data: response.body };
1213+
yield commandOutput(response.body);
12151214
return;
12161215
},
12171216
});

src/commands/auth/logout.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { getDbPath } from "../../lib/db/index.js";
1616
import { AuthError } from "../../lib/errors.js";
1717
import { formatLogoutResult } from "../../lib/formatters/human.js";
18+
import { commandOutput } from "../../lib/formatters/output.js";
1819

1920
/** Structured result of the logout operation */
2021
export type LogoutResult = {
@@ -38,9 +39,10 @@ export const logoutCommand = buildCommand({
3839
},
3940
async *func(this: SentryContext) {
4041
if (!(await isAuthenticated())) {
41-
yield {
42-
data: { loggedOut: false, message: "Not currently authenticated." },
43-
};
42+
yield commandOutput({
43+
loggedOut: false,
44+
message: "Not currently authenticated.",
45+
});
4446
return;
4547
}
4648

@@ -56,12 +58,10 @@ export const logoutCommand = buildCommand({
5658
const configPath = getDbPath();
5759
await clearAuth();
5860

59-
yield {
60-
data: {
61-
loggedOut: true,
62-
configPath,
63-
},
64-
};
61+
yield commandOutput({
62+
loggedOut: true,
63+
configPath,
64+
});
6565
return;
6666
},
6767
});

src/commands/auth/refresh.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +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 { commandOutput } from "../../lib/formatters/output.js";
1819

1920
type RefreshFlags = {
2021
readonly json: boolean;
@@ -104,7 +105,7 @@ Examples:
104105
: undefined,
105106
};
106107

107-
yield { data: payload };
108+
yield commandOutput(payload);
108109
return;
109110
},
110111
});

src/commands/auth/status.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getDbPath } from "../../lib/db/index.js";
2222
import { getUserInfo } from "../../lib/db/user.js";
2323
import { AuthError, stringifyUnknown } from "../../lib/errors.js";
2424
import { formatAuthStatus, maskToken } from "../../lib/formatters/human.js";
25+
import { commandOutput } from "../../lib/formatters/output.js";
2526
import {
2627
applyFreshFlag,
2728
FRESH_ALIASES,
@@ -189,7 +190,7 @@ export const statusCommand = buildCommand({
189190
verification: await verifyCredentials(),
190191
};
191192

192-
yield { data };
193+
yield commandOutput(data);
193194
return;
194195
},
195196
});

src/commands/auth/whoami.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isAuthenticated } from "../../lib/db/auth.js";
1313
import { setUserInfo } from "../../lib/db/user.js";
1414
import { AuthError } from "../../lib/errors.js";
1515
import { formatUserIdentity } from "../../lib/formatters/index.js";
16+
import { commandOutput } from "../../lib/formatters/output.js";
1617
import {
1718
applyFreshFlag,
1819
FRESH_ALIASES,
@@ -65,7 +66,7 @@ export const whoamiCommand = buildCommand({
6566
// Cache update failure is non-essential — user identity was already fetched.
6667
}
6768

68-
yield { data: user };
69+
yield commandOutput(user);
6970
return;
7071
},
7172
});

src/commands/cli/feedback.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { SentryContext } from "../../context.js";
1414
import { buildCommand } from "../../lib/command.js";
1515
import { ConfigError, ValidationError } from "../../lib/errors.js";
1616
import { formatFeedbackResult } from "../../lib/formatters/human.js";
17+
import { commandOutput } from "../../lib/formatters/output.js";
1718

1819
/** Structured result of the feedback submission */
1920
export type FeedbackResult = {
@@ -66,12 +67,10 @@ export const feedbackCommand = buildCommand({
6667
// Flush to ensure feedback is sent before process exits
6768
const sent = await Sentry.flush(3000);
6869

69-
yield {
70-
data: {
71-
sent,
72-
message,
73-
},
74-
};
70+
yield commandOutput({
71+
sent,
72+
message,
73+
});
7574
return;
7675
},
7776
});

src/commands/cli/fix.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "../../lib/db/schema.js";
1818
import { OutputError } from "../../lib/errors.js";
1919
import { formatFixResult } from "../../lib/formatters/human.js";
20+
import { commandOutput } from "../../lib/formatters/output.js";
2021
import { getRealUsername } from "../../lib/utils.js";
2122

2223
type FixFlags = {
@@ -734,7 +735,7 @@ export const fixCommand = buildCommand({
734735
throw new OutputError(result);
735736
}
736737

737-
yield { data: result };
738+
yield commandOutput(result);
738739
return;
739740
},
740741
});

src/commands/cli/upgrade.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from "../../lib/db/release-channel.js";
3232
import { UpgradeError } from "../../lib/errors.js";
3333
import { formatUpgradeResult } from "../../lib/formatters/human.js";
34+
import { commandOutput } from "../../lib/formatters/output.js";
3435
import { logger } from "../../lib/logger.js";
3536
import {
3637
detectInstallationMethod,
@@ -493,7 +494,7 @@ export const upgradeCommand = buildCommand({
493494
flags,
494495
});
495496
if (resolved.kind === "done") {
496-
yield { data: resolved.result };
497+
yield commandOutput(resolved.result);
497498
return;
498499
}
499500

@@ -510,17 +511,15 @@ export const upgradeCommand = buildCommand({
510511
target,
511512
versionArg
512513
);
513-
yield {
514-
data: {
515-
action: downgrade ? "downgraded" : "upgraded",
516-
currentVersion: CLI_VERSION,
517-
targetVersion: target,
518-
channel,
519-
method,
520-
forced: flags.force,
521-
warnings,
522-
} satisfies UpgradeResult,
523-
};
514+
yield commandOutput({
515+
action: downgrade ? "downgraded" : "upgraded",
516+
currentVersion: CLI_VERSION,
517+
targetVersion: target,
518+
channel,
519+
method,
520+
forced: flags.force,
521+
warnings,
522+
} satisfies UpgradeResult);
524523
return;
525524
}
526525

@@ -532,16 +531,14 @@ export const upgradeCommand = buildCommand({
532531
execPath: this.process.execPath,
533532
});
534533

535-
yield {
536-
data: {
537-
action: downgrade ? "downgraded" : "upgraded",
538-
currentVersion: CLI_VERSION,
539-
targetVersion: target,
540-
channel,
541-
method,
542-
forced: flags.force,
543-
} satisfies UpgradeResult,
544-
};
534+
yield commandOutput({
535+
action: downgrade ? "downgraded" : "upgraded",
536+
currentVersion: CLI_VERSION,
537+
targetVersion: target,
538+
channel,
539+
method,
540+
forced: flags.force,
541+
} satisfies UpgradeResult);
545542
return;
546543
},
547544
});

src/commands/event/view.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { openInBrowser } from "../../lib/browser.js";
2323
import { buildCommand } from "../../lib/command.js";
2424
import { ContextError, ResolutionError } from "../../lib/errors.js";
2525
import { formatEventDetails } from "../../lib/formatters/index.js";
26+
import { commandOutput } from "../../lib/formatters/output.js";
2627
import {
2728
applyFreshFlag,
2829
FRESH_ALIASES,
@@ -380,12 +381,11 @@ export const viewCommand = buildCommand({
380381
? { traceId: spanTreeResult.traceId, spans: spanTreeResult.spans }
381382
: null;
382383

383-
yield {
384-
data: { event, trace, spanTreeLines: spanTreeResult?.lines },
384+
yield commandOutput({ event, trace, spanTreeLines: spanTreeResult?.lines });
385+
return {
385386
hint: target.detectedFrom
386387
? `Detected from ${target.detectedFrom}`
387388
: undefined,
388389
};
389-
return;
390390
},
391391
});

src/commands/issue/explain.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +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 { commandOutput } from "../../lib/formatters/output.js";
1011
import {
1112
formatRootCauseList,
1213
handleSeerApiError,
@@ -104,11 +105,8 @@ export const explainCommand = buildCommand({
104105
);
105106
}
106107

107-
yield {
108-
data: causes,
109-
hint: `To create a plan, run: sentry issue plan ${issueArg}`,
110-
};
111-
return;
108+
yield commandOutput(causes);
109+
return { hint: `To create a plan, run: sentry issue plan ${issueArg}` };
112110
} catch (error) {
113111
// Handle API errors with friendly messages
114112
if (error instanceof ApiError) {

0 commit comments

Comments
 (0)