Skip to content

Commit 69e529d

Browse files
sshaderConvex, Inc.
authored and
Convex, Inc.
committed
Add ability to link tryitout deployments on login (#35877)
If someone tries Convex without an account and later decides to create an account, they can run `npx convex login` which will prompt the developer to link their "tryitout" deployments with their new account. By default, for each "tryitout" deployment, we'll create a new project and make the "tryitout" deployment the local dev deployment for the new project (so it'll have all the same data, but a different name, and it'll appear in dashboard.convex.dev when the user logs in there). Under the hood, this is * creating a project * calling `/local_deployment/start` on big brain to register the deployment as part of the project and get an instance name + admin key * copy the contents of `tryitout-foo` to `local-sarahs_team-foo` * update the config file of in `local-sarahs_team-foo` There's an interactive flow under `npx convex login --link-deployments` if the developer wants to link these one by one -- we prompt this if the developer has more "tryitout" deployments than projects available on their team. GitOrigin-RevId: 86983bea22755f5636d0f7931964506af4af0021
1 parent 5b349d8 commit 69e529d

File tree

9 files changed

+396
-14
lines changed

9 files changed

+396
-14
lines changed

npm-packages/convex/src/cli/configure.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export async function _deploymentCredentialsOrConfigure(
247247
return await ctx.crash({
248248
exitCode: 0,
249249
errorType: "fatal",
250-
printedMessage: `Run \`npx convex login\` first to link this deployment to your account, and then run \`npx convex dev\` again.`,
250+
printedMessage: `Run \`npx convex login --link-deployments\` first to link this deployment to your account, and then run \`npx convex dev\` again.`,
251251
});
252252
}
253253
return await handleChooseProject(
@@ -453,7 +453,7 @@ export async function handleManuallySetUrlAndAdminKey(
453453
return { url, adminKey };
454454
}
455455

456-
async function selectProject(
456+
export async function selectProject(
457457
ctx: Context,
458458
chosenConfiguration: ChosenConfiguration,
459459
cmdOptions: {
@@ -463,6 +463,7 @@ async function selectProject(
463463
local?: boolean | undefined;
464464
cloud?: boolean | undefined;
465465
partitionId?: number;
466+
defaultProjectName?: string | undefined;
466467
},
467468
): Promise<{
468469
teamSlug: string;
@@ -499,6 +500,7 @@ async function selectNewProject(
499500
cloud?: boolean | undefined;
500501
local?: boolean | undefined;
501502
partitionId?: number | undefined;
503+
defaultProjectName?: string | undefined;
502504
},
503505
) {
504506
const { teamSlug: selectedTeam, chosen: didChooseBetweenTeams } =
@@ -508,7 +510,7 @@ async function selectNewProject(
508510
if (!config.project) {
509511
projectName = await promptString(ctx, {
510512
message: "Project name:",
511-
default: cwd,
513+
default: config.defaultProjectName || cwd,
512514
});
513515
choseProjectInteractively = true;
514516
}
@@ -714,7 +716,7 @@ async function ensureDeploymentProvisioned(
714716
}
715717
}
716718

717-
async function updateEnvAndConfigForDeploymentSelection(
719+
export async function updateEnvAndConfigForDeploymentSelection(
718720
ctx: Context,
719721
options: {
720722
url: string;

npm-packages/convex/src/cli/dashboard.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
DEFAULT_LOCAL_DASHBOARD_API_PORT,
2121
checkIfDashboardIsRunning,
2222
} from "./lib/localDeployment/dashboard.js";
23-
const DASHBOARD_HOST = process.env.CONVEX_PROVISION_HOST
23+
24+
export const DASHBOARD_HOST = process.env.CONVEX_PROVISION_HOST
2425
? "http://localhost:6789"
2526
: "https://dashboard.convex.dev";
2627

npm-packages/convex/src/cli/lib/deployment.ts

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ export function isTryItOutDeployment(deploymentName: string) {
3232
return deploymentName.startsWith("tryitout-");
3333
}
3434

35+
export function removeTryItOutPrefix(deploymentName: string) {
36+
if (isTryItOutDeployment(deploymentName)) {
37+
return deploymentName.slice("tryitout-".length);
38+
}
39+
return deploymentName;
40+
}
41+
3542
export async function writeDeploymentEnvVar(
3643
ctx: Context,
3744
deploymentType: DeploymentType,

npm-packages/convex/src/cli/lib/deploymentSelection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ export const deploymentNameAndTypeFromSelection = (
616616
return null;
617617
};
618618

619-
const shouldAllowTryItOut = (): boolean => {
619+
export const shouldAllowTryItOut = (): boolean => {
620620
// Temporary flag while we build out this flow
621621
return process.env.CONVEX_TRY_IT_OUT !== undefined;
622622
};

npm-packages/convex/src/cli/lib/fsUtils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function recursivelyDelete(
5050
}
5151
}
5252

53-
export async function recusivelyCopy(
53+
export async function recursivelyCopy(
5454
ctx: Context,
5555
nodeFs: NodeFs,
5656
src: string,
@@ -60,7 +60,7 @@ export async function recusivelyCopy(
6060
if (st.isDirectory()) {
6161
nodeFs.mkdir(dest, { recursive: true });
6262
for (const entry of nodeFs.listDir(src)) {
63-
await recusivelyCopy(
63+
await recursivelyCopy(
6464
ctx,
6565
nodeFs,
6666
path.join(src, entry.name),

npm-packages/convex/src/cli/lib/localDeployment/download.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { promisify } from "util";
2020
import { Readable } from "stream";
2121
import { TempPath, nodeFs, withTmpDir } from "../../../bundler/fs.js";
2222
import { components } from "@octokit/openapi-types";
23-
import { recursivelyDelete, recusivelyCopy } from "../fsUtils.js";
23+
import { recursivelyDelete, recursivelyCopy } from "../fsUtils.js";
2424
import { LocalDeploymentError } from "./errors.js";
2525
import ProgressBar from "progress";
2626
import path from "path";
@@ -360,7 +360,7 @@ async function _ensureDashboardDownloaded(ctx: Context, version: string) {
360360
filename: "dashboard.zip",
361361
nameForLogging: "Convex dashboard",
362362
onDownloadComplete: async (ctx, unzippedPath) => {
363-
await recusivelyCopy(ctx, nodeFs, unzippedPath, outDir);
363+
await recursivelyCopy(ctx, nodeFs, unzippedPath, outDir);
364364
logVerbose(ctx, "Copied into out dir");
365365
},
366366
});

npm-packages/convex/src/cli/lib/localDeployment/filePaths.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function loadDeploymentConfig(
7171
): LocalDeploymentConfig | null {
7272
const dir = deploymentStateDir(deploymentKind, deploymentName);
7373
const configFile = path.join(dir, "config.json");
74-
if (!ctx.fs.stat(dir).isDirectory()) {
74+
if (!ctx.fs.exists(dir) || !ctx.fs.stat(dir).isDirectory()) {
7575
logVerbose(ctx, `Deployment ${deploymentName} not found`);
7676
return null;
7777
}

npm-packages/convex/src/cli/lib/localDeployment/tryitout.ts

+142-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44
import path from "path";
55
import {
66
Context,
7+
logFinishedStep,
78
logMessage,
89
logVerbose,
910
logWarning,
1011
} from "../../../bundler/context.js";
1112
import { promptSearch, promptString, promptYesNo } from "../utils/prompts.js";
12-
import { bigBrainGenerateTryItOutAdminKey } from "./bigBrain.js";
13+
import {
14+
bigBrainGenerateTryItOutAdminKey,
15+
bigBrainPause,
16+
bigBrainStart,
17+
} from "./bigBrain.js";
1318
import { LocalDeploymentError, printLocalDeploymentOnError } from "./errors.js";
14-
import { loadDeploymentConfig } from "./filePaths.js";
19+
import {
20+
LocalDeploymentKind,
21+
deploymentStateDir,
22+
loadDeploymentConfig,
23+
saveDeploymentConfig,
24+
} from "./filePaths.js";
1525
import { rootDeploymentStateDir } from "./filePaths.js";
1626
import { LocalDeploymentConfig } from "./filePaths.js";
1727
import { DeploymentDetails } from "./localDeployment.js";
@@ -26,8 +36,12 @@ import {
2636
} from "./utils.js";
2737
import { handleDashboard } from "./dashboard.js";
2838
import crypto from "crypto";
39+
import { recursivelyDelete, recursivelyCopy } from "../fsUtils.js";
2940
import { ensureBackendBinaryDownloaded } from "./download.js";
3041
import { isTryItOutDeployment } from "../deployment.js";
42+
import { createProject } from "../api.js";
43+
import { removeTryItOutPrefix } from "../deployment.js";
44+
import { nodeFs } from "../../../bundler/fs.js";
3145

3246
export async function handleTryItOutDeployment(
3347
ctx: Context,
@@ -374,3 +388,129 @@ async function getUniqueName(
374388
printedMessage: `Could not generate a unique name for your deployment, please choose a different name`,
375389
});
376390
}
391+
/**
392+
* This takes a "try it out" deployment and makes it a "local" deployment
393+
* that is associated with a project in the given team.
394+
*/
395+
export async function handleLinkToProject(
396+
ctx: Context,
397+
args: {
398+
deploymentName: string;
399+
teamSlug: string;
400+
projectSlug: string | null;
401+
},
402+
): Promise<{
403+
deploymentName: string;
404+
deploymentUrl: string;
405+
projectSlug: string;
406+
}> {
407+
logVerbose(
408+
ctx,
409+
`Linking ${args.deploymentName} to a project in team ${args.teamSlug}`,
410+
);
411+
const config = await loadDeploymentConfig(
412+
ctx,
413+
"tryItOut",
414+
args.deploymentName,
415+
);
416+
if (config === null) {
417+
return ctx.crash({
418+
exitCode: 1,
419+
errorType: "fatal",
420+
printedMessage: "Failed to load deployment config",
421+
});
422+
}
423+
await ensureBackendStopped(ctx, {
424+
ports: {
425+
cloud: config.ports.cloud,
426+
},
427+
deploymentName: args.deploymentName,
428+
allowOtherDeployments: true,
429+
maxTimeSecs: 5,
430+
});
431+
const projectName = removeTryItOutPrefix(args.deploymentName);
432+
let projectSlug: string;
433+
if (args.projectSlug !== null) {
434+
projectSlug = args.projectSlug;
435+
} else {
436+
const { projectSlug: newProjectSlug } = await createProject(ctx, {
437+
teamSlug: args.teamSlug,
438+
projectName,
439+
deploymentTypeToProvision: "prod",
440+
});
441+
projectSlug = newProjectSlug;
442+
}
443+
logVerbose(ctx, `Creating local deployment in project ${projectSlug}`);
444+
// Register it in big brain
445+
const { deploymentName: localDeploymentName, adminKey } = await bigBrainStart(
446+
ctx,
447+
{
448+
port: config.ports.cloud,
449+
projectSlug,
450+
teamSlug: args.teamSlug,
451+
instanceName: null,
452+
},
453+
);
454+
const localConfig = loadDeploymentConfig(ctx, "local", localDeploymentName);
455+
if (localConfig !== null) {
456+
return ctx.crash({
457+
exitCode: 1,
458+
errorType: "fatal",
459+
printedMessage: `Project ${projectSlug} already has a local deployment, so we cannot link this try-it-out deployment to it.`,
460+
});
461+
}
462+
logVerbose(ctx, `Moving ${args.deploymentName} to ${localDeploymentName}`);
463+
await moveDeployment(
464+
ctx,
465+
{
466+
deploymentKind: "tryItOut",
467+
deploymentName: args.deploymentName,
468+
},
469+
{
470+
deploymentKind: "local",
471+
deploymentName: localDeploymentName,
472+
},
473+
);
474+
logVerbose(ctx, `Saving deployment config for ${localDeploymentName}`);
475+
await saveDeploymentConfig(ctx, "local", localDeploymentName, {
476+
adminKey,
477+
backendVersion: config.backendVersion,
478+
ports: config.ports,
479+
});
480+
await bigBrainPause(ctx, {
481+
projectSlug,
482+
teamSlug: args.teamSlug,
483+
});
484+
logFinishedStep(
485+
ctx,
486+
`Linked ${args.deploymentName} to project ${projectSlug}`,
487+
);
488+
return {
489+
projectSlug,
490+
deploymentName: localDeploymentName,
491+
deploymentUrl: localDeploymentUrl(config.ports.cloud),
492+
};
493+
}
494+
495+
export async function moveDeployment(
496+
ctx: Context,
497+
oldDeployment: {
498+
deploymentKind: LocalDeploymentKind;
499+
deploymentName: string;
500+
},
501+
newDeployment: {
502+
deploymentKind: LocalDeploymentKind;
503+
deploymentName: string;
504+
},
505+
) {
506+
const oldPath = deploymentStateDir(
507+
oldDeployment.deploymentKind,
508+
oldDeployment.deploymentName,
509+
);
510+
const newPath = deploymentStateDir(
511+
newDeployment.deploymentKind,
512+
newDeployment.deploymentName,
513+
);
514+
await recursivelyCopy(ctx, nodeFs, oldPath, newPath);
515+
recursivelyDelete(ctx, oldPath);
516+
}

0 commit comments

Comments
 (0)