Skip to content

Commit 74fdfa9

Browse files
authored
Show project details when running the whoami command (#2231)
* add project details to whoami * findFirst * skip project check if no orgs found * add changeset
1 parent bdaa2ed commit 74fdfa9

File tree

5 files changed

+129
-8
lines changed

5 files changed

+129
-8
lines changed

.changeset/healthy-apricots-drop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Add project details to the whoami command

apps/webapp/app/routes/api.v2.whoami.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { prisma } from "~/db.server";
55
import { env } from "~/env.server";
66
import { logger } from "~/services/logger.server";
77
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
8+
import { v3ProjectPath } from "~/utils/pathBuilder";
89

910
export async function loader({ request }: LoaderFunctionArgs) {
1011
logger.info("whoami v2", { url: request.url });
@@ -27,10 +28,65 @@ export async function loader({ request }: LoaderFunctionArgs) {
2728
return json({ error: "User not found" }, { status: 404 });
2829
}
2930

31+
const url = new URL(request.url);
32+
const projectRef = url.searchParams.get("projectRef");
33+
34+
let projectDetails: WhoAmIResponse["project"];
35+
36+
if (projectRef) {
37+
const orgs = await prisma.organization.findMany({
38+
select: {
39+
id: true,
40+
},
41+
where: {
42+
members: {
43+
some: {
44+
userId: authenticationResult.userId,
45+
},
46+
},
47+
},
48+
});
49+
50+
if (orgs.length > 0) {
51+
const project = await prisma.project.findFirst({
52+
select: {
53+
externalRef: true,
54+
name: true,
55+
slug: true,
56+
organization: {
57+
select: {
58+
slug: true,
59+
title: true,
60+
},
61+
},
62+
},
63+
where: {
64+
externalRef: projectRef,
65+
organizationId: {
66+
in: orgs.map((org) => org.id),
67+
},
68+
},
69+
});
70+
71+
if (project) {
72+
const projectPath = v3ProjectPath(
73+
{ slug: project.organization.slug },
74+
{ slug: project.slug }
75+
);
76+
projectDetails = {
77+
url: new URL(projectPath, env.APP_ORIGIN).href,
78+
name: project.name,
79+
orgTitle: project.organization.title,
80+
};
81+
}
82+
}
83+
}
84+
3085
const result: WhoAmIResponse = {
3186
userId: authenticationResult.userId,
3287
email: user.email,
3388
dashboardUrl: env.APP_ORIGIN,
89+
project: projectDetails,
3490
};
3591
return json(result);
3692
} catch (error) {

packages/cli-v3/src/apiClient.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@ export class CliApiClient {
7979
});
8080
}
8181

82-
async whoAmI() {
82+
async whoAmI(projectRef?: string) {
8383
if (!this.accessToken) {
8484
throw new Error("whoAmI: No access token");
8585
}
8686

87-
return wrapZodFetch(WhoAmIResponseSchema, `${this.apiURL}/api/v2/whoami`, {
87+
const url = new URL("/api/v2/whoami", this.apiURL);
88+
89+
if (projectRef) {
90+
url.searchParams.append("projectRef", projectRef);
91+
}
92+
93+
return wrapZodFetch(WhoAmIResponseSchema, url.href, {
8894
headers: {
8995
Authorization: `Bearer ${this.accessToken}`,
9096
"Content-Type": "application/json",

packages/cli-v3/src/commands/whoami.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {
1313
import { z } from "zod";
1414
import { CliApiClient } from "../apiClient.js";
1515
import { spinner } from "../utilities/windows.js";
16+
import { loadConfig } from "../config.js";
17+
import { resolveLocalEnvVars } from "../utilities/localEnvVars.js";
18+
import { tryCatch } from "@trigger.dev/core";
1619

1720
type WhoAmIResult =
1821
| {
@@ -21,20 +24,36 @@ type WhoAmIResult =
2124
userId: string;
2225
email: string;
2326
dashboardUrl: string;
27+
projectUrl?: string;
2428
};
2529
}
2630
| {
2731
success: false;
2832
error: string;
2933
};
3034

31-
const WhoamiCommandOptions = CommonCommandOptions;
35+
const WhoamiCommandOptions = CommonCommandOptions.extend({
36+
config: z.string().optional(),
37+
projectRef: z.string().optional(),
38+
envFile: z.string().optional(),
39+
});
3240

3341
type WhoamiCommandOptions = z.infer<typeof WhoamiCommandOptions>;
3442

3543
export function configureWhoamiCommand(program: Command) {
3644
return commonOptions(
37-
program.command("whoami").description("display the current logged in user and project details")
45+
program
46+
.command("whoami")
47+
.description("display the current logged in user and project details")
48+
.option("-c, --config <config file>", "The name of the config file")
49+
.option(
50+
"-p, --project-ref <project ref>",
51+
"The project ref. This will override the project specified in the config file."
52+
)
53+
.option(
54+
"--env-file <env file>",
55+
"Path to the .env file to load into the CLI process. Defaults to .env in the project directory."
56+
)
3857
).action(async (options) => {
3958
await handleTelemetry(async () => {
4059
await printInitialBanner(false);
@@ -58,6 +77,23 @@ export async function whoAmI(
5877
intro(`Displaying your account details [${options?.profile ?? "default"}]`);
5978
}
6079

80+
const envVars = resolveLocalEnvVars(options?.envFile);
81+
82+
if (envVars.TRIGGER_PROJECT_REF) {
83+
logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF });
84+
}
85+
86+
const [configError, resolvedConfig] = await tryCatch(
87+
loadConfig({
88+
overrides: { project: options?.projectRef ?? envVars.TRIGGER_PROJECT_REF },
89+
configFile: options?.config,
90+
})
91+
);
92+
93+
if (configError) {
94+
logger.debug("Error loading config", { error: configError });
95+
}
96+
6197
const loadingSpinner = spinner();
6298

6399
if (!silent) {
@@ -94,7 +130,7 @@ export async function whoAmI(
94130
}
95131

96132
const apiClient = new CliApiClient(authentication.auth.apiUrl, authentication.auth.accessToken);
97-
const userData = await apiClient.whoAmI();
133+
const userData = await apiClient.whoAmI(resolvedConfig?.project);
98134

99135
if (!userData.success) {
100136
loadingSpinner.stop("Error getting your account details");
@@ -109,11 +145,21 @@ export async function whoAmI(
109145
loadingSpinner.stop("Retrieved your account details");
110146
note(
111147
`User ID: ${userData.data.userId}
112-
Email: ${userData.data.email}
113-
URL: ${chalkLink(authentication.auth.apiUrl)}
114-
`,
148+
Email: ${userData.data.email}
149+
URL: ${chalkLink(authentication.auth.apiUrl)}`,
115150
`Account details [${authentication.profile}]`
116151
);
152+
153+
const { project } = userData.data;
154+
155+
if (project) {
156+
note(
157+
`Name: ${project.name}
158+
Org: ${project.orgTitle}
159+
URL: ${chalkLink(project.url)}`,
160+
`Project details [${resolvedConfig?.project}]`
161+
);
162+
}
117163
} else {
118164
!silent && loadingSpinner.stop(`Retrieved your account details for ${userData.data.email}`);
119165
}

packages/core/src/v3/schemas/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export const WhoAmIResponseSchema = z.object({
1616
userId: z.string(),
1717
email: z.string().email(),
1818
dashboardUrl: z.string(),
19+
project: z
20+
.object({
21+
name: z.string(),
22+
url: z.string(),
23+
orgTitle: z.string(),
24+
})
25+
.optional(),
1926
});
2027

2128
export type WhoAmIResponse = z.infer<typeof WhoAmIResponseSchema>;

0 commit comments

Comments
 (0)