Skip to content

Commit e70d813

Browse files
kristof-siketclaude
andcommitted
feat(cli): add --region to project create and fix default region on new app deploys
Add --region <region> flag to `project create`, thread it through implicit project creation in `app deploy --create-project`, and remove the hardcoded DEFAULT_REGION ("eu-central-1") fallback so apps created without --region inherit the project's server-assigned default rather than always landing in Frankfurt. Also surface `defaultRegion` from the project API in `project show` output and add `defaultRegion` to ProjectRecord and ProjectSummary types. The selectRegion stub on DeployInteraction is removed entirely — it was dead code (interaction: undefined is always passed to sdk.deploy; the branch-app path calls createBranchApp directly before sdk.deploy, so sdk.deploy always receives a concrete appId and never reaches #resolveDeployTarget's region shortcut). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 10f6c43 commit e70d813

11 files changed

Lines changed: 264 additions & 21 deletions

File tree

docs/product/command-spec.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ prisma-cli project show --json
703703
prisma-cli project show --project proj_123 --json
704704
```
705705

706-
## `prisma-cli project create <name>`
706+
## `prisma-cli project create <name> --region <region>`
707707

708708
Purpose:
709709

@@ -713,6 +713,7 @@ Behavior:
713713

714714
- requires auth
715715
- creates a Project in the authenticated workspace
716+
- `--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
716717
- writes `.prisma/local.json` with Workspace and Project IDs
717718
- ensures `.prisma/` is ignored by Git
718719
- does not create a Branch, App, Deployment, database, or Git repository connection
@@ -722,6 +723,7 @@ Examples:
722723

723724
```bash
724725
prisma-cli project create my-app
726+
prisma-cli project create my-app --region us-east-1
725727
prisma-cli project create my-app --json
726728
```
727729

packages/cli/src/commands/project/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,18 @@ function createProjectCreateCommand(runtime: CliRuntime): Command {
186186
"project.create",
187187
);
188188

189-
command.argument("<name>", "Project name");
189+
command
190+
.argument("<name>", "Project name")
191+
.addOption(new Option("--region <region>", "Prisma Compute region id"));
190192
addGlobalFlags(command);
191193

192194
command.action(async (name, options) => {
195+
const region = (options as { region?: string }).region;
193196
await runCommand<ProjectSetupResult>(
194197
runtime,
195198
"project.create",
196199
options as Record<string, unknown>,
197-
(context) => runProjectCreate(context, String(name)),
200+
(context) => runProjectCreate(context, String(name), { region }),
198201
{
199202
renderHuman: (context, descriptor, result) =>
200203
renderProjectSetup(context, descriptor, result),

packages/cli/src/controllers/app.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk";
1818
import { matchError, Result } from "better-result";
1919
import open from "open";
2020
import { FileTokenStorage } from "../adapters/token-storage";
21-
import { DEFAULT_REGION } from "../lib/app/app-interaction";
2221
import {
2322
type AppRecord,
2423
createAppProvider,
@@ -645,6 +644,7 @@ async function runSingleAppDeploy(
645644
await requireProviderAndDeployProjectContext(context, options?.projectRef, {
646645
branch,
647646
createProjectName: options?.createProjectName,
647+
createProjectRegion: deployRegion?.value,
648648
envProjectId,
649649
localPin,
650650
});
@@ -2770,7 +2770,7 @@ async function resolveDeployAppByName(
27702770
matchedAnnotation: string;
27712771
newAnnotation: string;
27722772
requestedRegion: MergedDeployInput | undefined;
2773-
newAppRegion: string;
2773+
newAppRegion: string | undefined;
27742774
firstDeploy: boolean;
27752775
},
27762776
): Promise<{
@@ -2842,7 +2842,7 @@ async function resolveAmbiguousDeployApp(
28422842
matches: AppRecord[],
28432843
targetName: string,
28442844
requestedRegion: MergedDeployInput | undefined,
2845-
newAppRegion: string,
2845+
newAppRegion: string | undefined,
28462846
firstDeploy: boolean,
28472847
): Promise<{
28482848
appId?: string;
@@ -2923,8 +2923,8 @@ async function resolveAmbiguousDeployApp(
29232923

29242924
function deployNewAppRegion(
29252925
configRegion: MergedDeployInput | undefined,
2926-
): string {
2927-
return configRegion?.value ?? DEFAULT_REGION;
2926+
): string | undefined {
2927+
return configRegion?.value;
29282928
}
29292929

29302930
async function resolveExistingAppSelection(
@@ -3324,6 +3324,7 @@ async function requireProviderAndDeployProjectContext(
33243324
options: {
33253325
branch?: ResolvedDeployBranch;
33263326
createProjectName?: string;
3327+
createProjectRegion?: string;
33273328
envProjectId?: string;
33283329
localPin: LocalResolutionPinReadResult;
33293330
},
@@ -3417,6 +3418,7 @@ async function resolveDeployProjectContext(
34173418
options: {
34183419
branch?: ResolvedDeployBranch;
34193420
createProjectName?: string;
3421+
createProjectRegion?: string;
34203422
envProjectId?: string;
34213423
localPin: LocalResolutionPinReadResult;
34223424
},
@@ -3469,6 +3471,7 @@ async function resolveDeployProjectContext(
34693471
projectName,
34703472
workspace,
34713473
context.runtime.signal,
3474+
options.createProjectRegion,
34723475
);
34733476
return withRemoteDeployBranch(
34743477
provider,
@@ -3567,6 +3570,7 @@ async function resolveDeployProjectContext(
35673570
provider,
35683571
workspace,
35693572
projects,
3573+
options.createProjectRegion,
35703574
);
35713575
return withRemoteDeployBranch(
35723576
provider,
@@ -3588,6 +3592,7 @@ async function resolveInteractiveDeployProjectSetup(
35883592
provider: ReturnType<typeof createAppProvider>,
35893593
workspace: AuthWorkspace,
35903594
projects: ProjectCandidate[],
3595+
createProjectRegion?: string,
35913596
): Promise<Omit<ResolvedAppProjectContext, "branch">> {
35923597
const setup = await promptForProjectSetupChoice({
35933598
context,
@@ -3598,6 +3603,7 @@ async function resolveInteractiveDeployProjectSetup(
35983603
projectName,
35993604
workspace,
36003605
context.runtime.signal,
3606+
createProjectRegion,
36013607
),
36023608
cancel: {
36033609
why: "Deploy needs a Project before it can continue.",
@@ -3626,9 +3632,10 @@ async function createProjectForDeploySetup(
36263632
projectName: string,
36273633
workspace: AuthWorkspace,
36283634
signal: AbortSignal,
3635+
region?: string,
36293636
): Promise<ProjectCandidate> {
36303637
const created = await provider
3631-
.createProject({ name: projectName, signal })
3638+
.createProject({ name: projectName, region, signal })
36323639
.catch((error) => {
36333640
throw projectCreateFailedError(error, projectName, workspace, {
36343641
nextSteps: [

packages/cli/src/controllers/project.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export async function runProjectShow(
263263
export async function runProjectCreate(
264264
context: CommandContext,
265265
projectName: string,
266+
options?: { region?: string },
266267
): Promise<CommandSuccess<ProjectSetupResult>> {
267268
const authState = await requireAuthenticatedAuthState(context);
268269
const workspace = authState.workspace;
@@ -295,7 +296,11 @@ export async function runProjectCreate(
295296
const provider = createAppProvider(client);
296297
const name = projectName.trim();
297298
const created = await provider
298-
.createProject({ name, signal: context.runtime.signal })
299+
.createProject({
300+
name,
301+
region: options?.region,
302+
signal: context.runtime.signal,
303+
})
299304
.catch((error) => {
300305
throw projectCreateFailedError(error, name, workspace, {
301306
nextSteps: [
@@ -1444,6 +1449,9 @@ export async function listRealWorkspaceProjects(
14441449
...("url" in project && typeof project.url === "string"
14451450
? { url: project.url }
14461451
: {}),
1452+
...("defaultRegion" in project
1453+
? { defaultRegion: project.defaultRegion }
1454+
: {}),
14471455
slug:
14481456
"slug" in project && typeof project.slug === "string"
14491457
? project.slug

packages/cli/src/lib/app/app-interaction.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import type {
2-
AppInfo,
3-
DeployInteraction,
4-
RegionInfo,
5-
} from "@prisma/compute-sdk";
1+
import type { AppInfo, DeployInteraction } from "@prisma/compute-sdk";
62

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

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

138
export function createDeployInteraction(
149
context: CommandContext,
@@ -52,8 +47,5 @@ export function createDeployInteraction(
5247
!value?.trim() ? "App name is required" : undefined,
5348
}).then((value) => value.trim());
5449
},
55-
async selectRegion(_regions: RegionInfo[]): Promise<string> {
56-
return DEFAULT_REGION;
57-
},
5850
};
5951
}

packages/cli/src/lib/app/app-provider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface AppRecord {
3737
export interface ProjectRecord {
3838
id: string;
3939
name: string;
40+
defaultRegion?: string;
4041
}
4142

4243
export interface BranchRecord {
@@ -144,6 +145,7 @@ export class DomainApiError extends Error {
144145
export interface AppProvider {
145146
createProject(options: {
146147
name: string;
148+
region?: string;
147149
signal?: AbortSignal;
148150
}): Promise<ProjectRecord>;
149151
resolveBranch(
@@ -279,6 +281,7 @@ export function createAppProvider(
279281
async createProject(options) {
280282
const projectResult = await sdk.createProject({
281283
name: options.name,
284+
region: options.region,
282285
signal: options.signal,
283286
});
284287
if (projectResult.isErr()) {
@@ -288,6 +291,7 @@ export function createAppProvider(
288291
return {
289292
id: projectResult.value.id,
290293
name: projectResult.value.name,
294+
defaultRegion: projectResult.value.defaultRegion,
291295
};
292296
},
293297

packages/cli/src/presenters/project.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ function renderBoundProjectShow(
356356
lines.push(`${rail} ${ui.dim("→")} ${ui.link(result.project.url)}`);
357357
}
358358

359+
if (result.project.defaultRegion) {
360+
lines.push(
361+
`${rail} ${ui.accent(padDisplay("region", keyWidth))} ${ui.dim(result.project.defaultRegion)}`,
362+
);
363+
}
364+
359365
lines.push(
360366
...renderResolvedProjectContextBlock(context.ui, {
361367
workspace: result.workspace,

packages/cli/src/types/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface ProjectSummary {
44
id: string;
55
name: string;
66
url?: string;
7+
defaultRegion?: string | null;
78
}
89

910
export type ProjectSource =

packages/cli/tests/app-controller.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4066,7 +4066,7 @@ describe("app controller", () => {
40664066
);
40674067
});
40684068

4069-
it("creates a named new app with the default Frankfurt region in non-interactive mode", async () => {
4069+
it("omits region from deployApp when --region is not passed for a new app", async () => {
40704070
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
40714071
const listApps = vi.fn().mockResolvedValue([]);
40724072
const deployApp = vi.fn().mockResolvedValue({
@@ -4125,7 +4125,7 @@ describe("app controller", () => {
41254125
projectId: "proj_123",
41264126
appId: undefined,
41274127
appName: "hello-world",
4128-
region: "eu-central-1",
4128+
region: undefined,
41294129
interaction: undefined,
41304130
}),
41314131
);
@@ -4233,6 +4233,74 @@ describe("app controller", () => {
42334233
);
42344234
});
42354235

4236+
it("passes --region to createProject when --create-project and --region are used together", async () => {
4237+
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
4238+
const createProject = vi.fn(async (options: { name: string }) => ({
4239+
id: "proj_new",
4240+
name: options.name,
4241+
}));
4242+
const listApps = vi.fn().mockResolvedValue([]);
4243+
const deployApp = vi.fn().mockResolvedValue({
4244+
projectId: "proj_new",
4245+
app: {
4246+
id: "app_new",
4247+
name: "hello-world",
4248+
region: "us-east-1",
4249+
liveDeploymentId: "dep_123",
4250+
},
4251+
deployment: {
4252+
id: "dep_123",
4253+
status: "running",
4254+
url: "https://hello-world.prisma.app",
4255+
},
4256+
});
4257+
4258+
vi.doMock("../src/lib/auth/guard", () => ({
4259+
requireComputeAuth,
4260+
}));
4261+
vi.doMock("../src/lib/app/app-provider", () => ({
4262+
createAppProvider: vi.fn(() =>
4263+
withBranchDatabaseProviderDefaults({
4264+
resolveBranch: createResolveBranch(),
4265+
createProject,
4266+
listApps,
4267+
deployApp,
4268+
listDeployments: vi.fn(),
4269+
showDeployment: vi.fn(),
4270+
}),
4271+
),
4272+
}));
4273+
4274+
const { createTempCwd, createTestCommandContext } = await import(
4275+
"./helpers"
4276+
);
4277+
const { runAppDeploy } = await import("../src/controllers/app");
4278+
const cwd = await createTempCwd();
4279+
const stateDir = path.join(cwd, ".state");
4280+
const { context } = await createTestCommandContext({
4281+
cwd,
4282+
stateDir,
4283+
isTTY: false,
4284+
env: {
4285+
...process.env,
4286+
PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "",
4287+
PRISMA_CLI_MOCK_FIXTURE_PATH: undefined,
4288+
},
4289+
});
4290+
4291+
await runAppDeploy(context, "hello-world", {
4292+
createProjectName: "my-project",
4293+
region: "us-east-1",
4294+
framework: "hono",
4295+
});
4296+
4297+
expect(createProject).toHaveBeenCalledWith({
4298+
name: "my-project",
4299+
region: "us-east-1",
4300+
signal: context.runtime.signal,
4301+
});
4302+
});
4303+
42364304
it("reuses the created project on second deploy instead of creating another one", async () => {
42374305
const client = createProjectClient();
42384306
const requireComputeAuth = vi.fn().mockResolvedValue(client);

0 commit comments

Comments
 (0)