Skip to content

Commit 2c2ff94

Browse files
thomasballingerConvex, Inc.
authored andcommitted
npx convex login status and npx convex deployments (#40220)
Would be useful for some support requests. GitOrigin-RevId: b0230f557680b1aed4a9d5d824964e66925ae8cc
1 parent 3b94d5a commit 2c2ff94

File tree

6 files changed

+159
-37
lines changed

6 files changed

+159
-37
lines changed

src/cli/deployments.ts

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { Command } from "@commander-js/extra-typings";
2-
import { readProjectConfig } from "./lib/config.js";
3-
import chalk from "chalk";
4-
import { bigBrainAPI } from "./lib/utils/utils.js";
5-
import { oneoffContext } from "../bundler/context.js";
6-
import { logError, logMessage, logOutput } from "../bundler/log.js";
7-
8-
type Deployment = {
9-
id: number;
10-
name: string;
11-
create_time: number;
12-
deployment_type: "dev" | "prod";
13-
};
2+
import { oneoffContext, Context } from "../bundler/context.js";
3+
import { logMessage } from "../bundler/log.js";
4+
import {
5+
getDeploymentSelection,
6+
DeploymentSelection,
7+
} from "./lib/deploymentSelection.js";
8+
import { fetchTeamAndProject } from "./lib/api.js";
9+
10+
// This is a debugging command: it's output is not stable, don't write scripts
11+
// that depend on its output.
12+
13+
// TODO: for the deployments command to list all deployments in a project
14+
// we need a stable endpoint for listing projects (check) and a way to
15+
// get a project ID in all cases to use it. We have an endpoint that lists
16+
// deployments by team/project slug today but it's not in use and we'll
17+
// be able to deprecate it if we avoid using it.
1418

1519
export const deployments = new Command("deployments")
1620
.description("List deployments associated with a project")
@@ -21,18 +25,78 @@ export const deployments = new Command("deployments")
2125
adminKey: undefined,
2226
envFile: undefined,
2327
});
24-
const { projectConfig: config } = await readProjectConfig(ctx);
25-
26-
const url = `teams/${config.team}/projects/${config.project}/deployments`;
27-
28-
logMessage(`Deployments for project ${config.team}/${config.project}`);
29-
const deployments = (await bigBrainAPI({
30-
ctx,
31-
method: "GET",
32-
url,
33-
})) as Deployment[];
34-
logOutput(deployments);
35-
if (deployments.length === 0) {
36-
logError(chalk.yellow(`No deployments exist for project`));
37-
}
28+
29+
const deploymentSelection = await getDeploymentSelection(ctx, {
30+
url: undefined,
31+
adminKey: undefined,
32+
envFile: undefined,
33+
});
34+
35+
await displayCurrentDeploymentInfo(ctx, deploymentSelection);
3836
});
37+
38+
async function displayCurrentDeploymentInfo(
39+
ctx: Context,
40+
selection: DeploymentSelection,
41+
) {
42+
logMessage("Currently configured deployment:");
43+
44+
switch (selection.kind) {
45+
case "existingDeployment": {
46+
const { deploymentToActOn } = selection;
47+
logMessage(` URL: ${deploymentToActOn.url}`);
48+
49+
if (deploymentToActOn.deploymentFields) {
50+
const fields = deploymentToActOn.deploymentFields;
51+
logMessage(
52+
` Deployment: ${fields.deploymentName} (${fields.deploymentType})`,
53+
);
54+
logMessage(` Team: ${fields.teamSlug}`);
55+
logMessage(` Project: ${fields.projectSlug}`);
56+
} else {
57+
logMessage(` Type: ${deploymentToActOn.source}`);
58+
}
59+
break;
60+
}
61+
case "deploymentWithinProject": {
62+
const { targetProject } = selection;
63+
if (targetProject.kind === "teamAndProjectSlugs") {
64+
logMessage(` Team: ${targetProject.teamSlug}`);
65+
logMessage(` Project: ${targetProject.projectSlug}`);
66+
} else if (targetProject.kind === "deploymentName") {
67+
const slugs = await fetchTeamAndProject(
68+
ctx,
69+
targetProject.deploymentName,
70+
);
71+
logMessage(` Team: ${slugs.team}`);
72+
logMessage(` Project: ${slugs.project}`);
73+
logMessage(` Deployment: ${targetProject.deploymentName}`);
74+
if (targetProject.deploymentType) {
75+
logMessage(` Type: ${targetProject.deploymentType}`);
76+
}
77+
} else {
78+
logMessage(` Project deploy key configured`);
79+
}
80+
break;
81+
}
82+
case "preview": {
83+
logMessage(` Preview deployment (deploy key configured)`);
84+
break;
85+
}
86+
case "anonymous": {
87+
if (selection.deploymentName) {
88+
logMessage(` Anonymous deployment: ${selection.deploymentName}`);
89+
} else {
90+
logMessage(` Anonymous development (no deployment selected)`);
91+
}
92+
break;
93+
}
94+
case "chooseProject": {
95+
logMessage(` No project configured - will prompt interactively`);
96+
break;
97+
}
98+
default: {
99+
logMessage(` Unknown deployment configuration`);
100+
}
101+
}
102+
}

src/cli/lib/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ async function hasAccessToProject(
182182
try {
183183
await bigBrainAPIMaybeThrows({
184184
ctx,
185-
url: `/api/teams/${selector.teamSlug}/projects/${selector.projectSlug}/deployments`,
185+
url: `teams/${selector.teamSlug}/projects/${selector.projectSlug}/deployments`,
186186
method: "GET",
187187
});
188188
return true;
@@ -256,7 +256,7 @@ async function getTeamAndProjectSlugForDeployment(
256256
try {
257257
const body = await bigBrainAPIMaybeThrows({
258258
ctx,
259-
url: `/api/deployment/${selector.deploymentName}/team_and_project`,
259+
url: `deployment/${selector.deploymentName}/team_and_project`,
260260
method: "GET",
261261
});
262262
return { teamSlug: body.team, projectSlug: body.project };

src/cli/lib/localDeployment/bigBrain.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export async function bigBrainStart(
1414
return bigBrainAPI({
1515
ctx,
1616
method: "POST",
17-
url: "/api/local_deployment/start",
17+
url: "local_deployment/start",
1818
data,
1919
});
2020
}
@@ -29,7 +29,7 @@ export async function bigBrainPause(
2929
return bigBrainAPI({
3030
ctx,
3131
method: "POST",
32-
url: "/api/local_deployment/pause",
32+
url: "local_deployment/pause",
3333
data,
3434
});
3535
}
@@ -43,7 +43,7 @@ export async function bigBrainRecordActivity(
4343
return bigBrainAPI({
4444
ctx,
4545
method: "POST",
46-
url: "/api/local_deployment/record_activity",
46+
url: "local_deployment/record_activity",
4747
data,
4848
});
4949
}
@@ -54,7 +54,7 @@ export async function bigBrainEnableFeatureMetadata(
5454
return bigBrainAPI({
5555
ctx,
5656
method: "POST",
57-
url: "/api/local_deployment/enable_feature_metadata",
57+
url: "local_deployment/enable_feature_metadata",
5858
data: {},
5959
});
6060
}
@@ -69,7 +69,7 @@ export async function bigBrainGenerateAdminKeyForAnonymousDeployment(
6969
return bigBrainAPI({
7070
ctx,
7171
method: "POST",
72-
url: "/api/local_deployment/generate_admin_key",
72+
url: "local_deployment/generate_admin_key",
7373
data,
7474
});
7575
}
@@ -94,7 +94,7 @@ export async function projectHasExistingCloudDev(
9494
>({
9595
ctx,
9696
method: "POST",
97-
url: "/api/deployment/existing_dev",
97+
url: "deployment/existing_dev",
9898
data: { projectSlug, teamSlug },
9999
});
100100
if (response.kind === "Exists") {

src/cli/lib/localDeployment/dashboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function reportSelfHostedEvent(
8787
await bigBrainAPIMaybeThrows({
8888
ctx,
8989
method: "POST",
90-
url: "/api/self_hosted_event",
90+
url: "self_hosted_event",
9191
data: {
9292
selfHostedUuid: anonymousId,
9393
eventName,

src/cli/lib/login.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,17 @@ async function optins(ctx: Context, acceptOptIns: boolean): Promise<boolean> {
487487
return true;
488488
}
489489

490+
export async function getTeamsForUser(ctx: Context) {
491+
const teams = await bigBrainAPI<{ id: number; name: string; slug: string }[]>(
492+
{
493+
ctx,
494+
method: "GET",
495+
url: "teams",
496+
},
497+
);
498+
return teams;
499+
}
500+
490501
export async function ensureLoggedIn(
491502
ctx: Context,
492503
options?: {

src/cli/login.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Command, Option } from "@commander-js/extra-typings";
22
import { Context, oneoffContext } from "../bundler/context.js";
33
import { logFailure, logFinishedStep, logMessage } from "../bundler/log.js";
4-
import { checkAuthorization, performLogin } from "./lib/login.js";
4+
import {
5+
checkAuthorization,
6+
performLogin,
7+
getTeamsForUser,
8+
} from "./lib/login.js";
59
import { loadUuidForAnonymousUser } from "./lib/localDeployment/filePaths.js";
610
import {
711
handleLinkToProject,
@@ -23,6 +27,47 @@ import {
2327
shouldAllowAnonymousDevelopment,
2428
} from "./lib/deploymentSelection.js";
2529
import { removeAnonymousPrefix } from "./lib/deployment.js";
30+
import {
31+
readGlobalConfig,
32+
globalConfigPath,
33+
} from "./lib/utils/globalConfig.js";
34+
35+
const loginStatus = new Command("status")
36+
.description("Check login status and list accessible teams")
37+
.allowExcessArguments(false)
38+
.action(async () => {
39+
const ctx = await oneoffContext({
40+
url: undefined,
41+
adminKey: undefined,
42+
envFile: undefined,
43+
});
44+
45+
const globalConfig = readGlobalConfig(ctx);
46+
const hasToken = globalConfig?.accessToken !== null;
47+
48+
if (hasToken) {
49+
logMessage(`Convex account token found in: ${globalConfigPath()}`);
50+
} else {
51+
logMessage("No token found locally");
52+
return;
53+
}
54+
55+
const isLoggedIn = await checkAuthorization(ctx, false);
56+
57+
if (!isLoggedIn) {
58+
logMessage("Status: Not logged in");
59+
return;
60+
}
61+
62+
logMessage("Status: Logged in");
63+
const teams = await getTeamsForUser(ctx);
64+
logMessage(
65+
`Teams: ${teams.length} team${teams.length === 1 ? "" : "s"} accessible`,
66+
);
67+
for (const team of teams) {
68+
logMessage(` - ${team.name} (${team.slug})`);
69+
}
70+
});
2671

2772
export const login = new Command("login")
2873
.description("Login to Convex")
@@ -62,6 +107,8 @@ export const login = new Command("login")
62107
.addOption(new Option("--dump-access-token").hideHelp())
63108
// Hidden option for tests to check if the user is logged in.
64109
.addOption(new Option("--check-login").hideHelp())
110+
.addCommand(loginStatus)
111+
.addHelpCommand(false)
65112
.action(async (options, cmd: Command) => {
66113
const ctx = await oneoffContext({
67114
url: undefined,
@@ -274,7 +321,7 @@ async function getProjectsRemaining(ctx: Context, teamSlug: string) {
274321
const response = await bigBrainAPI<{ projectsRemaining: number }>({
275322
ctx,
276323
method: "GET",
277-
url: `/api/teams/${teamSlug}/projects_remaining`,
324+
url: `teams/${teamSlug}/projects_remaining`,
278325
});
279326

280327
return response.projectsRemaining;

0 commit comments

Comments
 (0)