Skip to content

Commit e5deb88

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 007dc36 commit e5deb88

File tree

5 files changed

+215
-4
lines changed

5 files changed

+215
-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,135 @@
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 { runInTempDir } from "./run-in-tmp";
8+
import { mkdirSync, writeFileSync } from "node:fs";
9+
import { setMockResponse } from "./mock-cfetch";
10+
import { initialise } from "../user";
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+
});
32+
33+
it("should return undefined if there is no config file", async () => {
34+
await initialise();
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+
await initialise();
42+
const userInfo = await getUserInfo();
43+
expect(userInfo).toBeUndefined();
44+
});
45+
46+
it("should return the user's email and accounts if authenticated via config token", async () => {
47+
writeUserConfig("some-oauth-token");
48+
setMockResponse("/user", () => {
49+
return { email: "[email protected]" };
50+
});
51+
setMockResponse("/accounts", () => {
52+
return [
53+
{ name: "Account One", id: "account-1" },
54+
{ name: "Account Two", id: "account-2" },
55+
{ name: "Account Three", id: "account-3" },
56+
];
57+
});
58+
59+
await initialise();
60+
const userInfo = await getUserInfo();
61+
62+
expect(userInfo).toEqual({
63+
authType: "OAuth",
64+
apiToken: "some-oauth-token",
65+
66+
accounts: [
67+
{ name: "Account One", id: "account-1" },
68+
{ name: "Account Two", id: "account-2" },
69+
{ name: "Account Three", id: "account-3" },
70+
],
71+
});
72+
});
73+
});
74+
75+
describe("WhoAmI component", () => {
76+
it("should return undefined if there is no user", async () => {
77+
const { lastFrame } = render(<WhoAmI user={undefined}></WhoAmI>);
78+
79+
expect(lastFrame()).toMatchInlineSnapshot(
80+
`"You are not authenticated. Please run \`wrangler login\`."`
81+
);
82+
});
83+
84+
it("should display the user's email and accounts", async () => {
85+
const user: UserInfo = {
86+
authType: "OAuth",
87+
apiToken: "some-oauth-token",
88+
89+
accounts: [
90+
{ name: "Account One", id: "account-1" },
91+
{ name: "Account Two", id: "account-2" },
92+
{ name: "Account Three", id: "account-3" },
93+
],
94+
};
95+
96+
const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>);
97+
98+
expect(lastFrame()).toMatchInlineSnapshot(`
99+
"👋 You are logged in with an OAuth Token, associated with the email '[email protected]'!
100+
┌───────────────┬────────────┐
101+
│ Account Name │ Account ID │
102+
├───────────────┼────────────┤
103+
│ Account One │ account-1 │
104+
├───────────────┼────────────┤
105+
│ Account Two │ account-2 │
106+
├───────────────┼────────────┤
107+
│ Account Three │ account-3 │
108+
└───────────────┴────────────┘"
109+
`);
110+
});
111+
});
112+
113+
function writeUserConfig(
114+
oauth_token?: string,
115+
refresh_token?: string,
116+
expiration_time?: string
117+
) {
118+
const lines: string[] = [];
119+
if (oauth_token) {
120+
lines.push(`oauth_token = "${oauth_token}"`);
121+
}
122+
if (refresh_token) {
123+
lines.push(`refresh_token = "${refresh_token}"`);
124+
}
125+
if (expiration_time) {
126+
lines.push(`expiration_time = "${expiration_time}"`);
127+
}
128+
const configPath = path.join(os.homedir(), ".wrangler/config");
129+
mkdirSync(configPath, { recursive: true });
130+
writeFileSync(
131+
path.join(configPath, "default.toml"),
132+
lines.join("\n"),
133+
"utf-8"
134+
);
135+
}

packages/wrangler/src/index.tsx

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

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

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 = 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)