convex
Version:
Client for the Convex Cloud
293 lines (269 loc) • 10.4 kB
text/typescript
import { Command } from "@commander-js/extra-typings";
import { Context, oneoffContext } from "../bundler/context.js";
import { loadSelectedDeploymentCredentials } from "./lib/api.js";
import { logFinishedStep } from "../bundler/log.js";
import { announceDeploymentTarget } from "./lib/announceDeploymentTarget.js";
import {
DeploymentSelection,
getDeploymentSelection,
deploymentNameFromSelection,
} from "./lib/deploymentSelection.js";
import {
parseDeploymentSelector,
ParsedDeploymentSelector,
} from "./lib/deploymentSelector.js";
import { updateEnvAndConfigForDeploymentSelection } from "./configure.js";
import { fetchDeploymentCanonicalUrls } from "./lib/deploy2.js";
import {
loadProjectLocalConfig,
saveDeploymentConfig,
} from "./lib/localDeployment/filePaths.js";
import {
checkLocalConfigMatchesProject,
getCloudProjectSlugsBestEffort,
pauseLocalDeploymentBestEffort,
targetProjectForLocalSelector,
} from "./lib/localDeployment/projectMismatch.js";
import { bigBrainStart } from "./lib/localDeployment/bigBrain.js";
import { promptYesNo } from "./lib/utils/prompts.js";
import { createLocalDeployment } from "./deploymentCreate.js";
import { chalkStderr } from "chalk";
import { logWarning } from "../bundler/log.js";
export const deploymentSelect = new Command("select")
.summary("Select the deployment to use when running commands")
.description(
[
"Select the deployment to use when running commands.",
"",
"The deployment will be used by all `npx convex` commands, except `npx convex deploy`. You can also run individual commands on another deployment by using the --deployment flag on that command.",
"",
"• Select your personal cloud dev deployment in the current project: `npx convex deployment select dev`",
"• Select your local deployment: `npx convex deployment select local`",
"• Select a deployment in the same project by its reference: `npx convex deployment select dev/james`",
"• Select a deployment in another project in the same team: `npx convex deployment select some-project:dev/james`",
"• Select a deployment in a particular team/project: `npx convex deployment select some-team:some-project:dev/james`",
].join("\n"),
)
.argument("<deployment>", "The deployment to use")
.allowExcessArguments(false)
.action(async (selector) => {
const ctx = await oneoffContext({
url: undefined,
adminKey: undefined,
envFile: undefined,
});
// Get the current deployment selection (no flags, just env/config state)
const currentSelection = await getDeploymentSelection(ctx, {});
const parsed = parseDeploymentSelector(selector);
const isLocalSelector = isLocalDeploymentSelector(parsed);
// If no project is configured and the selector needs project context, show a specific error
if (
currentSelection.kind === "chooseProject" &&
parsed.kind !== "inTeamProject" &&
parsed.kind !== "deploymentName" &&
!isLocalSelector
) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No project configured. Run \`npx convex dev\` to set up a project first, or use a full selector like 'my-team:my-project:dev/james' or 'happy-capybara-123'.`,
});
}
if (isLocalSelector) {
await handleLocalSelect(ctx, selector, parsed, currentSelection);
return;
}
// Resolve the new deployment using the selector relative to the current project
const newSelection = await getDeploymentSelection(ctx, {
url: undefined,
adminKey: undefined,
envFile: undefined,
deployment: selector,
});
const deployment = await saveSelectedDeployment(
ctx,
selector,
newSelection,
deploymentNameFromSelection(currentSelection),
);
logFinishedStep("Selected deployment:");
announceDeploymentTarget(null, deployment);
});
function isLocalDeploymentSelector(parsed: ParsedDeploymentSelector): boolean {
return (
(parsed.kind === "inCurrentProject" ||
parsed.kind === "inProject" ||
parsed.kind === "inTeamProject") &&
parsed.selector.kind === "local"
);
}
async function handleLocalSelect(
ctx: Context,
selector: string,
parsed: ParsedDeploymentSelector,
currentSelection: DeploymentSelection,
): Promise<void> {
const existing = loadProjectLocalConfig(ctx);
if (existing === null) {
// No local deployment on disk. Offer to create one (interactive only).
if (!process.stdin.isTTY) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No local deployment found. Run ${chalkStderr.bold("npx convex deployment create local")} to create one.`,
});
}
if (
currentSelection.kind === "chooseProject" &&
parsed.kind !== "inTeamProject"
) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No project configured. Run \`npx convex dev\` to set up a project first.`,
});
}
// Refusing to create a project if the user didn’t explicitly specify a team.
if (parsed.kind === "inProject") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No local deployment found. To create one in ${chalkStderr.bold(parsed.projectSlug)}, run ${chalkStderr.bold(`npx convex deployment create local --project ${parsed.projectSlug}`)}, or use a fully qualified selector like ${chalkStderr.bold(`my-team:${parsed.projectSlug}:local`)}.`,
});
}
const wantsToCreate = await promptYesNo(ctx, {
message: "No local deployment found. Create one now?",
default: true,
});
if (!wantsToCreate) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `No local deployment found. Run ${chalkStderr.bold("npx convex deployment create local")} to create one.`,
});
}
const teamAndProject = teamAndProjectFromParsed(parsed);
await createLocalDeployment(ctx, currentSelection, true, teamAndProject);
return;
}
// Resolve the target cloud project the user is asking about
const target = await targetProjectForLocalSelector(
ctx,
parsed,
currentSelection,
);
let resolvedDeploymentName = existing.deploymentName;
if (target !== null) {
const match = checkLocalConfigMatchesProject(ctx, existing.config, target);
if (match === "mismatch") {
// The on-disk local deployment is tied to a different cloud project.
// Move it to the new project (warn, pause old, re-register).
const oldProjectId = existing.config.cloudProjectId!;
const oldProject = await getCloudProjectSlugsBestEffort(
ctx,
oldProjectId,
);
const oldProjectLabel =
oldProject !== null
? `project ${chalkStderr.bold(`${oldProject.teamSlug}:${oldProject.slug}`)}`
: `an unknown cloud project (ID ${oldProjectId})`;
logWarning(
chalkStderr.yellow(
`⚠️ This local deployment was previously in ${oldProjectLabel}. Moving it to project ${chalkStderr.bold(`${target.teamSlug}:${target.slug}`)}.`,
),
);
await pauseLocalDeploymentBestEffort(ctx, oldProject);
const { deploymentName: newDeploymentName } = await bigBrainStart(ctx, {
port: existing.config.ports.cloud,
teamSlug: target.teamSlug,
projectSlug: target.slug,
instanceName: null,
});
saveDeploymentConfig(ctx, "local", newDeploymentName, {
...existing.config,
cloudProjectId: target.id,
});
resolvedDeploymentName = newDeploymentName;
} else if (match === "skip") {
// The on-disk config has no `cloudProjectId` — write the resolved id back
// so future invocations have it.
saveDeploymentConfig(ctx, "local", existing.deploymentName, {
...existing.config,
cloudProjectId: target.id,
});
}
}
const newSelection: DeploymentSelection = {
kind: "deploymentWithinProject",
targetProject: {
kind: "deploymentName",
deploymentName: resolvedDeploymentName,
deploymentType: "local",
},
selectionWithinProject: {
kind: "deploymentSelector",
selector,
},
};
await saveSelectedDeployment(
ctx,
selector,
newSelection,
deploymentNameFromSelection(currentSelection),
);
}
function teamAndProjectFromParsed(
parsed: ParsedDeploymentSelector,
): { teamSlug: string; projectSlug: string } | null {
if (parsed.kind === "inTeamProject") {
return { teamSlug: parsed.teamSlug, projectSlug: parsed.projectSlug };
}
return null;
}
export async function saveSelectedDeployment(
ctx: Context,
selector: string,
selection: DeploymentSelection,
previousDeploymentName: string | null,
) {
const deployment = await loadSelectedDeploymentCredentials(ctx, selection, {
ensureLocalRunning: false,
});
if (deployment.deploymentFields === null) {
// Should be unreachable since for now, `select` only allows users
// to select deployments that exist in Big Brain
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: null,
errForSentry: `Unexpected selection in select: ${JSON.stringify(deployment)}`,
});
}
if (deployment.deploymentFields.deploymentType === "prod") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Selecting a production deployment is unsupported. To run commands on a production deployment, pass the ${chalkStderr.bold(`--deployment ${selector}`)} flag to each command.`,
});
}
const { convexSiteUrl: siteUrl } =
deployment.deploymentFields.deploymentType === "local"
? { convexSiteUrl: null }
: await fetchDeploymentCanonicalUrls(ctx, {
adminKey: deployment.adminKey,
deploymentUrl: deployment.url,
});
await updateEnvAndConfigForDeploymentSelection(
ctx,
{
url: deployment.url,
siteUrl,
deploymentName: deployment.deploymentFields.deploymentName,
teamSlug: deployment.deploymentFields.teamSlug,
projectSlug: deployment.deploymentFields.projectSlug,
deploymentType: deployment.deploymentFields.deploymentType,
},
previousDeploymentName,
);
return deployment;
}