Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ prisma-cli project show --json
prisma-cli project show --project proj_123 --json
```

## `prisma-cli project create <name>`
## `prisma-cli project create <name> [--region <region>]`

Purpose:

Expand All @@ -713,6 +713,7 @@ Behavior:

- requires auth
- creates a Project in the authenticated workspace
- `--region <region>` sets the Project's default Compute region; Apps created in the Project inherit this region unless overridden at deploy time with `--region`; when omitted the platform assigns the default region
- writes `.prisma/local.json` with Workspace and Project IDs
- ensures `.prisma/` is ignored by Git
- does not create a Branch, App, Deployment, database, or Git repository connection
Expand All @@ -722,6 +723,7 @@ Examples:

```bash
prisma-cli project create my-app
prisma-cli project create my-app --region us-east-1
prisma-cli project create my-app --json
```

Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,18 @@ function createProjectCreateCommand(runtime: CliRuntime): Command {
"project.create",
);

command.argument("<name>", "Project name");
command
.argument("<name>", "Project name")
.addOption(new Option("--region <region>", "Prisma Compute region id"));
addGlobalFlags(command);

command.action(async (name, options) => {
const region = (options as { region?: string }).region;
await runCommand<ProjectSetupResult>(
runtime,
"project.create",
options as Record<string, unknown>,
(context) => runProjectCreate(context, String(name)),
(context) => runProjectCreate(context, String(name), { region }),
{
renderHuman: (context, descriptor, result) =>
renderProjectSetup(context, descriptor, result),
Expand Down
23 changes: 17 additions & 6 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import { matchError, Result } from "better-result";
import open from "open";
import { FileTokenStorage } from "../adapters/token-storage";
import { DEFAULT_REGION } from "../lib/app/app-interaction";
import {
type AppRecord,
createAppProvider,
Expand Down Expand Up @@ -575,7 +574,7 @@
});
}

async function runSingleAppDeploy(

Check notice on line 577 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 19 detected (max: 15).
context: CommandContext,
appName: string | undefined,
options: AppDeployOptions | undefined,
Expand Down Expand Up @@ -645,6 +644,7 @@
await requireProviderAndDeployProjectContext(context, options?.projectRef, {
branch,
createProjectName: options?.createProjectName,
createProjectRegion: deployRegion?.value,
envProjectId,
localPin,
});
Expand Down Expand Up @@ -869,6 +869,7 @@
entrypoint ?? buildSettingsResolution.settings.entrypoint ?? null,
httpPort: runtime.port,
region: deployResult.app.region ?? selectedApp.region ?? null,
regionSource: deployRegion?.annotation ?? null,
envVars: envVarNames(envVars),
},
durationMs: deployDurationMs,
Expand Down Expand Up @@ -1170,9 +1171,9 @@
...deployment.deployment,
live: providerLiveDeploymentId
? deployment.deployment.id === providerLiveDeploymentId
: knownLiveDeploymentId
? deployment.deployment.id === knownLiveDeploymentId
: deployment.deployment.live,

Check notice on line 1176 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
},
},
warnings: [],
Expand Down Expand Up @@ -1551,10 +1552,10 @@
});
}

await sleep(
Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)),
context.runtime.signal,
);

Check notice on line 1558 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
current = await target.provider
.showDomain(current.id, { signal: context.runtime.signal })
.catch((error) => {
Expand Down Expand Up @@ -2116,7 +2117,7 @@
const compute = await resolveComputeManagementContext(
context,
options?.configTarget,
commandName.replace(/^app /, ""),

Check warning on line 2120 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
const branch = resolveDomainBranch(options?.branchName);
if (toBranchKind(branch.name) !== "production") {
Expand Down Expand Up @@ -2249,7 +2250,7 @@
}

function normalizeDomainHostname(hostname: string): string {
const normalized = hostname.trim().replace(/\.$/, "").toLowerCase();

Check warning on line 2253 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
if (!isValidDomainHostname(normalized)) {
throw new CliError({
code: "DOMAIN_HOSTNAME_INVALID",
Expand Down Expand Up @@ -2284,13 +2285,13 @@
}

return labels.every((label) =>
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label),

Check warning on line 2288 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

function sameDomainHostname(left: string, right: string): boolean {
return (
left.trim().replace(/\.$/, "").toLowerCase() ===

Check warning on line 2294 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
right.trim().replace(/\.$/, "").toLowerCase()
);
}
Expand Down Expand Up @@ -2379,7 +2380,7 @@
}
}

function domainCommandError(

Check notice on line 2383 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 26 detected (max: 15).
command: AppDomainCommand,
error: unknown,
hostname: string,
Expand Down Expand Up @@ -2516,7 +2517,7 @@
text.includes("no cname") ||
text.includes("cname record") ||
text.includes("no a/aaaa") ||
/\bcname(?:s)?\s+to\b/.test(text)

Check warning on line 2520 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
);
}

Expand Down Expand Up @@ -2545,7 +2546,7 @@

function extractDomainDnsTarget(error: DomainApiError): string | null {
const text = `${error.hint ?? ""} ${error.message}`;
const match = /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i.exec(text);

Check warning on line 2549 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
return match?.[1]?.toLowerCase() ?? null;
}

Expand Down Expand Up @@ -2581,7 +2582,7 @@
return 0;
}

const match = /^(\d+)(ms|s|m|h)$/.exec(trimmed);

Check warning on line 2585 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
if (!match) {
throw usageError(
`Invalid timeout "${value}"`,
Expand All @@ -2597,11 +2598,11 @@
const multiplier =
unit === "h"
? 60 * 60 * 1000
: unit === "m"
? 60 * 1000
: unit === "s"
? 1000
: 1;

Check notice on line 2605 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

Check notice on line 2605 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.
return amount * multiplier;
}

Expand Down Expand Up @@ -2770,7 +2771,7 @@
matchedAnnotation: string;
newAnnotation: string;
requestedRegion: MergedDeployInput | undefined;
newAppRegion: string;
newAppRegion: string | undefined;
firstDeploy: boolean;
},
): Promise<{
Expand Down Expand Up @@ -2842,7 +2843,7 @@
matches: AppRecord[],
targetName: string,
requestedRegion: MergedDeployInput | undefined,
newAppRegion: string,
newAppRegion: string | undefined,
firstDeploy: boolean,
): Promise<{
appId?: string;
Expand Down Expand Up @@ -2923,8 +2924,8 @@

function deployNewAppRegion(
configRegion: MergedDeployInput | undefined,
): string {
return configRegion?.value ?? DEFAULT_REGION;
): string | undefined {
return configRegion?.value;
}

async function resolveExistingAppSelection(
Expand Down Expand Up @@ -3324,6 +3325,7 @@
options: {
branch?: ResolvedDeployBranch;
createProjectName?: string;
createProjectRegion?: string;
envProjectId?: string;
localPin: LocalResolutionPinReadResult;
},
Expand Down Expand Up @@ -3375,7 +3377,7 @@
listProjects: () =>
listRealWorkspaceProjects(
client,
authState.workspace!,

Check warning on line 3380 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
context.runtime.signal,
),
commandName: options?.commandName,
Expand Down Expand Up @@ -3409,7 +3411,7 @@
};
}

async function resolveDeployProjectContext(

Check notice on line 3414 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/complexity/noExcessiveCognitiveComplexity

Excessive complexity of 18 detected (max: 15).
context: CommandContext,
client: ManagementApiClient,
provider: ReturnType<typeof createAppProvider>,
Expand All @@ -3417,6 +3419,7 @@
options: {
branch?: ResolvedDeployBranch;
createProjectName?: string;
createProjectRegion?: string;
envProjectId?: string;
localPin: LocalResolutionPinReadResult;
},
Expand Down Expand Up @@ -3469,6 +3472,7 @@
projectName,
workspace,
context.runtime.signal,
options.createProjectRegion,
);
return withRemoteDeployBranch(
provider,
Expand Down Expand Up @@ -3567,6 +3571,7 @@
provider,
workspace,
projects,
options.createProjectRegion,
);
return withRemoteDeployBranch(
provider,
Expand All @@ -3588,6 +3593,7 @@
provider: ReturnType<typeof createAppProvider>,
workspace: AuthWorkspace,
projects: ProjectCandidate[],
createProjectRegion?: string,
): Promise<Omit<ResolvedAppProjectContext, "branch">> {
const setup = await promptForProjectSetupChoice({
context,
Expand All @@ -3598,6 +3604,7 @@
projectName,
workspace,
context.runtime.signal,
createProjectRegion,
),
cancel: {
why: "Deploy needs a Project before it can continue.",
Expand Down Expand Up @@ -3626,9 +3633,10 @@
projectName: string,
workspace: AuthWorkspace,
signal: AbortSignal,
region?: string,
): Promise<ProjectCandidate> {
const created = await provider
.createProject({ name: projectName, signal })
.createProject({ name: projectName, region, signal })
.catch((error) => {
throw projectCreateFailedError(error, projectName, workspace, {
nextSteps: [
Expand All @@ -3646,6 +3654,9 @@
return {
id: created.id,
name: created.name,
...(created.defaultRegion != null
? { defaultRegion: created.defaultRegion }
: {}),
workspace,
};
}
Expand Down Expand Up @@ -4084,7 +4095,7 @@
continue;
}

const configFile = await detectFrameworkConfigFile(cwd, framework, signal);

Check notice on line 4098 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
if (
!configFile.exists &&
!hasAnyPackageDependency(packageJson, framework.detectPackages)
Expand All @@ -4097,8 +4108,8 @@
const annotation =
framework.key === "nextjs" && configFile.standalone
? "standalone output detected"
: configFile.exists
? `detected from ${path.basename(configFile.path!)}`

Check warning on line 4112 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
: "detected from package.json";

Check notice on line 4113 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNestedTernary

Do not nest ternary expressions.

return {
Expand All @@ -4121,10 +4132,10 @@
const filePath = path.join(cwd, candidate);
signal.throwIfAborted();
try {
const content = await readFile(filePath, { encoding: "utf8", signal });

Check notice on line 4135 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/noAwaitInLoops

Avoid using await inside loops.
return {
exists: true,
standalone: /\boutput\s*:\s*["'`]standalone["'`]/.test(content),

Check warning on line 4138 in packages/cli/src/controllers/app.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/performance/useTopLevelRegex

This regex literal is not defined in the top level scope. This can lead to performance issues if this function is called frequently.
path: filePath,
};
} catch (error) {
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/controllers/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export async function runProjectShow(
export async function runProjectCreate(
context: CommandContext,
projectName: string,
options?: { region?: string },
): Promise<CommandSuccess<ProjectSetupResult>> {
const authState = await requireAuthenticatedAuthState(context);
const workspace = authState.workspace;
Expand Down Expand Up @@ -295,7 +296,11 @@ export async function runProjectCreate(
const provider = createAppProvider(client);
const name = projectName.trim();
const created = await provider
.createProject({ name, signal: context.runtime.signal })
.createProject({
name,
region: options?.region,
signal: context.runtime.signal,
})
.catch((error) => {
throw projectCreateFailedError(error, name, workspace, {
nextSteps: [
Expand All @@ -314,6 +319,9 @@ export async function runProjectCreate(
{
id: created.id,
name: created.name,
...(created.defaultRegion != null
? { defaultRegion: created.defaultRegion }
: {}),
},
"created",
);
Expand Down Expand Up @@ -1444,6 +1452,9 @@ export async function listRealWorkspaceProjects(
...("url" in project && typeof project.url === "string"
? { url: project.url }
: {}),
...("defaultRegion" in project
? { defaultRegion: project.defaultRegion }
: {}),
slug:
"slug" in project && typeof project.slug === "string"
? project.slug
Expand Down
10 changes: 1 addition & 9 deletions packages/cli/src/lib/app/app-interaction.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import type {
AppInfo,
DeployInteraction,
RegionInfo,
} from "@prisma/compute-sdk";
import type { AppInfo, DeployInteraction } from "@prisma/compute-sdk";

import { selectPrompt, textPrompt } from "../../shell/prompt";
import type { CommandContext } from "../../shell/runtime";

const CREATE_NEW_APP = "__create_new_app__";
export const DEFAULT_REGION = "eu-central-1";

export function createDeployInteraction(
context: CommandContext,
Expand Down Expand Up @@ -52,8 +47,5 @@ export function createDeployInteraction(
!value?.trim() ? "App name is required" : undefined,
}).then((value) => value.trim());
},
async selectRegion(_regions: RegionInfo[]): Promise<string> {
return DEFAULT_REGION;
},
};
}
4 changes: 4 additions & 0 deletions packages/cli/src/lib/app/app-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface AppRecord {
export interface ProjectRecord {
id: string;
name: string;
defaultRegion?: string;
}

export interface BranchRecord {
Expand Down Expand Up @@ -144,6 +145,7 @@ export class DomainApiError extends Error {
export interface AppProvider {
createProject(options: {
name: string;
region?: string;
signal?: AbortSignal;
}): Promise<ProjectRecord>;
resolveBranch(
Expand Down Expand Up @@ -279,6 +281,7 @@ export function createAppProvider(
async createProject(options) {
const projectResult = await sdk.createProject({
name: options.name,
region: options.region,
signal: options.signal,
});
if (projectResult.isErr()) {
Expand All @@ -288,6 +291,7 @@ export function createAppProvider(
return {
id: projectResult.value.id,
name: projectResult.value.name,
defaultRegion: projectResult.value.defaultRegion,
};
},

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/lib/project/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,11 +753,14 @@ function buildProjectRecoveryCommands(
}

function toProjectSummary(
project: Pick<ProjectCandidate, "id" | "name" | "url">,
project: Pick<ProjectCandidate, "id" | "name" | "url" | "defaultRegion">,
): ProjectSummary {
return {
id: project.id,
name: project.name,
...(project.url ? { url: project.url } : {}),
...(project.defaultRegion != null
? { defaultRegion: project.defaultRegion }
: {}),
};
}
5 changes: 4 additions & 1 deletion packages/cli/src/lib/project/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,15 @@ function localStateWriteFailedError(
}

export function toProjectSummary(
project: Pick<ProjectCandidate, "id" | "name" | "url">,
project: Pick<ProjectCandidate, "id" | "name" | "url" | "defaultRegion">,
): ProjectSummary {
return {
id: project.id,
name: project.name,
...(project.url ? { url: project.url } : {}),
...(project.defaultRegion != null
? { defaultRegion: project.defaultRegion }
: {}),
};
}

Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/presenters/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ export function renderAppDeploy(
},
]),
{ label: "Logs", value: logsCommand },
...(result.deploySettings.region &&
result.deploySettings.regionSource === null
? [
{
label: "Region",
value: result.deploySettings.region,
origin:
result.project.defaultRegion != null
? "project default"
: "platform default — pass --region to choose",
},
]
: []),
...(deployUsedComputeConfig(result)
? []
: [
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/presenters/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,20 @@ export function renderProjectList(
"name".length,
...result.projects.map((project) => stringWidth(project.name)),
);
const idWidth = Math.max(
"id".length,
...result.projects.map((project) => project.id.length),
);
lines.push(rail);
lines.push(
`${rail} ${ui.accent(padDisplay("name", nameWidth))} ${ui.accent("id")}`,
`${rail} ${ui.accent(padDisplay("name", nameWidth))} ${ui.accent(padDisplay("id", idWidth))} ${ui.accent("region")}`,
);
for (const project of result.projects) {
const region = project.defaultRegion
? project.defaultRegion
: ui.dim("none");
lines.push(
`${rail} ${padDisplay(project.name, nameWidth)} ${project.id}`,
`${rail} ${padDisplay(project.name, nameWidth)} ${padDisplay(project.id, idWidth)} ${region}`,
);
}

Expand Down Expand Up @@ -356,6 +363,12 @@ function renderBoundProjectShow(
lines.push(`${rail} ${ui.dim("→")} ${ui.link(result.project.url)}`);
}

if (result.project.defaultRegion) {
lines.push(
`${rail} ${ui.accent(padDisplay("region", keyWidth))} ${ui.dim(result.project.defaultRegion)}`,
);
}

lines.push(
...renderResolvedProjectContextBlock(context.ui, {
workspace: result.workspace,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface AppDeploySettings {
entrypoint: string | null;
httpPort: number;
region: string | null;
/** Annotation from the deploy input that produced region (e.g. "set by --region"). Null when the server assigned the region with no explicit input. */
regionSource: string | null;
envVars: string[];
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface ProjectSummary {
id: string;
name: string;
url?: string;
defaultRegion?: string | null;
}

export type ProjectSource =
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/use-cases/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,15 @@ function toProjectSummary(project: {
id: string;
name: string;
url?: string;
defaultRegion?: string | null;
}): ProjectSummary {
return {
id: project.id,
name: project.name,
...(project.url ? { url: project.url } : {}),
...(project.defaultRegion != null
? { defaultRegion: project.defaultRegion }
: {}),
};
}

Expand Down
Loading
Loading