Skip to main content
Glama
login.ts11.7 kB
import { Command, Option } from "@commander-js/extra-typings"; import { Context, oneoffContext } from "../bundler/context.js"; import { logFailure, logFinishedStep, logMessage } from "../bundler/log.js"; import { checkAuthorization, performLogin } from "./lib/login.js"; import { loadUuidForAnonymousUser } from "./lib/localDeployment/filePaths.js"; import { handleLinkToProject, listExistingAnonymousDeployments, } from "./lib/localDeployment/anonymous.js"; import { DASHBOARD_HOST, deploymentDashboardUrlPage, teamDashboardUrl, } from "./lib/dashboard.js"; import { promptSearch, promptYesNo } from "./lib/utils/prompts.js"; import { bigBrainAPI, validateOrSelectTeam } from "./lib/utils/utils.js"; import { selectProject, updateEnvAndConfigForDeploymentSelection, } from "./configure.js"; import { getDeploymentSelection, shouldAllowAnonymousDevelopment, } from "./lib/deploymentSelection.js"; import { removeAnonymousPrefix } from "./lib/deployment.js"; import { readGlobalConfig, globalConfigPath, } from "./lib/utils/globalConfig.js"; import { getTeamsForUser } from "./lib/api.js"; const loginStatus = new Command("status") .description("Check login status and list accessible teams") .allowExcessArguments(false) .action(async () => { const ctx = await oneoffContext({ url: undefined, adminKey: undefined, envFile: undefined, }); const globalConfig = readGlobalConfig(ctx); const hasToken = globalConfig?.accessToken !== null; if (hasToken) { logMessage(`Convex account token found in: ${globalConfigPath()}`); } else { logMessage("No token found locally"); return; } const isLoggedIn = await checkAuthorization(ctx, false); if (!isLoggedIn) { logMessage("Status: Not logged in"); return; } logMessage("Status: Logged in"); const teams = await getTeamsForUser(ctx); logMessage( `Teams: ${teams.length} team${teams.length === 1 ? "" : "s"} accessible`, ); for (const team of teams) { logMessage(` - ${team.name} (${team.slug})`); } }); export const login = new Command("login") .description("Login to Convex") .allowExcessArguments(false) .option( "--device-name <name>", "Provide a name for the device being authorized", ) .option( "-f, --force", "Proceed with login even if a valid access token already exists for this device", ) .option( "--no-open", "Don't automatically open the login link in the default browser", ) .addOption( new Option( "--login-flow <mode>", `How to log in; defaults to guessing based on the environment.`, ) .choices(["paste", "auto", "poll"] as const) .default("auto" as const), ) .addOption(new Option("--link-deployments").hideHelp()) // These options are hidden from the help/usage message, but allow overriding settings for testing. // Change the auth credentials with the auth provider .addOption(new Option("--override-auth-url <url>").hideHelp()) .addOption(new Option("--override-auth-client <id>").hideHelp()) .addOption(new Option("--override-auth-username <username>").hideHelp()) .addOption(new Option("--override-auth-password <password>").hideHelp()) // Skip the auth provider login and directly use this access token .addOption(new Option("--override-access-token <token>").hideHelp()) // Automatically accept opt ins without prompting .addOption(new Option("--accept-opt-ins").hideHelp()) // Dump the access token from the auth provider and skip authorization with Convex .addOption(new Option("--dump-access-token").hideHelp()) // Hidden option for tests to check if the user is logged in. .addOption(new Option("--check-login").hideHelp()) // Redirect to Vercel SSO integration URL .addOption( new Option( "--vercel", "Redirect to Vercel SSO integration for login", ).hideHelp(), ) // Override the Vercel URL slug (defaults to 'convex') .addOption(new Option("--vercel-override <slug>").hideHelp()) .addCommand(loginStatus) .addHelpCommand(false) .action(async (options, cmd: Command) => { const ctx = await oneoffContext({ url: undefined, adminKey: undefined, envFile: undefined, }); if ( !options.force && (await checkAuthorization(ctx, !!options.acceptOptIns)) ) { logFinishedStep( "This device has previously been authorized and is ready for use with Convex.", ); await handleLinkingDeployments(ctx, { interactive: !!options.linkDeployments, }); return; } if (!options.force && options.checkLogin) { const isLoggedIn = await checkAuthorization(ctx, !!options.acceptOptIns); if (!isLoggedIn) { return ctx.crash({ exitCode: 1, errorType: "fatal", errForSentry: "You are not logged in.", printedMessage: "You are not logged in.", }); } } if (!!options.overrideAuthUsername !== !!options.overrideAuthPassword) { cmd.error( "If overriding credentials, both username and password must be provided", ); } const uuid = loadUuidForAnonymousUser(ctx); await performLogin(ctx, { ...options, anonymousId: uuid, vercel: options.vercel, vercelOverride: options.vercelOverride, }); await handleLinkingDeployments(ctx, { interactive: !!options.linkDeployments, }); }); async function handleLinkingDeployments( ctx: Context, args: { interactive: boolean; }, ) { if (!shouldAllowAnonymousDevelopment()) { return; } const anonymousDeployments = await listExistingAnonymousDeployments(ctx); if (anonymousDeployments.length === 0) { if (args.interactive) { logMessage( "It doesn't look like you have any deployments to link. You can run `npx convex dev` to set up a new project or select an existing one.", ); } return; } if (!args.interactive) { const message = getMessage( anonymousDeployments.map((d) => d.deploymentName), ); const createProjects = await promptYesNo(ctx, { message, default: true, }); if (!createProjects) { logMessage( "Not linking your existing deployments. If you want to link them later, run `npx convex login --link-deployments`.", ); logMessage( `Visit ${DASHBOARD_HOST} or run \`npx convex dev\` to get started with your new account.`, ); return; } const { teamSlug } = await validateOrSelectTeam( ctx, undefined, "Choose a team for your deployments:", ); const projectsRemaining = await getProjectsRemaining(ctx, teamSlug); if (anonymousDeployments.length > projectsRemaining) { logFailure( `You have ${anonymousDeployments.length} deployments to link, but only have ${projectsRemaining} projects remaining. If you'd like to choose which ones to link, run this command with the --link-deployments flag.`, ); return; } const deploymentSelection = await getDeploymentSelection(ctx, { url: undefined, adminKey: undefined, envFile: undefined, }); const configuredDeployment = deploymentSelection.kind === "anonymous" ? deploymentSelection.deploymentName : null; let dashboardUrl = teamDashboardUrl(teamSlug); for (const deployment of anonymousDeployments) { const linkedDeployment = await handleLinkToProject(ctx, { deploymentName: deployment.deploymentName, teamSlug, projectSlug: null, }); logFinishedStep( `Added ${deployment.deploymentName} to project ${linkedDeployment.projectSlug}`, ); if (deployment.deploymentName === configuredDeployment) { // If the current project has a `CONVEX_DEPLOYMENT` env var configured, replace // it with the new value. await updateEnvAndConfigForDeploymentSelection( ctx, { url: linkedDeployment.deploymentUrl, deploymentName: linkedDeployment.deploymentName, teamSlug, projectSlug: linkedDeployment.projectSlug, deploymentType: "local", }, configuredDeployment, ); dashboardUrl = deploymentDashboardUrlPage( linkedDeployment.deploymentName, "", ); } } logFinishedStep( `Sucessfully linked your deployments! Visit ${dashboardUrl} to get started.`, ); return; } const deploymentSelection = await getDeploymentSelection(ctx, { url: undefined, adminKey: undefined, envFile: undefined, }); const configuredDeployment = deploymentSelection.kind === "anonymous" ? deploymentSelection.deploymentName : null; while (true) { logMessage( getDeploymentListMessage( anonymousDeployments.map((d) => d.deploymentName), ), ); const updatedAnonymousDeployments = await listExistingAnonymousDeployments(ctx); const deploymentToLink = await promptSearch(ctx, { message: "Which deployment would you like to link to your account?", choices: updatedAnonymousDeployments.map((d) => ({ name: d.deploymentName, value: d.deploymentName, })), }); const { teamSlug } = await validateOrSelectTeam( ctx, undefined, "Choose a team for your deployment:", ); const { projectSlug } = await selectProject(ctx, "ask", { team: teamSlug, devDeployment: "local", defaultProjectName: removeAnonymousPrefix(deploymentToLink), }); const linkedDeployment = await handleLinkToProject(ctx, { deploymentName: deploymentToLink, teamSlug, projectSlug, }); logFinishedStep( `Added ${deploymentToLink} to project ${linkedDeployment.projectSlug}`, ); if (deploymentToLink === configuredDeployment) { await updateEnvAndConfigForDeploymentSelection( ctx, { url: linkedDeployment.deploymentUrl, deploymentName: linkedDeployment.deploymentName, teamSlug, projectSlug: linkedDeployment.projectSlug, deploymentType: "local", }, configuredDeployment, ); } const shouldContinue = await promptYesNo(ctx, { message: "Would you like to link another deployment?", default: true, }); if (!shouldContinue) { break; } } } async function getProjectsRemaining(ctx: Context, teamSlug: string) { const response = await bigBrainAPI<{ projectsRemaining: number }>({ ctx, method: "GET", url: `teams/${teamSlug}/projects_remaining`, }); return response.projectsRemaining; } function getDeploymentListMessage(anonymousDeploymentNames: string[]) { let message = `You have ${anonymousDeploymentNames.length} existing deployments.`; message += `\n\nDeployments:`; for (const deploymentName of anonymousDeploymentNames) { message += `\n- ${deploymentName}`; } return message; } function getMessage(anonymousDeploymentNames: string[]) { if (anonymousDeploymentNames.length === 1) { return `Would you like to link your existing deployment to your account? ("${anonymousDeploymentNames[0]}")`; } let message = `You have ${anonymousDeploymentNames.length} existing deployments. Would you like to link them to your account?`; message += `\n\nDeployments:`; for (const deploymentName of anonymousDeploymentNames) { message += `\n- ${deploymentName}`; } message += `\n\nYou can alternatively run \`npx convex login --link-deployments\` to interactively choose which deployments to add.`; return message; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server