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>(cmd: T): T { return cmd .addOption( new Option( "--type ", "Manage default env vars for the given deployment type (dev, preview, prod) instead of inferring from the current deployment.", ), ) .addOption( new Option( "--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] ") .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 ", "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("") .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("") .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 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 { 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, ): CloudDeploymentType { if (deploymentType === "local") return "dev"; return deploymentType; }