@prisma/cli 3.0.0-dev.54.1 → 3.0.0-dev.57.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.
package/README.md CHANGED
@@ -57,6 +57,7 @@ Useful next commands:
57
57
  npx prisma-cli app logs
58
58
  npx prisma-cli app open
59
59
  npx prisma-cli project env add DATABASE_URL=postgresql://example --role preview
60
+ npx prisma-cli project env list
60
61
  npx prisma-cli project env list --role preview
61
62
  ```
62
63
 
@@ -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)
@@ -1,16 +1,11 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
2
  import { requireComputeAuth } from "../lib/auth/guard.js";
3
3
  import { resolveProjectTarget } from "../lib/project/resolution.js";
4
+ import { readLocalGitBranch } from "../lib/git/local-branch.js";
4
5
  import { requireAuthenticatedAuthState } from "./auth.js";
5
6
  import { listRealWorkspaceProjects } from "./project.js";
6
7
  import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
7
8
  //#region src/controllers/app-env.ts
8
- function defaultRoleScope() {
9
- return {
10
- kind: "role",
11
- role: "production"
12
- };
13
- }
14
9
  async function runEnvAdd(context, rawAssignment, flags) {
15
10
  const { key, value } = parseKeyValuePositional(rawAssignment, "add", context.runtime.env);
16
11
  const scope = resolveEnvScope(flags, {
@@ -108,25 +103,30 @@ async function runEnvUpdate(context, rawAssignment, flags) {
108
103
  };
109
104
  }
110
105
  async function runEnvList(context, flags) {
111
- const scope = resolveEnvScope(flags, {
106
+ const explicit = resolveEnvScope(flags, {
112
107
  requireExplicit: false,
113
108
  command: "list"
114
- }) ?? defaultRoleScope();
109
+ });
115
110
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
116
- const resolved = await resolveScopeToApi(client, projectId, scope, {
117
- createBranchIfMissing: false,
111
+ const resolved = await resolveListScopeToApi(client, projectId, explicit, {
112
+ cwd: context.runtime.cwd,
118
113
  signal: context.runtime.signal
119
114
  });
120
- const variables = await listVariables(client, projectId, resolved, context.runtime.signal);
115
+ const variables = resolved.kind === "scoped" ? await listVariables(client, projectId, {
116
+ scope: resolved.addScope,
117
+ descriptor: resolved.descriptor,
118
+ apiTarget: resolved.apiTarget
119
+ }, context.runtime.signal) : await listOverviewVariables(client, projectId, context.runtime.signal);
121
120
  return {
122
121
  command: "project.env.list",
123
122
  result: {
124
123
  projectId,
125
124
  scope: resolved.descriptor,
125
+ target: resolved.target,
126
126
  variables: variables.map((row) => toMetadata(row, resolved.descriptor))
127
127
  },
128
128
  warnings: [],
129
- nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(scope)}`] : []
129
+ nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`] : []
130
130
  };
131
131
  }
132
132
  async function runEnvRemove(context, key, flags) {
@@ -196,12 +196,12 @@ async function resolveScopeToApi(client, projectId, scope, options) {
196
196
  }
197
197
  };
198
198
  const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
199
- if (branch.isDefault) throw new CliError({
199
+ if (branch.role === "production") throw new CliError({
200
200
  code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
201
201
  domain: "app",
202
- summary: `Branch "${scope.branchName}" is the default branch`,
202
+ summary: `Branch "${scope.branchName}" is the production branch`,
203
203
  why: "Production variables are project-level only; branch overrides apply to preview branches.",
204
- fix: "Use --role production for the default branch.",
204
+ fix: "Use --role production for the production branch.",
205
205
  exitCode: 1,
206
206
  nextSteps: ["prisma-cli project env list --role production"]
207
207
  });
@@ -218,6 +218,123 @@ async function resolveScopeToApi(client, projectId, scope, options) {
218
218
  }
219
219
  };
220
220
  }
221
+ async function resolveListScopeToApi(client, projectId, explicit, options) {
222
+ if (explicit) {
223
+ const resolved = await resolveScopeToApi(client, projectId, explicit, {
224
+ createBranchIfMissing: false,
225
+ signal: options.signal
226
+ });
227
+ return {
228
+ kind: "scoped",
229
+ descriptor: resolved.descriptor,
230
+ target: targetFromExplicitScope(resolved.descriptor),
231
+ apiTarget: resolved.apiTarget,
232
+ addScope: resolved.scope
233
+ };
234
+ }
235
+ const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
236
+ if (gitBranch) {
237
+ const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
238
+ if (!branch) return {
239
+ kind: "scoped",
240
+ descriptor: {
241
+ kind: "role",
242
+ role: "preview"
243
+ },
244
+ target: {
245
+ source: "local-git",
246
+ branchName: gitBranch,
247
+ branchExists: false,
248
+ envMap: "preview"
249
+ },
250
+ apiTarget: {
251
+ class: "preview",
252
+ branchId: null
253
+ },
254
+ addScope: {
255
+ kind: "branch",
256
+ branchName: gitBranch
257
+ }
258
+ };
259
+ if (branch.role === "production") return {
260
+ kind: "scoped",
261
+ descriptor: {
262
+ kind: "role",
263
+ role: "production"
264
+ },
265
+ target: {
266
+ source: "local-git",
267
+ branchName: branch.gitName,
268
+ branchId: branch.id,
269
+ branchRole: branch.role,
270
+ branchExists: true,
271
+ envMap: "production"
272
+ },
273
+ apiTarget: {
274
+ class: "production",
275
+ branchId: null
276
+ },
277
+ addScope: {
278
+ kind: "role",
279
+ role: "production"
280
+ }
281
+ };
282
+ return {
283
+ kind: "scoped",
284
+ descriptor: {
285
+ kind: "branch",
286
+ branchName: branch.gitName,
287
+ branchId: branch.id
288
+ },
289
+ target: {
290
+ source: "local-git",
291
+ branchName: branch.gitName,
292
+ branchId: branch.id,
293
+ branchRole: branch.role,
294
+ branchExists: true,
295
+ envMap: "preview"
296
+ },
297
+ apiTarget: {
298
+ class: "preview",
299
+ branchId: branch.id
300
+ },
301
+ addScope: {
302
+ kind: "branch",
303
+ branchName: branch.gitName
304
+ }
305
+ };
306
+ }
307
+ return {
308
+ kind: "overview",
309
+ descriptor: { kind: "overview" },
310
+ target: {
311
+ source: "overview",
312
+ envMap: "overview"
313
+ },
314
+ addScope: {
315
+ kind: "role",
316
+ role: "preview"
317
+ }
318
+ };
319
+ }
320
+ function targetFromExplicitScope(scope) {
321
+ if (scope.kind === "branch") return {
322
+ source: "explicit",
323
+ branchName: scope.branchName,
324
+ branchId: scope.branchId,
325
+ branchRole: "preview",
326
+ branchExists: true,
327
+ envMap: "preview"
328
+ };
329
+ if (scope.kind === "role") return {
330
+ source: "explicit",
331
+ envMap: scope.role
332
+ };
333
+ return {
334
+ source: "overview",
335
+ envMap: "overview"
336
+ };
337
+ }
221
338
  function formatScopeFlag(scope) {
222
339
  if (scope.kind === "role") return `--role ${scope.role}`;
223
340
  return `--branch ${scope.branchName}`;
@@ -306,25 +423,38 @@ async function findVariableByNaturalKey(client, projectId, key, resolved, signal
306
423
  return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
307
424
  }
308
425
  async function listVariables(client, projectId, resolved, signal) {
426
+ return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
427
+ className: resolved.apiTarget.class,
428
+ filter: (row) => rowMatchesScope(row, resolved)
429
+ }), resolved);
430
+ }
431
+ async function listOverviewVariables(client, projectId, signal) {
432
+ return (await collectEnvironmentVariables(client, projectId, signal, { filter: (row) => row.branchId === null && (row.class === "production" || row.class === "preview") })).sort((left, right) => {
433
+ const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
434
+ return roleOrder !== 0 ? roleOrder : left.key.localeCompare(right.key);
435
+ });
436
+ }
437
+ async function collectEnvironmentVariables(client, projectId, signal, options) {
309
438
  const collected = [];
310
439
  let cursor;
311
440
  while (true) {
312
- const query = {
313
- projectId,
314
- class: resolved.apiTarget.class
315
- };
441
+ const query = { projectId };
442
+ if (options.className !== void 0) query.class = options.className;
316
443
  if (cursor !== void 0) query.cursor = cursor;
317
444
  const result = await client.GET("/v1/environment-variables", {
318
445
  params: { query },
319
446
  signal
320
447
  });
321
448
  if (result.error || !result.data) throw apiCallError(`Failed to list environment variables`, result.response, result.error);
322
- const page = result.data.data.filter((row) => rowMatchesScope(row, resolved));
449
+ const page = result.data.data.filter(options.filter);
323
450
  collected.push(...page);
324
451
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
325
452
  cursor = result.data.pagination.nextCursor;
326
453
  }
327
- return materializeEffectiveRows(collected, resolved);
454
+ return collected;
455
+ }
456
+ function roleSortOrder(role) {
457
+ return role === "production" ? 0 : 1;
328
458
  }
329
459
  function rowMatchesScope(row, resolved) {
330
460
  if (row.class !== resolved.apiTarget.class) return false;
@@ -357,6 +487,7 @@ function toMetadata(row, requestedScope) {
357
487
  }
358
488
  function formatDescriptorLabel(scope) {
359
489
  if (scope.kind === "role") return scope.role ?? "unknown";
490
+ if (scope.kind === "overview") return "overview";
360
491
  return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
361
492
  }
362
493
  function apiCallError(summary, response, error) {
@@ -16,10 +16,12 @@ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../l
16
16
  import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
17
17
  import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
18
18
  import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
19
+ import { readLocalGitBranch } from "../lib/git/local-branch.js";
19
20
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
20
21
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
21
22
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
22
23
  import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
24
+ import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
23
25
  import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
24
26
  import { createSelectPromptPort } from "./select-prompt-port.js";
25
27
  import { requireAuthenticatedAuthState } from "./auth.js";
@@ -158,6 +160,12 @@ async function runAppDeploy(context, appName, options) {
158
160
  });
159
161
  framework = customized.framework;
160
162
  runtime = customized.runtime;
163
+ await enforceProductionDeployGate(context, provider, {
164
+ appId: selectedApp.appId,
165
+ appName: selectedApp.displayName,
166
+ branchKind: target.branch.kind,
167
+ prod: options?.prod === true
168
+ });
161
169
  const buildType = framework.buildType;
162
170
  assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
163
171
  const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
@@ -1322,7 +1330,7 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1322
1330
  if (!workspace) throw workspaceRequiredError();
1323
1331
  const branch = options.branch ?? await resolveDeployBranch(context, void 0);
1324
1332
  const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
1325
- if (explicitProject) return withDeployBranch({
1333
+ if (explicitProject) return withRemoteDeployBranch(provider, {
1326
1334
  workspace,
1327
1335
  project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
1328
1336
  resolution: {
@@ -1331,11 +1339,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1331
1339
  targetNameSource: "explicit"
1332
1340
  },
1333
1341
  localPinAction: "linked"
1334
- }, branch);
1342
+ }, branch, context.runtime.signal);
1335
1343
  if (options.createProjectName) {
1336
1344
  const projectName = options.createProjectName.trim();
1337
1345
  if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
1338
- return withDeployBranch({
1346
+ return withRemoteDeployBranch(provider, {
1339
1347
  workspace,
1340
1348
  project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
1341
1349
  resolution: {
@@ -1344,12 +1352,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1344
1352
  targetNameSource: "explicit"
1345
1353
  },
1346
1354
  localPinAction: "created"
1347
- }, branch);
1355
+ }, branch, context.runtime.signal);
1348
1356
  }
1349
1357
  if (options.envProjectId) {
1350
1358
  const project = projects.find((candidate) => candidate.id === options.envProjectId);
1351
1359
  if (!project) throw projectNotFoundError(options.envProjectId, workspace);
1352
- return withDeployBranch({
1360
+ return withRemoteDeployBranch(provider, {
1353
1361
  workspace,
1354
1362
  project: toProjectSummary(project),
1355
1363
  resolution: {
@@ -1357,14 +1365,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1357
1365
  targetName: options.envProjectId,
1358
1366
  targetNameSource: "env"
1359
1367
  }
1360
- }, branch);
1368
+ }, branch, context.runtime.signal);
1361
1369
  }
1362
1370
  const localPin = options.localPin;
1363
1371
  if (localPin.kind === "present") {
1364
1372
  if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
1365
1373
  const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
1366
1374
  if (!project) throw localResolutionPinStaleError();
1367
- return withDeployBranch({
1375
+ return withRemoteDeployBranch(provider, {
1368
1376
  workspace,
1369
1377
  project: toProjectSummary(project),
1370
1378
  resolution: {
@@ -1372,10 +1380,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1372
1380
  targetName: project.name,
1373
1381
  targetNameSource: "local-pin"
1374
1382
  }
1375
- }, branch);
1383
+ }, branch, context.runtime.signal);
1376
1384
  }
1377
1385
  const platformMapping = await resolveDurablePlatformMapping();
1378
- if (platformMapping && platformMapping.workspace.id === workspace.id) return withDeployBranch({
1386
+ if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
1379
1387
  workspace,
1380
1388
  project: toProjectSummary(platformMapping),
1381
1389
  resolution: {
@@ -1383,8 +1391,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1383
1391
  targetName: platformMapping.name,
1384
1392
  targetNameSource: "platform-mapping"
1385
1393
  }
1386
- }, branch);
1387
- if (canPrompt(context) && !context.flags.yes) return withDeployBranch(await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch);
1394
+ }, branch, context.runtime.signal);
1395
+ if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
1388
1396
  throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
1389
1397
  }
1390
1398
  async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
@@ -1430,12 +1438,16 @@ async function createProjectForDeploySetup(provider, projectName, workspace, sig
1430
1438
  workspace
1431
1439
  };
1432
1440
  }
1433
- function withDeployBranch(target, branch) {
1441
+ async function withRemoteDeployBranch(provider, target, branch, signal) {
1442
+ const remoteBranch = await provider.resolveBranch(target.project.id, {
1443
+ branchName: branch.name,
1444
+ signal
1445
+ });
1434
1446
  return {
1435
1447
  ...target,
1436
1448
  branch: {
1437
- name: branch.name,
1438
- kind: toBranchKind(branch.name)
1449
+ name: remoteBranch.name,
1450
+ kind: remoteBranch.role
1439
1451
  }
1440
1452
  };
1441
1453
  }
@@ -1470,42 +1482,6 @@ async function resolveDeployBranch(context, explicitBranchName) {
1470
1482
  annotation: "default"
1471
1483
  };
1472
1484
  }
1473
- async function readLocalGitBranch(cwd, signal) {
1474
- const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
1475
- if (!headPath) return null;
1476
- try {
1477
- const head = (await readFile(headPath, {
1478
- encoding: "utf8",
1479
- signal
1480
- })).trim();
1481
- if (head.startsWith("ref: refs/heads/")) return head.slice(16);
1482
- } catch (error) {
1483
- if (signal.aborted) throw error;
1484
- return null;
1485
- }
1486
- return null;
1487
- }
1488
- async function resolveGitHeadPath(gitPath, signal) {
1489
- signal.throwIfAborted();
1490
- try {
1491
- const raw = await readFile(gitPath, {
1492
- encoding: "utf8",
1493
- signal
1494
- });
1495
- if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
1496
- } catch (error) {
1497
- if (signal.aborted) throw error;
1498
- }
1499
- signal.throwIfAborted();
1500
- try {
1501
- await access(path.join(gitPath, "HEAD"));
1502
- signal.throwIfAborted();
1503
- return path.join(gitPath, "HEAD");
1504
- } catch (error) {
1505
- if (signal.aborted) throw error;
1506
- return null;
1507
- }
1508
- }
1509
1485
  async function resolveDeployFramework(context, options) {
1510
1486
  if (options.requestedFramework) return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
1511
1487
  if (options.entrypoint) return {
@@ -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 };
@@ -0,0 +1,41 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region src/lib/git/local-branch.ts
4
+ async function readLocalGitBranch(cwd, signal) {
5
+ const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
6
+ if (!headPath) return null;
7
+ try {
8
+ const head = (await readFile(headPath, {
9
+ encoding: "utf8",
10
+ signal
11
+ })).trim();
12
+ if (head.startsWith("ref: refs/heads/")) return head.slice(16);
13
+ } catch (error) {
14
+ if (signal.aborted) throw error;
15
+ return null;
16
+ }
17
+ return null;
18
+ }
19
+ async function resolveGitHeadPath(gitPath, signal) {
20
+ signal.throwIfAborted();
21
+ try {
22
+ const raw = await readFile(gitPath, {
23
+ encoding: "utf8",
24
+ signal
25
+ });
26
+ if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
27
+ } catch (error) {
28
+ if (signal.aborted) throw error;
29
+ }
30
+ signal.throwIfAborted();
31
+ try {
32
+ await access(path.join(gitPath, "HEAD"));
33
+ signal.throwIfAborted();
34
+ return path.join(gitPath, "HEAD");
35
+ } catch (error) {
36
+ if (signal.aborted) throw error;
37
+ return null;
38
+ }
39
+ }
40
+ //#endregion
41
+ export { readLocalGitBranch };
@@ -2,8 +2,18 @@ import { renderList, renderShow, serializeList } from "../output/patterns.js";
2
2
  //#region src/presenters/app-env.ts
3
3
  function scopeLabel(scope) {
4
4
  if (scope.kind === "role") return scope.role ?? "unknown";
5
+ if (scope.kind === "overview") return "overview";
5
6
  return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
6
7
  }
8
+ function listTargetLabel(result) {
9
+ const target = result.target;
10
+ if (target.source === "overview") return "overview";
11
+ if (target.branchName) {
12
+ const suffix = target.branchExists === false ? " (not created yet)" : "";
13
+ return `branch:${target.branchName} -> ${target.envMap}${suffix}`;
14
+ }
15
+ return scopeLabel(result.scope);
16
+ }
7
17
  function renderEnvAdd(context, descriptor, result) {
8
18
  return renderShow({
9
19
  title: "Setting a new environment variable.",
@@ -75,8 +85,8 @@ function renderEnvList(context, descriptor, result) {
75
85
  title: "Listing environment variables for the selected scope.",
76
86
  descriptor,
77
87
  parentContext: {
78
- key: "scope",
79
- value: scopeLabel(result.scope)
88
+ key: "target",
89
+ value: listTargetLabel(result)
80
90
  },
81
91
  items: result.variables.map((variable) => ({
82
92
  noun: "variable",
@@ -91,8 +101,9 @@ function serializeEnvList(result) {
91
101
  return {
92
102
  projectId: result.projectId,
93
103
  scope: result.scope,
104
+ target: result.target,
94
105
  ...serializeList({
95
- context: { scope: scopeLabel(result.scope) },
106
+ context: { target: listTargetLabel(result) },
96
107
  items: result.variables.map((variable) => ({
97
108
  noun: "variable",
98
109
  label: `${variable.key} (${variable.source})`,
@@ -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
  ]
@@ -378,7 +379,7 @@ const DESCRIPTORS = [
378
379
  ],
379
380
  description: "Manage environment variables for the active project",
380
381
  examples: [
381
- "prisma-cli project env list --role production",
382
+ "prisma-cli project env list",
382
383
  "prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
383
384
  "prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
384
385
  "prisma-cli project env remove STRIPE_KEY --role preview"
@@ -425,6 +426,7 @@ const DESCRIPTORS = [
425
426
  ],
426
427
  description: "List environment variable metadata for a scope (no values).",
427
428
  examples: [
429
+ "prisma-cli project env list",
428
430
  "prisma-cli project env list --role production",
429
431
  "prisma-cli project env list --role preview",
430
432
  "prisma-cli project env list --branch feature/foo"
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.57.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {