Skip to content

Commit e95f4c8

Browse files
feat: add whoami command
This commit adds a new `whoami` command to the CLI, which aligns with Wrangler 1. Resolves #274
1 parent 08b6612 commit e95f4c8

File tree

5 files changed

+213
-4
lines changed

5 files changed

+213
-4
lines changed

.changeset/cyan-comics-sneeze.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Add whoami command

packages/wrangler/src/__tests__/index.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe("wrangler", () => {
1717
1818
Commands:
1919
wrangler init [name] 📥 Create a wrangler.toml configuration file
20+
wrangler whoami 🕵️ Retrieve your user info and test your auth config
2021
wrangler dev <filename> 👂 Start a local server for developing your worker
2122
wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
2223
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
@@ -49,6 +50,7 @@ describe("wrangler", () => {
4950
5051
Commands:
5152
wrangler init [name] 📥 Create a wrangler.toml configuration file
53+
wrangler whoami 🕵️ Retrieve your user info and test your auth config
5254
wrangler dev <filename> 👂 Start a local server for developing your worker
5355
wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
5456
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React from "react";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { render } from "ink-testing-library";
5+
import type { UserInfo } from "../whoami";
6+
import { getUserInfo, WhoAmI } from "../whoami";
7+
import { resetLocalState } from "../user";
8+
import { runInTempDir } from "./run-in-tmp";
9+
import { mkdirSync, writeFileSync } from "node:fs";
10+
import { setMockResponse } from "./mock-cfetch";
11+
12+
const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN;
13+
const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID;
14+
15+
describe("getUserInfo()", () => {
16+
runInTempDir();
17+
18+
beforeEach(() => {
19+
// Clear the environment variables, so we can control them in the tests
20+
delete process.env.CF_API_TOKEN;
21+
delete process.env.CF_ACCOUNT_ID;
22+
// Override where the home directory is so that we can specify a user config
23+
mkdirSync("./home");
24+
jest.spyOn(os, "homedir").mockReturnValue("./home");
25+
});
26+
27+
afterEach(() => {
28+
// Reset any changes to the environment variables
29+
process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN;
30+
process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID;
31+
resetLocalState();
32+
});
33+
34+
it("should return undefined if there is no config file", async () => {
35+
const userInfo = await getUserInfo();
36+
expect(userInfo).toBeUndefined();
37+
});
38+
39+
it("should return undefined if there is an empty config file", async () => {
40+
writeUserConfig();
41+
const userInfo = await getUserInfo();
42+
expect(userInfo).toBeUndefined();
43+
});
44+
45+
it("should return the user's email and accounts if authenticated via config token", async () => {
46+
writeUserConfig("some-oauth-token");
47+
setMockResponse("/user", () => {
48+
return { email: "[email protected]" };
49+
});
50+
setMockResponse("/accounts", () => {
51+
return [
52+
{ name: "Account One", id: "account-1" },
53+
{ name: "Account Two", id: "account-2" },
54+
{ name: "Account Three", id: "account-3" },
55+
];
56+
});
57+
58+
const userInfo = await getUserInfo();
59+
60+
expect(userInfo).toEqual({
61+
authType: "OAuth",
62+
apiToken: "some-oauth-token",
63+
64+
accounts: [
65+
{ name: "Account One", id: "account-1" },
66+
{ name: "Account Two", id: "account-2" },
67+
{ name: "Account Three", id: "account-3" },
68+
],
69+
});
70+
});
71+
});
72+
73+
describe("WhoAmI component", () => {
74+
it("should return undefined if there is no user", async () => {
75+
const { lastFrame } = render(<WhoAmI user={undefined}></WhoAmI>);
76+
77+
expect(lastFrame()).toMatchInlineSnapshot(
78+
`"You are not authenticated. Please run \`wrangler login\`."`
79+
);
80+
});
81+
82+
it("should display the user's email and accounts", async () => {
83+
const user: UserInfo = {
84+
authType: "OAuth",
85+
apiToken: "some-oauth-token",
86+
87+
accounts: [
88+
{ name: "Account One", id: "account-1" },
89+
{ name: "Account Two", id: "account-2" },
90+
{ name: "Account Three", id: "account-3" },
91+
],
92+
};
93+
94+
const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>);
95+
96+
expect(lastFrame()).toMatchInlineSnapshot(`
97+
"👋 You are logged in with an OAuth Token, associated with the email '[email protected]'!
98+
┌───────────────┬────────────┐
99+
│ Account Name │ Account ID │
100+
├───────────────┼────────────┤
101+
│ Account One │ account-1 │
102+
├───────────────┼────────────┤
103+
│ Account Two │ account-2 │
104+
├───────────────┼────────────┤
105+
│ Account Three │ account-3 │
106+
└───────────────┴────────────┘"
107+
`);
108+
});
109+
});
110+
111+
function writeUserConfig(
112+
oauth_token?: string,
113+
refresh_token?: string,
114+
expiration_time?: string
115+
) {
116+
const lines: string[] = [];
117+
if (oauth_token) {
118+
lines.push(`oauth_token = "${oauth_token}"`);
119+
}
120+
if (refresh_token) {
121+
lines.push(`refresh_token = "${refresh_token}"`);
122+
}
123+
if (expiration_time) {
124+
lines.push(`expiration_time = "${expiration_time}"`);
125+
}
126+
const configPath = path.join(os.homedir(), ".wrangler/config");
127+
mkdirSync(configPath, { recursive: true });
128+
writeFileSync(
129+
path.join(configPath, "default.toml"),
130+
lines.join("\n"),
131+
"utf-8"
132+
);
133+
}

packages/wrangler/src/index.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import onExit from "signal-exit";
4444
import { setTimeout } from "node:timers/promises";
4545
import * as fs from "node:fs";
4646
import { execa } from "execa";
47+
import { whoami } from "./whoami";
4748

4849
const resetColor = "\x1b[0m";
4950
const fgGreenColor = "\x1b[32m";
@@ -363,11 +364,10 @@ export async function main(argv: string[]): Promise<void> {
363364
// whoami
364365
wrangler.command(
365366
"whoami",
366-
false, // we don't need to show this the menu
367-
// "🕵️ Retrieve your user info and test your auth config",
367+
"🕵️ Retrieve your user info and test your auth config",
368368
() => {},
369-
(args) => {
370-
console.log(":whoami", args);
369+
async () => {
370+
await whoami();
371371
}
372372
);
373373

packages/wrangler/src/whoami.tsx

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Text, render } from "ink";
2+
import Table from "ink-table";
3+
import React from "react";
4+
import { fetchListResult, fetchResult } from "./cfetch";
5+
import { getAPIToken } from "./user";
6+
7+
export async function whoami() {
8+
console.log("Getting User settings...");
9+
const user = await getUserInfo();
10+
render(<WhoAmI user={user}></WhoAmI>);
11+
}
12+
13+
export function WhoAmI({ user }: { user: UserInfo | undefined }) {
14+
return user ? (
15+
<>
16+
<Email tokenType={user.authType} email={user.email}></Email>
17+
<Accounts accounts={user.accounts}></Accounts>
18+
</>
19+
) : (
20+
<Text>You are not authenticated. Please run `wrangler login`.</Text>
21+
);
22+
}
23+
24+
function Email(props: { tokenType: string; email: string }) {
25+
return (
26+
<Text>
27+
👋 You are logged in with an {props.tokenType} Token, associated with the
28+
email &apos;{props.email}&apos;!
29+
</Text>
30+
);
31+
}
32+
33+
function Accounts(props: { accounts: AccountInfo[] }) {
34+
const accounts = props.accounts.map((account) => ({
35+
"Account Name": account.name,
36+
"Account ID": account.id,
37+
}));
38+
return <Table data={accounts}></Table>;
39+
}
40+
41+
export interface UserInfo {
42+
apiToken: string;
43+
authType: string;
44+
email: string;
45+
accounts: AccountInfo[];
46+
}
47+
48+
export async function getUserInfo(): Promise<UserInfo | undefined> {
49+
const apiToken = await getAPIToken();
50+
return apiToken
51+
? {
52+
apiToken,
53+
authType: "OAuth",
54+
email: await getEmail(),
55+
accounts: await getAccounts(),
56+
}
57+
: undefined;
58+
}
59+
60+
async function getEmail(): Promise<string> {
61+
const { email } = await fetchResult<{ email: string }>("/user");
62+
return email;
63+
}
64+
65+
type AccountInfo = { name: string; id: string };
66+
67+
async function getAccounts(): Promise<AccountInfo[]> {
68+
return await fetchListResult<AccountInfo>("/accounts");
69+
}

0 commit comments

Comments
 (0)