Skip to content

Commit a077e50

Browse files
authored
feat(cli): add whoami command (#6552)
## What and why Adds `supabase whoami`, a read-only command that fetches the active user's Management API profile. Text output presents clear user-facing labels, while JSON and stream-JSON expose the stable `id`, `email`, and `username` fields instead of leaking API-specific field names. Linear: [CLI-1280](https://linear.app/supabase/issue/CLI-1280/add-a-whoami-command) ## Usage ```console $ supabase whoami USER ID USERNAME EMAIL 00000000-0000-0000-0000-000000000000 example user@example.com ``` ```console $ supabase whoami --output-format json {"id":"00000000-0000-0000-0000-000000000000","email":"user@example.com","username":"example"} ``` Stream-JSON emits the same identity object under a standard `result.data` envelope. ## Testing strategy Handler integration coverage exercises the Management API request, all output modes, transport/status/decoding failures, unsupported legacy output flags, machine-mode progress behavior, and telemetry flushing. Focused formatter unit coverage protects text rendering, with the full CLI unit and integration suites covering workspace interactions. ## Review The complete branch diff was reviewed for correctness, test coverage and failure behavior, and scope and maintainability. Follow-up findings were closed by removing externally mutating live coverage, tightening machine-output and failure assertions, documenting inherited filesystem/environment/telemetry behavior, and normalizing the public JSON contract.
1 parent 10e6e3e commit a077e50

25 files changed

Lines changed: 738 additions & 18 deletions

apps/cli/src/cli/root.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { testCommand } from "../commands/test/test.command.ts";
4545
import { telemetryCommand } from "../commands/telemetry/telemetry.command.ts";
4646
import { unlinkCommand } from "../commands/unlink/unlink.command.ts";
4747
import { vanitySubdomainsCommand } from "../commands/vanity-subdomains/vanity-subdomains.command.ts";
48+
import { whoamiCommand } from "../commands/whoami/whoami.command.ts";
4849
import { OutputFormatFlag } from "../shared/cli/global-flags.ts";
4950
import { outputLayerFor } from "../shared/output/output.layer.ts";
5051
import { quietProgressTextOutputLayer } from "../output/quiet-progress-text-output.layer.ts";
@@ -121,6 +122,7 @@ export const rootCommandForBackend = (backend: StackBackend = "legacy"): CliRoot
121122
testCommand,
122123
unlinkCommand,
123124
vanitySubdomainsCommand,
125+
whoamiCommand,
124126
]),
125127
Command.provide(
126128
Layer.unwrap(

apps/cli/src/command-internal/go-output-flag.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ export function outputFormatEnumMessage(allowed: ReadonlyArray<string>): string
4949
export function invalidOutputFormatMessage(value: string, allowed: ReadonlyArray<string>): string {
5050
return `invalid argument "${value}" for "-o, --output" flag: ${outputFormatEnumMessage(allowed)}`;
5151
}
52+
53+
/** Directs commands that do not support the legacy output flag to `--output-format`. */
54+
export function unsupportedOutputFlagMessage(command: string): string {
55+
return `the -o/--output flag is not supported by ${command}; use --output-format json|stream-json instead.`;
56+
}

apps/cli/src/command-internal/go-output-flag.unit.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
RESOURCE_OUTPUT_FORMATS,
55
invalidOutputFormatMessage,
66
outputFormatEnumMessage,
7+
unsupportedOutputFlagMessage,
78
} from "./go-output-flag.ts";
89

910
describe("go-output-flag", () => {
@@ -25,4 +26,19 @@ describe("go-output-flag", () => {
2526
'invalid argument "yaml" for "-o, --output" flag: must be one of [ json | table | csv ]',
2627
);
2728
});
29+
30+
it("directs unsupported commands to --output-format", () => {
31+
expect(unsupportedOutputFlagMessage("whoami")).toBe(
32+
"the -o/--output flag is not supported by whoami; use --output-format json|stream-json instead.",
33+
);
34+
expect(unsupportedOutputFlagMessage("pull")).toBe(
35+
"the -o/--output flag is not supported by pull; use --output-format json|stream-json instead.",
36+
);
37+
expect(unsupportedOutputFlagMessage("config diff")).toBe(
38+
"the -o/--output flag is not supported by config diff; use --output-format json|stream-json instead.",
39+
);
40+
expect(unsupportedOutputFlagMessage("config pull")).toBe(
41+
"the -o/--output flag is not supported by config pull; use --output-format json|stream-json instead.",
42+
);
43+
});
2844
});

apps/cli/src/command-internal/http-errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export function unexpectedStatusMessage(status: number, body: string): string {
101101
return `unexpected status ${status}: ${body}`;
102102
}
103103

104+
/** Shared remediation for Management API requests rejected with HTTP 401. */
105+
export const AUTHENTICATION_FAILED_STATUS_MESSAGE =
106+
"Authentication failed: your access token is invalid or has expired. Run `supabase login` to re-authenticate.";
107+
104108
export type NetworkErrorFactory<E> = new (args: {
105109
readonly message: string;
106110
readonly decode?: boolean;

apps/cli/src/command-internal/pooler-fallback.unit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function captureOutput() {
5151
message: () => Effect.void,
5252
stop: () => Effect.void,
5353
}),
54+
result: () => Effect.void,
5455
success: () => Effect.void,
5556
fail: () => Effect.void,
5657
raw: (text: string, stream: "stdout" | "stderr" = "stdout") =>

apps/cli/src/commands/config/config.read-status.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { sanitizeInlineName, unexpectedStatusMessage } from "../../command-internal/http-errors.ts";
1+
import {
2+
AUTHENTICATION_FAILED_STATUS_MESSAGE,
3+
sanitizeInlineName,
4+
unexpectedStatusMessage,
5+
} from "../../command-internal/http-errors.ts";
26

37
/**
48
* Purpose-written messages for the status codes a wrong or inaccessible ref
@@ -26,7 +30,7 @@ export function configReadStatusMessage(
2630
apiHost: string,
2731
): string {
2832
if (status === 401) {
29-
return "Authentication failed: your access token is invalid or has expired. Run `supabase login` to re-authenticate.";
33+
return AUTHENTICATION_FAILED_STATUS_MESSAGE;
3034
}
3135
if (status === 403) {
3236
return `Access denied for project ${sanitizeInlineName(ref)}: your account does not have permission to view its configuration.`;

apps/cli/src/commands/config/diff/diff.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { validateWorkdirIsDirectory } from "../../../command-internal/workdir-va
1313
import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts";
1414
import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts";
1515
import { OutputFlag } from "../../../command-internal/global-flags.ts";
16+
import { unsupportedOutputFlagMessage } from "../../../command-internal/go-output-flag.ts";
1617
import { Output } from "../../../shared/output/output.service.ts";
1718
import { ProcessControl } from "../../../shared/runtime/process-control.service.ts";
1819
import {
@@ -110,8 +111,7 @@ export const configDiff = Effect.fn("config.diff")(function* (flags: ConfigDiffF
110111
// the same as every other failure here.
111112
if (Option.isSome(goOutputFlag)) {
112113
return yield* new ConfigDiffOutputFlagUnsupportedError({
113-
message:
114-
"the -o/--output flag is not supported by config diff; use --output-format json|stream-json instead.",
114+
message: unsupportedOutputFlagMessage("config diff"),
115115
});
116116
}
117117

apps/cli/src/commands/config/pull/pull.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { validateWorkdirIsDirectory } from "../../../command-internal/workdir-va
1616
import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts";
1717
import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts";
1818
import { resolveYes, OutputFlag } from "../../../command-internal/global-flags.ts";
19+
import { unsupportedOutputFlagMessage } from "../../../command-internal/go-output-flag.ts";
1920
import { promptYesNo } from "../../../command-internal/prompt-yes-no.ts";
2021
import { Output } from "../../../shared/output/output.service.ts";
2122
import { Tty } from "../../../shared/runtime/tty.service.ts";
@@ -275,8 +276,7 @@ export const configPull = Effect.fn("config.pull")(function* (flags: ConfigPullF
275276
// contract (CLI-2156, mirrors `config diff`).
276277
if (Option.isSome(goOutputFlag)) {
277278
return yield* new ConfigPullOutputFlagUnsupportedError({
278-
message:
279-
"the -o/--output flag is not supported by config pull; use --output-format json|stream-json instead.",
279+
message: unsupportedOutputFlagMessage("config pull"),
280280
});
281281
}
282282

apps/cli/src/commands/issue/issue.integration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function issueMockOutput(opts: { readonly format?: OutputFormat } = {}) {
7979
message: () => Effect.void,
8080
stop: () => Effect.void,
8181
}),
82+
result: () => Effect.void,
8283
success: (message: string, data?: Record<string, unknown>) =>
8384
Effect.sync(() => {
8485
messages.push({ type: "success", message, data });

apps/cli/src/commands/pull/pull.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CommandSettings } from "../../config/command-settings.service.ts";
44
import { pathHasUncommittedChanges } from "../../command-internal/git-status.ts";
55
import { mapHttpError } from "../../command-internal/http-errors.ts";
66
import { MigrationsReadError } from "../../command-internal/migration.errors.ts";
7+
import { unsupportedOutputFlagMessage } from "../../command-internal/go-output-flag.ts";
78
import { validateWorkdirIsDirectory } from "../../command-internal/workdir-validation.ts";
89
import { MachineErrorContext } from "../../shared/output/machine-error-context.service.ts";
910
import { resolveYes, OutputFlag } from "../../command-internal/global-flags.ts";
@@ -248,8 +249,7 @@ export const pull = Effect.fn("pull")(function* (flags: PullFlags) {
248249
// a net-new TS command with no Go parity contract (CLI-2156).
249250
if (Option.isSome(goOutputFlag)) {
250251
return yield* new PullOutputFlagUnsupportedError({
251-
message:
252-
"the -o/--output flag is not supported by pull; use --output-format json|stream-json instead.",
252+
message: unsupportedOutputFlagMessage("pull"),
253253
});
254254
}
255255

0 commit comments

Comments
 (0)