convex
Version:
Client for the Convex Cloud
294 lines (276 loc) • 10.6 kB
text/typescript
import { Command, Option } from "@commander-js/extra-typings";
import {
envGet,
envList,
envRemove,
envSet,
EnvVarBackend,
} from "./lib/env.js";
import { ensureHasConvexDependency } from "./lib/utils/utils.js";
import { defaultEnvBackend } from "./lib/defaultEnv.js";
import {
CloudDeploymentType,
DeploymentSelectionOptions,
DeploymentType,
fetchTeamAndProject,
} from "./lib/api.js";
import { Context, oneoffContext } from "../bundler/context.js";
import { selectEnvDeployment } from "./env.js";
import { getProjectDetails } from "./lib/deploymentSelection.js";
type EnvDefaultExtraOptions = {
type?: string;
project?: string;
};
function addEnvDefaultOptions<T extends Command<any, any>>(cmd: T): T {
return cmd
.addOption(
new Option(
"--type <type>",
"Manage default env vars for the given deployment type (dev, preview, prod) instead of inferring from the current deployment.",
),
)
.addOption(
new Option(
"--project <project>",
"Select a project manually. Accepts `team-slug:project-slug` or just `project-slug` (team inferred from your current project). Requires --type.",
),
) as T;
}
const envDefaultSet = addEnvDefaultOptions(
new Command("set")
.usage("[options] <name> <value>")
.arguments("[name] [value]")
.summary("Set a default variable")
.description(
"Set default environment variables for your project's deployment type.\n\n" +
" npx convex env default set NAME 'value'\n" +
" npx convex env default set NAME # omit a value to set one interactively\n" +
" npx convex env default set NAME --from-file value.txt\n" +
" npx convex env default set --from-file .env.defaults\n" +
"When setting multiple values, it will refuse all changes if any " +
"variables are already set to different values by default. " +
"Pass --force to overwrite the provided values.\n" +
"The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.\n",
)
.option(
"--from-file <file>",
"Read environment variables from a .env file. Without --force, fails if any existing variable has a different value.",
)
.option(
"--force",
"When setting multiple variables, overwrite existing environment variable values instead of failing on mismatch.",
)
.configureHelp({ showGlobalOptions: true })
.allowExcessArguments(false),
).action(async (name, value, cmdOptions, cmd) => {
const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
EnvDefaultExtraOptions;
const { ctx, backend } = await resolveEnvDefaultBackend(options);
await ensureHasConvexDependency(ctx, "env default set");
const didAnything = await envSet(ctx, backend, name, value, cmdOptions);
if (didAnything === false) {
cmd.outputHelp({ error: true });
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "error: No environment variables specified to be set.",
});
}
});
const envDefaultGet = addEnvDefaultOptions(
new Command("get")
.arguments("<name>")
.summary("Print a default variable's value")
.description(
"Print a default variable's value: `npx convex env default get NAME`\n" +
"The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.",
)
.configureHelp({ showGlobalOptions: true })
.allowExcessArguments(false),
).action(async (envVarName, _options, cmd) => {
const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
EnvDefaultExtraOptions;
const { ctx, backend } = await resolveEnvDefaultBackend(options);
await ensureHasConvexDependency(ctx, "env default get");
await envGet(ctx, backend, envVarName);
});
const envDefaultRemove = addEnvDefaultOptions(
new Command("remove")
.alias("rm")
.alias("unset")
.arguments("<name>")
.summary("Unset a default variable")
.description(
"Unset a default variable: `npx convex env default remove NAME`\n" +
"If the variable doesn't exist, the command doesn't do anything and succeeds.\n" +
"The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.",
)
.configureHelp({ showGlobalOptions: true })
.allowExcessArguments(false),
).action(async (name, _options, cmd) => {
const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
EnvDefaultExtraOptions;
const { ctx, backend } = await resolveEnvDefaultBackend(options);
await ensureHasConvexDependency(ctx, "env default remove");
await envRemove(ctx, backend, name);
});
const envDefaultList = addEnvDefaultOptions(
new Command("list")
.summary("List all default variables")
.description(
"List all default variables: `npx convex env default list`\n" +
"The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.",
)
.configureHelp({ showGlobalOptions: true })
.allowExcessArguments(false),
).action(async (_options, cmd) => {
const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
EnvDefaultExtraOptions;
const { ctx, backend } = await resolveEnvDefaultBackend(options);
await ensureHasConvexDependency(ctx, "env default list");
await envList(ctx, backend);
});
export const envDefault = new Command("default")
.summary("Manage project-level default environment variables")
.description(
"Manage default environment variables for your project.\n\n" +
"The default environment variables read and written to by this command are the ones for the deployment type of the current deployment (i.e. dev in most cases), unless --type is provided.\n\n" +
" Set a default variable: `npx convex env default set NAME 'value'`\n" +
" Unset a default variable: `npx convex env default remove NAME`\n" +
" List all default variables: `npx convex env default list`\n" +
" Print a default variable's value: `npx convex env default get NAME`\n\n",
)
.addCommand(envDefaultSet)
.addCommand(envDefaultGet)
.addCommand(envDefaultRemove)
.addCommand(envDefaultList)
.helpCommand(false);
type ParsedProjectOption =
| { kind: "teamAndProject"; teamSlug: string; projectSlug: string }
| { kind: "projectOnly"; projectSlug: string };
async function resolveEnvDefaultBackend(
options: DeploymentSelectionOptions & EnvDefaultExtraOptions,
): Promise<{ ctx: Context; backend: EnvVarBackend }> {
const dtypeOverride = normalizeTypeOption(options.type);
if (options.project !== undefined) {
const parsedProject = parseProjectOption(options.project);
if (parsedProject === null) {
const ctx = await oneoffContext(options);
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
"error: --project must be `team-slug:project-slug` or `project-slug`.",
});
}
if (dtypeOverride === undefined) {
const ctx = await oneoffContext(options);
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "error: --project requires --type to also be set.",
});
}
let ctx: Context;
let resolved: { teamSlug: string; projectSlug: string };
if (parsedProject.kind === "teamAndProject") {
ctx = await oneoffContext(options);
resolved = {
teamSlug: parsedProject.teamSlug,
projectSlug: parsedProject.projectSlug,
};
} else {
const selected = await selectEnvDeployment(options);
ctx = selected.ctx;
if (selected.deployment.deploymentFields === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
"error: --project <project-slug> requires a current cloud deployment to infer the team from. Use `team-slug:project-slug` to specify the team explicitly.",
});
}
const { team } = await fetchTeamAndProject(
ctx,
selected.deployment.deploymentFields.deploymentName,
);
resolved = { teamSlug: team, projectSlug: parsedProject.projectSlug };
}
const details = await getProjectDetails(ctx, {
kind: "teamAndProjectSlugs",
teamSlug: resolved.teamSlug,
projectSlug: resolved.projectSlug,
});
return {
ctx,
backend: defaultEnvBackend(ctx, details.id, dtypeOverride),
};
}
const { ctx, deployment } = await selectEnvDeployment(options);
const backend = await resolveDefaultEnvBackend(
ctx,
deployment.deploymentFields,
dtypeOverride,
);
return { ctx, backend };
}
function normalizeTypeOption(
type: string | undefined,
): CloudDeploymentType | undefined {
if (type === undefined) return undefined;
if (type === "development") return "dev";
if (type === "production") return "prod";
return type as CloudDeploymentType;
}
function parseProjectOption(value: string): ParsedProjectOption | null {
const parts = value.split(":");
if (parts.length === 1 && parts[0].length > 0) {
return { kind: "projectOnly", projectSlug: parts[0] };
}
if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
return {
kind: "teamAndProject",
teamSlug: parts[0],
projectSlug: parts[1],
};
}
return null;
}
export async function resolveDefaultEnvBackend(
ctx: Context,
deploymentFields: {
deploymentName: string;
deploymentType: DeploymentType;
} | null,
dtypeOverride?: CloudDeploymentType,
): Promise<EnvVarBackend> {
if (deploymentFields === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
"Default environment variables are only available for cloud projects.",
});
}
if (deploymentFields.deploymentType === "anonymous") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
"Default environment variables are not available for anonymous deployments.",
});
}
const dtype =
dtypeOverride ?? resolveDefaultEnvDtype(deploymentFields.deploymentType);
const { projectId } = await fetchTeamAndProject(
ctx,
deploymentFields.deploymentName,
);
return defaultEnvBackend(ctx, projectId, dtype);
}
function resolveDefaultEnvDtype(
deploymentType: Exclude<DeploymentType, "anonymous">,
): CloudDeploymentType {
if (deploymentType === "local") return "dev";
return deploymentType;
}