@prisma/cli 3.0.0-dev.54.1 → 3.0.0-dev.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,7 +64,7 @@ function createDeployCommand(runtime) {
64
64
  "hono",
65
65
  "tanstack-start",
66
66
  "bun"
67
- ])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues));
67
+ ])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--prod", "Confirm intent to deploy to production"));
68
68
  addGlobalFlags(command);
69
69
  command.action(async (options) => {
70
70
  const appName = options.app;
@@ -75,6 +75,7 @@ function createDeployCommand(runtime) {
75
75
  const envAssignments = options.env;
76
76
  const projectRef = options.project;
77
77
  const createProjectName = options.createProject;
78
+ const prod = options.prod;
78
79
  await runCommand(runtime, "app.deploy", options, (context) => runAppDeploy(context, appName, {
79
80
  projectRef,
80
81
  createProjectName,
@@ -82,7 +83,8 @@ function createDeployCommand(runtime) {
82
83
  entrypoint: entry,
83
84
  framework,
84
85
  httpPort,
85
- envAssignments
86
+ envAssignments,
87
+ prod: prod === true
86
88
  }), {
87
89
  renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
88
90
  renderJson: (result) => serializeAppDeploy(result)
@@ -20,6 +20,7 @@ import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild
20
20
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
21
21
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
22
22
  import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
23
+ import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
23
24
  import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
24
25
  import { createSelectPromptPort } from "./select-prompt-port.js";
25
26
  import { requireAuthenticatedAuthState } from "./auth.js";
@@ -158,6 +159,12 @@ async function runAppDeploy(context, appName, options) {
158
159
  });
159
160
  framework = customized.framework;
160
161
  runtime = customized.runtime;
162
+ await enforceProductionDeployGate(context, provider, {
163
+ appId: selectedApp.appId,
164
+ appName: selectedApp.displayName,
165
+ branchKind: target.branch.kind,
166
+ prod: options?.prod === true
167
+ });
161
168
  const buildType = framework.buildType;
162
169
  assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
163
170
  const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
@@ -1322,7 +1329,7 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1322
1329
  if (!workspace) throw workspaceRequiredError();
1323
1330
  const branch = options.branch ?? await resolveDeployBranch(context, void 0);
1324
1331
  const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
1325
- if (explicitProject) return withDeployBranch({
1332
+ if (explicitProject) return withRemoteDeployBranch(provider, {
1326
1333
  workspace,
1327
1334
  project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
1328
1335
  resolution: {
@@ -1331,11 +1338,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1331
1338
  targetNameSource: "explicit"
1332
1339
  },
1333
1340
  localPinAction: "linked"
1334
- }, branch);
1341
+ }, branch, context.runtime.signal);
1335
1342
  if (options.createProjectName) {
1336
1343
  const projectName = options.createProjectName.trim();
1337
1344
  if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
1338
- return withDeployBranch({
1345
+ return withRemoteDeployBranch(provider, {
1339
1346
  workspace,
1340
1347
  project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
1341
1348
  resolution: {
@@ -1344,12 +1351,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1344
1351
  targetNameSource: "explicit"
1345
1352
  },
1346
1353
  localPinAction: "created"
1347
- }, branch);
1354
+ }, branch, context.runtime.signal);
1348
1355
  }
1349
1356
  if (options.envProjectId) {
1350
1357
  const project = projects.find((candidate) => candidate.id === options.envProjectId);
1351
1358
  if (!project) throw projectNotFoundError(options.envProjectId, workspace);
1352
- return withDeployBranch({
1359
+ return withRemoteDeployBranch(provider, {
1353
1360
  workspace,
1354
1361
  project: toProjectSummary(project),
1355
1362
  resolution: {
@@ -1357,14 +1364,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1357
1364
  targetName: options.envProjectId,
1358
1365
  targetNameSource: "env"
1359
1366
  }
1360
- }, branch);
1367
+ }, branch, context.runtime.signal);
1361
1368
  }
1362
1369
  const localPin = options.localPin;
1363
1370
  if (localPin.kind === "present") {
1364
1371
  if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
1365
1372
  const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
1366
1373
  if (!project) throw localResolutionPinStaleError();
1367
- return withDeployBranch({
1374
+ return withRemoteDeployBranch(provider, {
1368
1375
  workspace,
1369
1376
  project: toProjectSummary(project),
1370
1377
  resolution: {
@@ -1372,10 +1379,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1372
1379
  targetName: project.name,
1373
1380
  targetNameSource: "local-pin"
1374
1381
  }
1375
- }, branch);
1382
+ }, branch, context.runtime.signal);
1376
1383
  }
1377
1384
  const platformMapping = await resolveDurablePlatformMapping();
1378
- if (platformMapping && platformMapping.workspace.id === workspace.id) return withDeployBranch({
1385
+ if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
1379
1386
  workspace,
1380
1387
  project: toProjectSummary(platformMapping),
1381
1388
  resolution: {
@@ -1383,8 +1390,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1383
1390
  targetName: platformMapping.name,
1384
1391
  targetNameSource: "platform-mapping"
1385
1392
  }
1386
- }, branch);
1387
- if (canPrompt(context) && !context.flags.yes) return withDeployBranch(await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch);
1393
+ }, branch, context.runtime.signal);
1394
+ if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
1388
1395
  throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
1389
1396
  }
1390
1397
  async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
@@ -1430,12 +1437,16 @@ async function createProjectForDeploySetup(provider, projectName, workspace, sig
1430
1437
  workspace
1431
1438
  };
1432
1439
  }
1433
- function withDeployBranch(target, branch) {
1440
+ async function withRemoteDeployBranch(provider, target, branch, signal) {
1441
+ const remoteBranch = await provider.resolveBranch(target.project.id, {
1442
+ branchName: branch.name,
1443
+ signal
1444
+ });
1434
1445
  return {
1435
1446
  ...target,
1436
1447
  branch: {
1437
- name: branch.name,
1438
- kind: toBranchKind(branch.name)
1448
+ name: remoteBranch.name,
1449
+ kind: remoteBranch.role
1439
1450
  }
1440
1451
  };
1441
1452
  }
@@ -36,6 +36,18 @@ function createPreviewAppProvider(client, options) {
36
36
  signal: options?.signal
37
37
  });
38
38
  },
39
+ async resolveBranch(projectId, options) {
40
+ const branch = await resolveOrCreateBranch(client, {
41
+ projectId,
42
+ gitName: options.branchName,
43
+ signal: options.signal
44
+ });
45
+ return {
46
+ id: branch.id,
47
+ name: branch.gitName,
48
+ role: branch.role
49
+ };
50
+ },
39
51
  async removeApp(appId, options) {
40
52
  const appResult = await sdk.showService({
41
53
  serviceId: appId,
@@ -316,17 +328,19 @@ async function listBranches(client, options) {
316
328
  signal: options.signal
317
329
  });
318
330
  if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
319
- return result.data.data;
331
+ return result.data.data.map((branch) => ({
332
+ id: branch.id,
333
+ gitName: branch.gitName,
334
+ isDefault: branch.isDefault,
335
+ role: branch.role
336
+ }));
320
337
  }
321
338
  async function resolveOrCreateBranch(client, options) {
322
339
  const existing = (await listBranches(client, options))[0];
323
340
  if (existing) return existing;
324
341
  const result = await client.POST("/v1/projects/{projectId}/branches", {
325
342
  params: { path: { projectId: options.projectId } },
326
- body: {
327
- gitName: options.gitName,
328
- isDefault: options.gitName === "main"
329
- },
343
+ body: { gitName: options.gitName },
330
344
  signal: options.signal
331
345
  });
332
346
  if (result.error || !result.data) {
@@ -336,7 +350,13 @@ async function resolveOrCreateBranch(client, options) {
336
350
  }
337
351
  throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
338
352
  }
339
- return result.data.data;
353
+ const branch = result.data.data;
354
+ return {
355
+ id: branch.id,
356
+ gitName: branch.gitName,
357
+ isDefault: branch.isDefault,
358
+ role: branch.role
359
+ };
340
360
  }
341
361
  async function listComputeServices(client, options) {
342
362
  const services = [];
@@ -0,0 +1,160 @@
1
+ import { CliError } from "../../shell/errors.js";
2
+ import { canPrompt } from "../../shell/runtime.js";
3
+ import { confirmPrompt } from "../../shell/prompt.js";
4
+ //#region src/lib/app/production-deploy-gate.ts
5
+ async function enforceProductionDeployGate(context, provider, options) {
6
+ if (options.branchKind !== "production") return;
7
+ if (!options.appId) {
8
+ renderFirstProductionDeployLine(context, options.appName);
9
+ return;
10
+ }
11
+ const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
12
+ throw productionDeployInspectionFailedError(error);
13
+ }));
14
+ if (!currentLiveDeployment) {
15
+ renderFirstProductionDeployLine(context, options.appName);
16
+ return;
17
+ }
18
+ if (!options.prod) throw productionDeployRequiresFlagError();
19
+ if (context.flags.yes) {
20
+ renderProductionDeployYesLine(context);
21
+ return;
22
+ }
23
+ if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
24
+ renderProductionDeployConfirmation(context, currentLiveDeployment);
25
+ if (!await confirmPrompt({
26
+ input: context.runtime.stdin,
27
+ output: context.output.stderr,
28
+ message: "Deploy to production?",
29
+ initialValue: false
30
+ })) throw productionDeployCancelledError();
31
+ }
32
+ function resolveCurrentProductionDeployment(result) {
33
+ if (result.deployments.length === 0) return null;
34
+ if (result.app.liveDeploymentId) {
35
+ const live = result.deployments.find((deployment) => deployment.id === result.app.liveDeploymentId);
36
+ if (live) return live;
37
+ }
38
+ return result.deployments.find((deployment) => deployment.live === true) ?? result.deployments[0] ?? null;
39
+ }
40
+ function renderFirstProductionDeployLine(context, appName) {
41
+ if (context.flags.json || context.flags.quiet) return;
42
+ context.output.stderr.write(`First deploy of "${appName}" -- promoting to production.\n\n`);
43
+ }
44
+ function renderProductionDeployYesLine(context) {
45
+ if (context.flags.json || context.flags.quiet) return;
46
+ context.output.stderr.write("Deploying to production (--prod --yes).\n\n");
47
+ }
48
+ function renderProductionDeployConfirmation(context, currentLiveDeployment) {
49
+ if (context.flags.json || context.flags.quiet) return;
50
+ const lines = [
51
+ "This will deploy to production and replace the live deployment.",
52
+ "",
53
+ ` Current live: ${currentLiveDeployment.id} deployed ${formatDeploymentAge(currentLiveDeployment.createdAt)}`,
54
+ " New deploy: will be built from your local code",
55
+ ""
56
+ ];
57
+ context.output.stderr.write(`${lines.join("\n")}\n`);
58
+ }
59
+ function formatDeploymentAge(createdAt) {
60
+ const createdAtMs = Date.parse(createdAt);
61
+ if (!Number.isFinite(createdAtMs)) return createdAt;
62
+ const elapsedMs = Math.max(0, Date.now() - createdAtMs);
63
+ for (const unit of [
64
+ {
65
+ label: "day",
66
+ ms: 1440 * 60 * 1e3
67
+ },
68
+ {
69
+ label: "hour",
70
+ ms: 3600 * 1e3
71
+ },
72
+ {
73
+ label: "minute",
74
+ ms: 60 * 1e3
75
+ }
76
+ ]) if (elapsedMs >= unit.ms) {
77
+ const value = Math.floor(elapsedMs / unit.ms);
78
+ return `${value} ${unit.label}${value === 1 ? "" : "s"} ago`;
79
+ }
80
+ return "less than a minute ago";
81
+ }
82
+ function productionDeployRequiresFlagError() {
83
+ return new CliError({
84
+ code: "PROD_DEPLOY_REQUIRES_FLAG",
85
+ domain: "app",
86
+ summary: "Production deploy requires --prod",
87
+ why: "The resolved Branch is production and this App already has a production deployment.",
88
+ fix: "Re-run with --prod, or deploy from a preview Branch.",
89
+ exitCode: 2,
90
+ nextActions: [{
91
+ kind: "run-command",
92
+ journey: "deploy-app",
93
+ label: "Deploy to production",
94
+ command: "prisma-cli app deploy --prod"
95
+ }, {
96
+ kind: "run-command",
97
+ journey: "deploy-app",
98
+ label: "Create a preview branch",
99
+ commands: ["git checkout -b <branch-name>", "prisma-cli app deploy"]
100
+ }],
101
+ humanLines: [
102
+ "This would deploy to production.",
103
+ "",
104
+ "Production deploys require explicit intent. Re-run with:",
105
+ "",
106
+ " prisma app deploy --prod",
107
+ "",
108
+ "Or deploy a preview from a feature branch:",
109
+ "",
110
+ " git checkout -b <branch-name>",
111
+ " prisma app deploy"
112
+ ]
113
+ });
114
+ }
115
+ function productionDeployConfirmationRequiredError(appName) {
116
+ return new CliError({
117
+ code: "CONFIRMATION_REQUIRED",
118
+ domain: "app",
119
+ summary: "Production deploy requires confirmation in the current mode",
120
+ why: "This command cannot prompt for production deploy confirmation in the current mode.",
121
+ fix: `Pass --prod --yes to confirm deployment of "${appName}" to production.`,
122
+ exitCode: 1,
123
+ nextSteps: ["prisma-cli app deploy --prod --yes"],
124
+ nextActions: [{
125
+ kind: "run-command",
126
+ journey: "deploy-app",
127
+ label: "Deploy to production non-interactively",
128
+ command: "prisma-cli app deploy --prod --yes"
129
+ }]
130
+ });
131
+ }
132
+ function productionDeployCancelledError() {
133
+ return new CliError({
134
+ code: "CONFIRMATION_REQUIRED",
135
+ domain: "app",
136
+ summary: "Production deploy cancelled",
137
+ why: null,
138
+ fix: null,
139
+ exitCode: 0,
140
+ humanLines: ["Cancelled."]
141
+ });
142
+ }
143
+ function productionDeployInspectionFailedError(error) {
144
+ return new CliError({
145
+ code: "DEPLOY_FAILED",
146
+ domain: "app",
147
+ summary: "Failed to inspect production deployments",
148
+ why: error instanceof Error ? error.message : String(error),
149
+ fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
150
+ debug: formatDebugDetails(error),
151
+ exitCode: 1,
152
+ nextSteps: ["prisma-cli app list-deploys"]
153
+ });
154
+ }
155
+ function formatDebugDetails(error) {
156
+ if (error instanceof Error) return error.stack ?? error.message;
157
+ return typeof error === "string" ? error : null;
158
+ }
159
+ //#endregion
160
+ export { enforceProductionDeployGate };
@@ -216,6 +216,7 @@ const DESCRIPTORS = [
216
216
  "prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
217
217
  "prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
218
218
  "prisma-cli app deploy --branch feat-login --framework hono",
219
+ "prisma-cli app deploy --prod --yes",
219
220
  "pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v<cli-version> --all",
220
221
  "prisma-cli app deploy --framework bun --entry src/server.ts"
221
222
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.54.1",
3
+ "version": "3.0.0-dev.56.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {