From e70d813982d0df4fa7f2626ccf19c2a96e7383ae Mon Sep 17 00:00:00 2001 From: Kristof Siket Date: Tue, 14 Jul 2026 11:27:07 +0200 Subject: [PATCH 1/3] feat(cli): add --region to project create and fix default region on new app deploys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --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 --- docs/product/command-spec.md | 4 +- packages/cli/src/commands/project/index.ts | 7 +- packages/cli/src/controllers/app.ts | 19 ++-- packages/cli/src/controllers/project.ts | 10 +- packages/cli/src/lib/app/app-interaction.ts | 10 +- packages/cli/src/lib/app/app-provider.ts | 4 + packages/cli/src/presenters/project.ts | 6 ++ packages/cli/src/types/project.ts | 1 + packages/cli/tests/app-controller.test.ts | 72 +++++++++++++- packages/cli/tests/app-provider.test.ts | 98 +++++++++++++++++++ packages/cli/tests/project-controller.test.ts | 54 ++++++++++ 11 files changed, 264 insertions(+), 21 deletions(-) diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 5ee18ce..ce2da18 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -703,7 +703,7 @@ prisma-cli project show --json prisma-cli project show --project proj_123 --json ``` -## `prisma-cli project create ` +## `prisma-cli project create --region ` Purpose: @@ -713,6 +713,7 @@ Behavior: - requires auth - creates a Project in the authenticated workspace +- `--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 @@ -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 ``` diff --git a/packages/cli/src/commands/project/index.ts b/packages/cli/src/commands/project/index.ts index cadfd37..dd0ab82 100644 --- a/packages/cli/src/commands/project/index.ts +++ b/packages/cli/src/commands/project/index.ts @@ -186,15 +186,18 @@ function createProjectCreateCommand(runtime: CliRuntime): Command { "project.create", ); - command.argument("", "Project name"); + command + .argument("", "Project name") + .addOption(new Option("--region ", "Prisma Compute region id")); addGlobalFlags(command); command.action(async (name, options) => { + const region = (options as { region?: string }).region; await runCommand( runtime, "project.create", options as Record, - (context) => runProjectCreate(context, String(name)), + (context) => runProjectCreate(context, String(name), { region }), { renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result), diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 3d7a0af..be28899 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -18,7 +18,6 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk"; 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, @@ -645,6 +644,7 @@ async function runSingleAppDeploy( await requireProviderAndDeployProjectContext(context, options?.projectRef, { branch, createProjectName: options?.createProjectName, + createProjectRegion: deployRegion?.value, envProjectId, localPin, }); @@ -2770,7 +2770,7 @@ async function resolveDeployAppByName( matchedAnnotation: string; newAnnotation: string; requestedRegion: MergedDeployInput | undefined; - newAppRegion: string; + newAppRegion: string | undefined; firstDeploy: boolean; }, ): Promise<{ @@ -2842,7 +2842,7 @@ async function resolveAmbiguousDeployApp( matches: AppRecord[], targetName: string, requestedRegion: MergedDeployInput | undefined, - newAppRegion: string, + newAppRegion: string | undefined, firstDeploy: boolean, ): Promise<{ appId?: string; @@ -2923,8 +2923,8 @@ async function resolveAmbiguousDeployApp( function deployNewAppRegion( configRegion: MergedDeployInput | undefined, -): string { - return configRegion?.value ?? DEFAULT_REGION; +): string | undefined { + return configRegion?.value; } async function resolveExistingAppSelection( @@ -3324,6 +3324,7 @@ async function requireProviderAndDeployProjectContext( options: { branch?: ResolvedDeployBranch; createProjectName?: string; + createProjectRegion?: string; envProjectId?: string; localPin: LocalResolutionPinReadResult; }, @@ -3417,6 +3418,7 @@ async function resolveDeployProjectContext( options: { branch?: ResolvedDeployBranch; createProjectName?: string; + createProjectRegion?: string; envProjectId?: string; localPin: LocalResolutionPinReadResult; }, @@ -3469,6 +3471,7 @@ async function resolveDeployProjectContext( projectName, workspace, context.runtime.signal, + options.createProjectRegion, ); return withRemoteDeployBranch( provider, @@ -3567,6 +3570,7 @@ async function resolveDeployProjectContext( provider, workspace, projects, + options.createProjectRegion, ); return withRemoteDeployBranch( provider, @@ -3588,6 +3592,7 @@ async function resolveInteractiveDeployProjectSetup( provider: ReturnType, workspace: AuthWorkspace, projects: ProjectCandidate[], + createProjectRegion?: string, ): Promise> { const setup = await promptForProjectSetupChoice({ context, @@ -3598,6 +3603,7 @@ async function resolveInteractiveDeployProjectSetup( projectName, workspace, context.runtime.signal, + createProjectRegion, ), cancel: { why: "Deploy needs a Project before it can continue.", @@ -3626,9 +3632,10 @@ async function createProjectForDeploySetup( projectName: string, workspace: AuthWorkspace, signal: AbortSignal, + region?: string, ): Promise { const created = await provider - .createProject({ name: projectName, signal }) + .createProject({ name: projectName, region, signal }) .catch((error) => { throw projectCreateFailedError(error, projectName, workspace, { nextSteps: [ diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index a003332..8dc4eaa 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -263,6 +263,7 @@ export async function runProjectShow( export async function runProjectCreate( context: CommandContext, projectName: string, + options?: { region?: string }, ): Promise> { const authState = await requireAuthenticatedAuthState(context); const workspace = authState.workspace; @@ -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: [ @@ -1444,6 +1449,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 diff --git a/packages/cli/src/lib/app/app-interaction.ts b/packages/cli/src/lib/app/app-interaction.ts index f31b625..4e9da3a 100644 --- a/packages/cli/src/lib/app/app-interaction.ts +++ b/packages/cli/src/lib/app/app-interaction.ts @@ -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, @@ -52,8 +47,5 @@ export function createDeployInteraction( !value?.trim() ? "App name is required" : undefined, }).then((value) => value.trim()); }, - async selectRegion(_regions: RegionInfo[]): Promise { - return DEFAULT_REGION; - }, }; } diff --git a/packages/cli/src/lib/app/app-provider.ts b/packages/cli/src/lib/app/app-provider.ts index 310fa7e..ec3f635 100644 --- a/packages/cli/src/lib/app/app-provider.ts +++ b/packages/cli/src/lib/app/app-provider.ts @@ -37,6 +37,7 @@ export interface AppRecord { export interface ProjectRecord { id: string; name: string; + defaultRegion?: string; } export interface BranchRecord { @@ -144,6 +145,7 @@ export class DomainApiError extends Error { export interface AppProvider { createProject(options: { name: string; + region?: string; signal?: AbortSignal; }): Promise; resolveBranch( @@ -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()) { @@ -288,6 +291,7 @@ export function createAppProvider( return { id: projectResult.value.id, name: projectResult.value.name, + defaultRegion: projectResult.value.defaultRegion, }; }, diff --git a/packages/cli/src/presenters/project.ts b/packages/cli/src/presenters/project.ts index a160ff9..3433070 100644 --- a/packages/cli/src/presenters/project.ts +++ b/packages/cli/src/presenters/project.ts @@ -356,6 +356,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, diff --git a/packages/cli/src/types/project.ts b/packages/cli/src/types/project.ts index fa424e3..e388df2 100644 --- a/packages/cli/src/types/project.ts +++ b/packages/cli/src/types/project.ts @@ -4,6 +4,7 @@ export interface ProjectSummary { id: string; name: string; url?: string; + defaultRegion?: string | null; } export type ProjectSource = diff --git a/packages/cli/tests/app-controller.test.ts b/packages/cli/tests/app-controller.test.ts index 80dc1b9..0f94c00 100644 --- a/packages/cli/tests/app-controller.test.ts +++ b/packages/cli/tests/app-controller.test.ts @@ -4066,7 +4066,7 @@ describe("app controller", () => { ); }); - it("creates a named new app with the default Frankfurt region in non-interactive mode", async () => { + it("omits region from deployApp when --region is not passed for a new app", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); const deployApp = vi.fn().mockResolvedValue({ @@ -4125,7 +4125,7 @@ describe("app controller", () => { projectId: "proj_123", appId: undefined, appName: "hello-world", - region: "eu-central-1", + region: undefined, interaction: undefined, }), ); @@ -4233,6 +4233,74 @@ describe("app controller", () => { ); }); + it("passes --region to createProject when --create-project and --region are used together", async () => { + const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); + const createProject = vi.fn(async (options: { name: string }) => ({ + id: "proj_new", + name: options.name, + })); + const listApps = vi.fn().mockResolvedValue([]); + const deployApp = vi.fn().mockResolvedValue({ + projectId: "proj_new", + app: { + id: "app_new", + name: "hello-world", + region: "us-east-1", + liveDeploymentId: "dep_123", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://hello-world.prisma.app", + }, + }); + + vi.doMock("../src/lib/auth/guard", () => ({ + requireComputeAuth, + })); + vi.doMock("../src/lib/app/app-provider", () => ({ + createAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), + })); + + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runAppDeploy } = await import("../src/controllers/app"); + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + const { context } = await createTestCommandContext({ + cwd, + stateDir, + isTTY: false, + env: { + ...process.env, + PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", + PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, + }, + }); + + await runAppDeploy(context, "hello-world", { + createProjectName: "my-project", + region: "us-east-1", + framework: "hono", + }); + + expect(createProject).toHaveBeenCalledWith({ + name: "my-project", + region: "us-east-1", + signal: context.runtime.signal, + }); + }); + it("reuses the created project on second deploy instead of creating another one", async () => { const client = createProjectClient(); const requireComputeAuth = vi.fn().mockResolvedValue(client); diff --git a/packages/cli/tests/app-provider.test.ts b/packages/cli/tests/app-provider.test.ts index 2a0317b..dab2dff 100644 --- a/packages/cli/tests/app-provider.test.ts +++ b/packages/cli/tests/app-provider.test.ts @@ -233,6 +233,104 @@ describe("preview app provider", () => { ); }); + it("includes regionId in app POST body when region is specified", async () => { + const deploy = vi.fn().mockResolvedValue({ + isErr: () => false, + isOk: () => true, + value: { + projectId: "proj_123", + appId: "svc_branch", + appName: "hello-world", + region: "us-east-1", + deploymentId: "dep_123", + deploymentEndpointDomain: "cv-123.iad.prisma.build", + appEndpointDomain: "hello-world.iad.prisma.build", + }, + }); + const AppBuildStrategy = mockAppBuildStrategy(); + const client = { + GET: vi.fn().mockResolvedValue({ + data: { + data: [], + pagination: { hasMore: false, nextCursor: null }, + }, + response: { status: 200 }, + }), + POST: vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/projects/{projectId}/branches") { + return { + data: { + data: { + id: "br_billing", + gitName: "feat/billing", + isDefault: false, + role: "preview", + }, + }, + response: { status: 201 }, + }; + } + + if (pathName === "/v1/apps") { + return { + data: { + data: { + id: "svc_branch", + type: "app", + name: "hello-world", + region: { id: "us-east-1", name: "US East (N. Virginia)" }, + projectId: "proj_123", + branchId: "br_billing", + latestDeploymentId: null, + appEndpointDomain: "hello-world.iad.prisma.build", + }, + }, + response: { status: 201 }, + }; + } + + throw new Error(`Unexpected path ${pathName}`); + }), + }; + + vi.doMock("../src/lib/app/build", () => ({ + AppBuildStrategy, + })); + vi.doMock("@prisma/compute-sdk", () => ({ + ApiError: { is: () => false }, + ComputeClient: class { + deploy = deploy; + }, + })); + + const { createAppProvider } = await import("../src/lib/app/app-provider"); + + const provider = createAppProvider(client as never); + const cwd = path.resolve("/tmp/next-smoke"); + + await provider.deployApp({ + cwd, + projectId: "proj_123", + branchName: "feat/billing", + appName: "hello-world", + region: "us-east-1", + buildType: "nextjs", + portMapping: { http: 3000 }, + }); + + expect(client.POST).toHaveBeenCalledWith( + "/v1/apps", + expect.objectContaining({ + body: { + projectId: "proj_123", + branchId: "br_billing", + displayName: "hello-world", + regionId: "us-east-1", + }, + }), + ); + }); + it("uses an existing branch-scoped service when app creation races", async () => { const deploy = vi.fn().mockResolvedValue({ isErr: () => false, diff --git a/packages/cli/tests/project-controller.test.ts b/packages/cli/tests/project-controller.test.ts index 8f926cf..04a6980 100644 --- a/packages/cli/tests/project-controller.test.ts +++ b/packages/cli/tests/project-controller.test.ts @@ -224,6 +224,60 @@ describe("project controller", () => { ); }); + it("passes region to createProject when --region is provided", async () => { + const requireComputeAuth = vi.fn().mockResolvedValue({ token: "token" }); + const createProject = vi.fn().mockResolvedValue({ + id: "proj_new", + name: "New Dashboard", + defaultRegion: "us-east-1", + }); + + vi.doMock("../src/lib/auth/auth-ops", () => ({ + readAuthState: vi.fn().mockResolvedValue({ + authenticated: true, + provider: null, + user: { + email: "test@example.com", + }, + workspace: { + id: "ws_123", + name: "Acme Inc", + }, + }), + performLogin: vi.fn(), + performLogout: vi.fn(), + })); + vi.doMock("../src/lib/auth/guard", () => ({ + requireComputeAuth, + })); + vi.doMock("../src/lib/app/app-provider", () => ({ + createAppProvider: vi.fn(() => ({ + createProject, + })), + })); + + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + const { context } = await createTestCommandContext({ + cwd, + stateDir, + isTTY: false, + env: { + ...process.env, + PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, + }, + }); + + const { runProjectCreate } = await import("../src/controllers/project"); + await runProjectCreate(context, "New Dashboard", { region: "us-east-1" }); + + expect(createProject).toHaveBeenCalledWith({ + name: "New Dashboard", + region: "us-east-1", + signal: context.runtime.signal, + }); + }); + it("bare project link can create a new project from the interactive setup picker", async () => { const requireComputeAuth = vi.fn().mockResolvedValue({ token: "token", From 3f2a7ca14b15b406444c05c784832a8f7e053159 Mon Sep 17 00:00:00 2001 From: Kristof Siket Date: Tue, 14 Jul 2026 11:43:58 +0200 Subject: [PATCH 2/3] feat(cli): add region provenance line and project list region column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a deploy runs without --region, the main output now shows the server-resolved region with provenance: "project default" when the project has a defaultRegion, "platform default — pass --region to choose" otherwise. Explicit --region suppresses the provenance suffix. Also propagates defaultRegion through all three toProjectSummary copies (setup.ts, resolution.ts, use-cases/project.ts) and adds a defaultRegion column to `project list` output (absent value: dimmed "none"). Co-Authored-By: Claude Fable 5 --- packages/cli/src/controllers/app.ts | 1 + packages/cli/src/lib/project/resolution.ts | 5 +- packages/cli/src/lib/project/setup.ts | 5 +- packages/cli/src/presenters/app.ts | 13 ++++ packages/cli/src/presenters/project.ts | 11 +++- packages/cli/src/types/app.ts | 2 + packages/cli/src/use-cases/project.ts | 4 ++ packages/cli/tests/app-presenter.test.ts | 74 ++++++++++++++++++++++ packages/cli/tests/project.test.ts | 2 +- 9 files changed, 112 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index be28899..6f2cd40 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -869,6 +869,7 @@ async function runSingleAppDeploy( entrypoint ?? buildSettingsResolution.settings.entrypoint ?? null, httpPort: runtime.port, region: deployResult.app.region ?? selectedApp.region ?? null, + regionSource: deployRegion?.annotation ?? null, envVars: envVarNames(envVars), }, durationMs: deployDurationMs, diff --git a/packages/cli/src/lib/project/resolution.ts b/packages/cli/src/lib/project/resolution.ts index 51319a9..3d9aaac 100644 --- a/packages/cli/src/lib/project/resolution.ts +++ b/packages/cli/src/lib/project/resolution.ts @@ -753,11 +753,14 @@ function buildProjectRecoveryCommands( } function toProjectSummary( - project: Pick, + project: Pick, ): ProjectSummary { return { id: project.id, name: project.name, ...(project.url ? { url: project.url } : {}), + ...(project.defaultRegion != null + ? { defaultRegion: project.defaultRegion } + : {}), }; } diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index 8c7b6c9..0db7b7d 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -145,12 +145,15 @@ function localStateWriteFailedError( } export function toProjectSummary( - project: Pick, + project: Pick, ): ProjectSummary { return { id: project.id, name: project.name, ...(project.url ? { url: project.url } : {}), + ...(project.defaultRegion != null + ? { defaultRegion: project.defaultRegion } + : {}), }; } diff --git a/packages/cli/src/presenters/app.ts b/packages/cli/src/presenters/app.ts index fac67ec..7483764 100644 --- a/packages/cli/src/presenters/app.ts +++ b/packages/cli/src/presenters/app.ts @@ -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) ? [] : [ diff --git a/packages/cli/src/presenters/project.ts b/packages/cli/src/presenters/project.ts index 3433070..b45eb2e 100644 --- a/packages/cli/src/presenters/project.ts +++ b/packages/cli/src/presenters/project.ts @@ -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}`, ); } diff --git a/packages/cli/src/types/app.ts b/packages/cli/src/types/app.ts index 85b0b80..ec17447 100644 --- a/packages/cli/src/types/app.ts +++ b/packages/cli/src/types/app.ts @@ -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[]; } diff --git a/packages/cli/src/use-cases/project.ts b/packages/cli/src/use-cases/project.ts index a576f1d..d914302 100644 --- a/packages/cli/src/use-cases/project.ts +++ b/packages/cli/src/use-cases/project.ts @@ -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 } + : {}), }; } diff --git a/packages/cli/tests/app-presenter.test.ts b/packages/cli/tests/app-presenter.test.ts index 8f24252..bbde242 100644 --- a/packages/cli/tests/app-presenter.test.ts +++ b/packages/cli/tests/app-presenter.test.ts @@ -94,6 +94,7 @@ function createDeployResult(): AppDeployResult { entrypoint: "src/index.ts", httpPort: 8080, region: "fra", + regionSource: null, envVars: ["DATABASE_URL"], }, durationMs: 12_345, @@ -305,6 +306,79 @@ describe("app deploy presenter", () => { expect(lines).not.toMatch(/^Live in/m); }); + it("shows region with platform-default provenance when no --region was passed and project has no default region", async () => { + const { context } = await createTestCommandContext({}); + context.ui.dim = (text) => `dim(${text})`; + const result: AppDeployResult = { + ...createDeployResult(), + project: { id: "proj_123", name: "Billing API" }, + deploySettings: { + ...createDeployResult().deploySettings, + region: "us-east-1", + regionSource: null, + }, + }; + + const lines = renderAppDeploy( + context, + getCommandDescriptor("app.deploy"), + result, + ).join("\n"); + + expect(lines).toContain("Region"); + expect(lines).toContain("us-east-1"); + expect(lines).toContain("platform default — pass --region to choose"); + }); + + it("shows region with project-default provenance when no --region was passed and project has a default region", async () => { + const { context } = await createTestCommandContext({}); + context.ui.dim = (text) => `dim(${text})`; + const result: AppDeployResult = { + ...createDeployResult(), + project: { + id: "proj_123", + name: "Billing API", + defaultRegion: "eu-central-1", + }, + deploySettings: { + ...createDeployResult().deploySettings, + region: "eu-central-1", + regionSource: null, + }, + }; + + const lines = renderAppDeploy( + context, + getCommandDescriptor("app.deploy"), + result, + ).join("\n"); + + expect(lines).toContain("Region"); + expect(lines).toContain("eu-central-1"); + expect(lines).toContain("project default"); + }); + + it("omits the provenance region row when --region was passed explicitly", async () => { + const { context } = await createTestCommandContext({}); + const result: AppDeployResult = { + ...createDeployResult(), + deploySettings: { + ...createDeployResult().deploySettings, + region: "us-east-1", + regionSource: "set by --region", + }, + }; + + const lines = renderAppDeploy( + context, + getCommandDescriptor("app.deploy"), + result, + ).join("\n"); + + expect(lines).not.toContain("platform default"); + expect(lines).not.toContain("project default"); + }); + it("keeps promoted status and candidate url in JSON serialization", () => { const json = JSON.parse( JSON.stringify( diff --git a/packages/cli/tests/project.test.ts b/packages/cli/tests/project.test.ts index efda39a..6e06036 100644 --- a/packages/cli/tests/project.test.ts +++ b/packages/cli/tests/project.test.ts @@ -133,7 +133,7 @@ describe("project commands", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); expect(result.stderr).toBe( - "project list → Listing projects for the authenticated workspace.\n\n│ workspace: Acme Inc\n│\n│ name id\n│ Acme Dashboard proj_123\n│ Billing API proj_456\n│ Sandbox proj_999\n\nNext steps:\n- Link an existing Project you choose: prisma-cli project link \n- Create a new Project: prisma-cli project create \n", + "project list → Listing projects for the authenticated workspace.\n\n│ workspace: Acme Inc\n│\n│ name id region\n│ Acme Dashboard proj_123 none\n│ Billing API proj_456 none\n│ Sandbox proj_999 none\n\nNext steps:\n- Link an existing Project you choose: prisma-cli project link \n- Create a new Project: prisma-cli project create \n", ); }); From f1a28de2da106dea99dc5fe1e7fa2f0b148ab759 Mon Sep 17 00:00:00 2001 From: Kristof Siket Date: Tue, 14 Jul 2026 11:51:52 +0200 Subject: [PATCH 3/3] fix(cli): surface the server-assigned defaultRegion from project creation Both create-project call sites dropped the provider's defaultRegion, so project create output and the deploy-setup candidate never showed the server-assigned region. Also mark --region optional in the command-spec heading. Co-Authored-By: Claude Fable 5 --- docs/product/command-spec.md | 2 +- packages/cli/src/controllers/app.ts | 3 +++ packages/cli/src/controllers/project.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index ce2da18..793a25b 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -703,7 +703,7 @@ prisma-cli project show --json prisma-cli project show --project proj_123 --json ``` -## `prisma-cli project create --region ` +## `prisma-cli project create [--region ]` Purpose: diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 6f2cd40..464bbca 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -3654,6 +3654,9 @@ async function createProjectForDeploySetup( return { id: created.id, name: created.name, + ...(created.defaultRegion != null + ? { defaultRegion: created.defaultRegion } + : {}), workspace, }; } diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index 8dc4eaa..983a117 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -319,6 +319,9 @@ export async function runProjectCreate( { id: created.id, name: created.name, + ...(created.defaultRegion != null + ? { defaultRegion: created.defaultRegion } + : {}), }, "created", );