@prisma/cli 3.0.0-dev.70.1 → 3.0.0-dev.73.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.
@@ -65,7 +65,7 @@ function createDeployCommand(runtime) {
65
65
  "hono",
66
66
  "tanstack-start",
67
67
  "bun"
68
- ])).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|file>", "Environment variable assignment or dotenv file").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire an isolated database for the preview Branch")).addOption(new Option("--no-db", "Skip branch database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
68
+ ])).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|file>", "Environment variable assignment or dotenv file").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire a Prisma Postgres database for this deploy target")).addOption(new Option("--no-db", "Skip database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
69
69
  addGlobalFlags(command);
70
70
  command.action(async (options) => {
71
71
  const appName = options.app;
@@ -80,7 +80,7 @@ function createDeployCommand(runtime) {
80
80
  const db = options.db;
81
81
  const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
82
82
  await runCommand(runtime, "app.deploy", options, (context) => {
83
- if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests branch database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
83
+ if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
84
84
  return runAppDeploy(context, appName, {
85
85
  projectRef,
86
86
  createProjectName,
@@ -31,6 +31,7 @@ import { listRealWorkspaceProjects } from "./project.js";
31
31
  import { access, readFile } from "node:fs/promises";
32
32
  import path from "node:path";
33
33
  import open from "open";
34
+ import { Result, matchError } from "better-result";
34
35
  //#region src/controllers/app.ts
35
36
  const DEPLOY_FRAMEWORKS = [
36
37
  "nextjs",
@@ -112,9 +113,9 @@ async function runAppDeploy(context, appName, options) {
112
113
  createProjectName: options?.createProjectName,
113
114
  envProjectId
114
115
  });
115
- const skipLocalPin = Boolean(envProjectId || options?.projectRef || options?.createProjectName);
116
- const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
117
- if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
116
+ const localPinReadResult = Boolean(envProjectId || options?.projectRef || options?.createProjectName) ? Result.ok({ kind: "missing" }) : await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
117
+ if (localPinReadResult.isErr()) throw localPinReadErrorToDeployError(localPinReadResult.error);
118
+ const localPin = localPinReadResult.value;
118
119
  const branch = await resolveDeployBranch(context, options?.branchName);
119
120
  if (options?.httpPort) parseDeployHttpPort(options.httpPort);
120
121
  assertSupportedEntrypointForRequestedDeployShape({
@@ -162,7 +163,7 @@ async function runAppDeploy(context, appName, options) {
162
163
  });
163
164
  framework = customized.framework;
164
165
  runtime = customized.runtime;
165
- await enforceProductionDeployGate(context, provider, {
166
+ const productionDeployGate = await enforceProductionDeployGate(context, provider, {
166
167
  appId: selectedApp.appId,
167
168
  appName: selectedApp.displayName,
168
169
  branchKind: target.branch.kind,
@@ -180,7 +181,8 @@ async function runAppDeploy(context, appName, options) {
180
181
  const portMapping = parseDeployPortMapping(String(runtime.port));
181
182
  const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
182
183
  db: options?.db,
183
- providedEnvVars: envVars
184
+ providedEnvVars: envVars,
185
+ firstProductionDeploy: productionDeployGate.firstProductionDeploy
184
186
  });
185
187
  const progressState = createPreviewDeployProgressState();
186
188
  const deployStartedAt = Date.now();
@@ -1975,6 +1977,18 @@ function localResolutionPinStaleError() {
1975
1977
  ]
1976
1978
  });
1977
1979
  }
1980
+ function localPinReadErrorToDeployError(error) {
1981
+ return matchError(error, {
1982
+ LocalResolutionPinInvalidJsonError: () => localResolutionPinStaleError(),
1983
+ LocalResolutionPinInvalidShapeError: () => localResolutionPinStaleError(),
1984
+ LocalResolutionPinReadAbortedError: (error) => {
1985
+ throw error;
1986
+ },
1987
+ UnhandledException: (error) => {
1988
+ throw error;
1989
+ }
1990
+ });
1991
+ }
1978
1992
  function readDeployEnvOverride(context, name) {
1979
1993
  const value = context.runtime.env[name]?.trim();
1980
1994
  return value ? value : void 0;
@@ -13,6 +13,7 @@ import { requireAuthenticatedAuthState } from "./auth.js";
13
13
  import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
14
14
  import { createProjectUseCases } from "../use-cases/project.js";
15
15
  import open from "open";
16
+ import { matchError } from "better-result";
16
17
  //#region src/controllers/project.ts
17
18
  const GITHUB_INSTALL_POLL_INTERVAL_MS = 2e3;
18
19
  const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
@@ -20,11 +21,24 @@ function isRealMode(context) {
20
21
  return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
21
22
  }
22
23
  async function readProjectListLocalBinding(cwd, workspace, projects, signal) {
23
- const pin = await readLocalResolutionPin(cwd, signal);
24
+ const pinResult = await readLocalResolutionPin(cwd, signal);
25
+ if (pinResult.isErr()) return localPinReadErrorToInvalidLocalBinding(pinResult.error);
26
+ const pin = pinResult.value;
24
27
  if (pin.kind === "present") return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId) ? { status: "linked" } : { status: "invalid" };
25
- if (pin.kind === "invalid") return { status: "invalid" };
26
28
  return { status: "not-linked" };
27
29
  }
30
+ function localPinReadErrorToInvalidLocalBinding(error) {
31
+ return matchError(error, {
32
+ LocalResolutionPinInvalidJsonError: () => ({ status: "invalid" }),
33
+ LocalResolutionPinInvalidShapeError: () => ({ status: "invalid" }),
34
+ LocalResolutionPinReadAbortedError: (error) => {
35
+ throw error;
36
+ },
37
+ UnhandledException: (error) => {
38
+ throw error;
39
+ }
40
+ });
41
+ }
28
42
  async function runProjectList(context) {
29
43
  const authState = await requireAuthenticatedAuthState(context);
30
44
  const workspace = authState.workspace;
@@ -3,71 +3,72 @@ import { renderSummaryLine } from "../../shell/ui.js";
3
3
  import { canPrompt } from "../../shell/runtime.js";
4
4
  import { confirmPrompt } from "../../shell/prompt.js";
5
5
  import { formatCommandArgument } from "../../shell/command-arguments.js";
6
+ import "../project/setup.js";
6
7
  import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup } from "./branch-database.js";
7
8
  import path from "node:path";
8
9
  //#region src/lib/app/branch-database-deploy.ts
9
10
  async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
10
11
  if (options.db === false) return emptyBranchDatabaseSetupOutcome();
11
12
  if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
12
- if (options.db === true) throw usageError("Branch database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create a branch override, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
13
+ if (options.db === true) throw usageError("Database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
13
14
  return emptyBranchDatabaseSetupOutcome();
14
15
  }
15
- if (branch.kind === "production") {
16
- if (options.db === true) throw usageError("Branch database setup is only available for preview branches", "Production database wiring is a durable environment decision and is not created implicitly by app deploy.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
16
+ if (branch.kind === "production" && !options.firstProductionDeploy) {
17
+ if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
17
18
  return emptyBranchDatabaseSetupOutcome();
18
19
  }
19
- const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
20
- const envState = await inspectBranchDatabaseEnv(provider, projectId, branch.id, context.runtime.signal);
21
- const branchEnvVars = [envState.branchDatabaseUrl, envState.branchDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
22
- if (envState.branchDatabaseUrl) {
23
- const warning = options.db === true ? `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.` : null;
20
+ const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
21
+ const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
22
+ if (hasExistingDatabaseEnvForTarget(branch, envState)) {
23
+ const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
24
24
  if (warning) emitBranchDatabaseWarning(context, warning);
25
25
  return {
26
26
  result: options.db === true ? {
27
27
  status: "skipped",
28
- reason: "branch-env-exists",
29
- envVars: branchEnvVars,
28
+ reason: existingDatabaseEnvReason(branch),
29
+ envVars: targetEnvVars,
30
30
  schema: null
31
31
  } : void 0,
32
32
  warnings: warning ? [warning] : []
33
33
  };
34
34
  }
35
+ const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
35
36
  if (localSignal.unsupportedSchema) {
36
- if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch.name, context);
37
+ if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
37
38
  return emptyBranchDatabaseSetupOutcome();
38
39
  }
39
- const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.previewDatabaseUrl);
40
+ const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
40
41
  if (options.db !== true) {
41
42
  if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
42
43
  if (!canPrompt(context) || context.flags.yes) {
43
- const warning = "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
44
+ const warning = databasePromptSuppressedWarning(branch);
44
45
  emitBranchDatabaseWarning(context, warning);
45
46
  return {
46
47
  result: void 0,
47
48
  warnings: [warning]
48
49
  };
49
50
  }
50
- maybeRenderBranchDatabaseSignal(context, branch.name, localSignal, envState);
51
+ maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
51
52
  if (!await confirmPrompt({
52
53
  input: context.runtime.stdin,
53
54
  output: context.output.stderr,
54
- message: `Create an isolated database for branch "${branch.name}"?`,
55
+ message: databasePromptMessage(branch),
55
56
  initialValue: false
56
57
  })) return emptyBranchDatabaseSetupOutcome();
57
- }
58
+ } else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
58
59
  return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
59
60
  }
60
61
  async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
61
- emitBranchDatabaseProgress(context, "pending", "Creating branch database");
62
+ emitBranchDatabaseProgress(context, "pending", "Creating database");
62
63
  const database = await provider.createBranchDatabase({
63
64
  projectId,
64
65
  branchId: branch.id,
65
66
  branchName: branch.name,
66
67
  signal: context.runtime.signal
67
68
  }).catch((error) => {
68
- throw branchDatabaseSetupFailedError("Failed to create branch database", error, branch.name);
69
+ throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
69
70
  });
70
- emitBranchDatabaseProgress(context, "success", "Created branch database");
71
+ emitBranchDatabaseProgress(context, "success", "Created database");
71
72
  try {
72
73
  let schemaSetup = null;
73
74
  const warnings = [];
@@ -80,12 +81,12 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
80
81
  databaseUrl: database.databaseUrl,
81
82
  directUrl: database.directUrl
82
83
  }).catch((error) => {
83
- throw schemaSetupFailedError(error, signal.schema, branch.name, context.runtime.cwd);
84
+ throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
84
85
  });
85
86
  emitBranchDatabaseProgress(context, "success", "Applied database schema");
86
- } else skippedSchemaWarning = "No supported Prisma schema source was found. Branch database env vars were created, but schema setup was skipped.";
87
+ } else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
87
88
  const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
88
- emitBranchDatabaseProgress(context, "success", `Added branch env override${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
89
+ emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
89
90
  if (skippedSchemaWarning) {
90
91
  emitBranchDatabaseWarning(context, skippedSchemaWarning);
91
92
  warnings.push(skippedSchemaWarning);
@@ -107,37 +108,36 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
107
108
  warnings
108
109
  };
109
110
  } catch (error) {
110
- throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch.name, error);
111
+ throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
111
112
  }
112
113
  }
113
114
  async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
115
+ const scope = envScopeForBranch(branch);
114
116
  const written = [];
115
117
  await upsertBranchDatabaseEnvVar(context, provider, {
116
118
  projectId,
117
- branchId: branch.id,
118
- className: "preview",
119
+ ...scope,
119
120
  key: "DATABASE_URL",
120
121
  value: database.databaseUrl,
121
- existing: envState.branchDatabaseUrl,
122
- branchName: branch.name
122
+ existing: envState.targetDatabaseUrl,
123
+ branch
123
124
  });
124
125
  written.push("DATABASE_URL");
125
126
  if (database.directUrl) {
126
127
  await upsertBranchDatabaseEnvVar(context, provider, {
127
128
  projectId,
128
- branchId: branch.id,
129
- className: "preview",
129
+ ...scope,
130
130
  key: "DIRECT_URL",
131
131
  value: database.directUrl,
132
- existing: envState.branchDirectUrl,
133
- branchName: branch.name
132
+ existing: envState.targetDirectUrl,
133
+ branch
134
134
  });
135
135
  written.push("DIRECT_URL");
136
- } else if (envState.branchDirectUrl) await provider.deleteEnvironmentVariable({
137
- envVarId: envState.branchDirectUrl.id,
136
+ } else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
137
+ envVarId: envState.targetDirectUrl.id,
138
138
  signal: context.runtime.signal
139
139
  }).catch((error) => {
140
- throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch.name);
140
+ throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
141
141
  });
142
142
  return written;
143
143
  }
@@ -148,37 +148,39 @@ async function upsertBranchDatabaseEnvVar(context, provider, options) {
148
148
  value: options.value,
149
149
  signal: context.runtime.signal
150
150
  }).catch((error) => {
151
- throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branchName);
151
+ throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
152
152
  });
153
153
  return;
154
154
  }
155
155
  await provider.createEnvironmentVariable({
156
156
  projectId: options.projectId,
157
- branchId: options.branchId,
158
157
  className: options.className,
159
158
  key: options.key,
160
159
  value: options.value,
160
+ ...options.branchId ? { branchId: options.branchId } : {},
161
161
  signal: context.runtime.signal
162
162
  }).catch((error) => {
163
- throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branchName);
163
+ throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
164
164
  });
165
165
  }
166
- async function inspectBranchDatabaseEnv(provider, projectId, branchId, signal) {
166
+ async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
167
+ const scope = envScopeForBranch(branch);
167
168
  const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
168
169
  projectId,
169
- className: "preview",
170
+ className: scope.className,
170
171
  key: "DATABASE_URL",
171
172
  signal
172
173
  }), provider.listEnvironmentVariables({
173
174
  projectId,
174
- className: "preview",
175
+ className: scope.className,
175
176
  key: "DIRECT_URL",
176
177
  signal
177
178
  })]);
179
+ const targetBranchId = branch.kind === "preview" ? branch.id : null;
178
180
  return {
179
- branchDatabaseUrl: findEnvVar(databaseUrlRows, { branchId }),
180
- branchDirectUrl: findEnvVar(directUrlRows, { branchId }),
181
- previewDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: null })
181
+ targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
182
+ targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
183
+ inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
182
184
  };
183
185
  }
184
186
  function findEnvVar(rows, options) {
@@ -187,14 +189,47 @@ function findEnvVar(rows, options) {
187
189
  function hasProvidedDatabaseEnvVars(envVars) {
188
190
  return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
189
191
  }
190
- function maybeRenderBranchDatabaseSignal(context, branchName, signal, envState) {
192
+ function envScopeForBranch(branch) {
193
+ return branch.kind === "production" ? { className: "production" } : {
194
+ className: "preview",
195
+ branchId: branch.id
196
+ };
197
+ }
198
+ function envScopeLabel(branch) {
199
+ return branch.kind === "production" ? "production" : "branch";
200
+ }
201
+ function getTargetDatabaseEnvVarKeys(envState) {
202
+ return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
203
+ }
204
+ function hasExistingDatabaseEnvForTarget(branch, envState) {
205
+ if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
206
+ return Boolean(envState.targetDatabaseUrl);
207
+ }
208
+ function existingDatabaseEnvReason(branch) {
209
+ return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
210
+ }
211
+ function existingDatabaseEnvWarning(branch, envVars) {
212
+ if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
213
+ return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
214
+ }
215
+ function databasePromptSuppressedWarning(branch) {
216
+ if (branch.kind === "production") return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
217
+ return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
218
+ }
219
+ function databasePromptMessage(branch) {
220
+ return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
221
+ }
222
+ function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
191
223
  if (context.flags.json || context.flags.quiet) return;
192
224
  const rows = [
193
225
  signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
194
226
  signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
195
- envState.previewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
227
+ envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
196
228
  ].filter((row) => Boolean(row));
197
- context.output.stderr.write(`Database signal found for branch "${branchName}"\n${rows.join("\n")}\n\n`);
229
+ context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
230
+ }
231
+ function databaseTargetLabel(branch) {
232
+ return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
198
233
  }
199
234
  function emitBranchDatabaseProgress(context, status, message) {
200
235
  if (context.flags.json || context.flags.quiet) return;
@@ -211,6 +246,12 @@ function emptyBranchDatabaseSetupOutcome() {
211
246
  warnings: []
212
247
  };
213
248
  }
249
+ function productionDatabaseSetupAfterFirstDeployError() {
250
+ return usageError("Database setup is only available during the first production deploy", "The selected production app already has a live deployment.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
251
+ }
252
+ function nonInteractiveDatabaseSetupRequiresYesError(branch) {
253
+ return usageError("Database setup requires --yes in non-interactive mode", "The deploy command received --db, but prompts are not available and --yes was not passed.", "Pass --yes together with --db to confirm non-interactive database creation.", [branch.kind === "production" ? "prisma-cli app deploy --prod --db --yes" : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`], "app");
254
+ }
214
255
  function formatSchemaSetupCommand(command) {
215
256
  switch (command) {
216
257
  case "migrate-deploy": return "prisma migrate deploy";
@@ -218,46 +259,46 @@ function formatSchemaSetupCommand(command) {
218
259
  case "prisma-next-db-init": return "prisma-next db init";
219
260
  }
220
261
  }
221
- function branchDatabaseSetupFailedError(summary, error, branchName) {
262
+ function branchDatabaseSetupFailedError(summary, error, branch) {
222
263
  return new CliError({
223
264
  code: "BRANCH_DATABASE_SETUP_FAILED",
224
265
  domain: "app",
225
266
  summary,
226
267
  why: error instanceof Error ? error.message : String(error),
227
- fix: "Retry the command, or create the branch database and env vars manually with project env commands.",
268
+ fix: "Retry the command, or create the database and env vars manually with project env commands.",
228
269
  debug: formatDebugDetails(error),
229
- meta: { branch: branchName },
270
+ meta: { branch: branch.name },
230
271
  exitCode: 1,
231
- nextSteps: [`prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`, `prisma-cli project env list --branch ${formatCommandArgument(branchName)}`]
272
+ nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
232
273
  });
233
274
  }
234
- async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branchName, error) {
235
- const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Branch database setup failed", error, branchName);
236
- emitBranchDatabaseProgress(context, "pending", "Removing branch database after setup failed");
275
+ async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
276
+ const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
277
+ emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
237
278
  try {
238
279
  await provider.deleteBranchDatabase({
239
280
  databaseId: database.id,
240
281
  signal: context.runtime.signal
241
282
  });
242
- emitBranchDatabaseProgress(context, "success", "Removed branch database after setup failed");
283
+ emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
243
284
  } catch (cleanupError) {
244
- return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName);
285
+ return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
245
286
  }
246
287
  return setupError;
247
288
  }
248
- function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branchName) {
289
+ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
249
290
  const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
250
- const setupWhy = setupError.why ?? "Branch database setup failed.";
291
+ const setupWhy = setupError.why ?? "Database setup failed.";
251
292
  return new CliError({
252
293
  code: setupError.code,
253
294
  domain: setupError.domain,
254
295
  summary: setupError.summary,
255
296
  why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
256
- fix: "Delete the created branch database from Console or contact Prisma support, then rerun deploy with --db.",
297
+ fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
257
298
  debug: formatCombinedDebugDetails(setupError, cleanupError),
258
299
  meta: {
259
300
  ...setupError.meta,
260
- branch: branchName,
301
+ branch: branch.name,
261
302
  databaseId: database.id,
262
303
  databaseName: database.name,
263
304
  cleanupFailed: true
@@ -266,7 +307,7 @@ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, br
266
307
  nextSteps: []
267
308
  });
268
309
  }
269
- function schemaSetupFailedError(error, schema, branchName, cwd) {
310
+ function schemaSetupFailedError(error, schema, branch, cwd) {
270
311
  return new CliError({
271
312
  code: "SCHEMA_SETUP_FAILED",
272
313
  domain: "app",
@@ -275,17 +316,26 @@ function schemaSetupFailedError(error, schema, branchName, cwd) {
275
316
  fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
276
317
  debug: formatDebugDetails(error),
277
318
  meta: {
278
- branch: branchName,
319
+ branch: branch.name,
279
320
  schemaPath: schema.path,
280
321
  source: schema.kind,
281
322
  command: schema.command
282
323
  },
283
324
  exitCode: 1,
284
- nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), `prisma-cli app deploy --branch ${formatCommandArgument(branchName)} --db`]
325
+ nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
285
326
  });
286
327
  }
287
- function unsupportedBranchDatabaseSchemaError(schema, branchName, context) {
288
- return usageError("Branch database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL for this branch, or switch the Prisma schema source to PostgreSQL before using --db.", [`prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branchName)}`, `prisma-cli app deploy --branch ${formatCommandArgument(branchName)}`], "app");
328
+ function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
329
+ return usageError("Database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL, or switch the Prisma schema source to PostgreSQL before using --db.", [formatProjectEnvAddNextStep(branch), `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)}`], "app");
330
+ }
331
+ function formatAppDeployWithDbNextStep(branch) {
332
+ return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
333
+ }
334
+ function formatProjectEnvListNextStep(branch) {
335
+ return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
336
+ }
337
+ function formatProjectEnvAddNextStep(branch) {
338
+ return branch.kind === "production" ? "prisma-cli project env add DATABASE_URL=<value> --role production" : `prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branch.name)}`;
289
339
  }
290
340
  function formatSchemaSetupNextSteps(schema, cwd) {
291
341
  const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
@@ -3,22 +3,22 @@ import { canPrompt } from "../../shell/runtime.js";
3
3
  import { confirmPrompt } from "../../shell/prompt.js";
4
4
  //#region src/lib/app/production-deploy-gate.ts
5
5
  async function enforceProductionDeployGate(context, provider, options) {
6
- if (options.branchKind !== "production") return;
6
+ if (options.branchKind !== "production") return { firstProductionDeploy: false };
7
7
  if (!options.appId) {
8
8
  renderFirstProductionDeployLine(context, options.appName);
9
- return;
9
+ return { firstProductionDeploy: true };
10
10
  }
11
11
  const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
12
12
  throw productionDeployInspectionFailedError(error);
13
13
  }));
14
14
  if (!currentLiveDeployment) {
15
15
  renderFirstProductionDeployLine(context, options.appName);
16
- return;
16
+ return { firstProductionDeploy: true };
17
17
  }
18
18
  if (!options.prod) throw productionDeployRequiresFlagError();
19
19
  if (context.flags.yes) {
20
20
  renderProductionDeployYesLine(context);
21
- return;
21
+ return { firstProductionDeploy: false };
22
22
  }
23
23
  if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
24
24
  renderProductionDeployConfirmation(context, currentLiveDeployment);
@@ -28,6 +28,7 @@ async function enforceProductionDeployGate(context, provider, options) {
28
28
  message: "Deploy to production?",
29
29
  initialValue: false
30
30
  })) throw productionDeployCancelledError();
31
+ return { firstProductionDeploy: false };
31
32
  }
32
33
  function resolveCurrentProductionDeployment(result) {
33
34
  if (result.deployments.length === 0) return null;
@@ -1,25 +1,75 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { Result, TaggedError, UnhandledException } from "better-result";
3
4
  //#region src/lib/project/local-pin.ts
4
5
  const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
5
- async function readLocalResolutionPin(cwd, signal) {
6
- signal?.throwIfAborted();
7
- try {
8
- const raw = await readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
9
- encoding: "utf8",
10
- signal
6
+ var LocalResolutionPinInvalidJsonError = class extends TaggedError("LocalResolutionPinInvalidJsonError")() {
7
+ constructor(cause) {
8
+ super({
9
+ message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} contains invalid JSON.`,
10
+ cause,
11
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
12
+ });
13
+ }
14
+ };
15
+ var LocalResolutionPinInvalidShapeError = class extends TaggedError("LocalResolutionPinInvalidShapeError")() {
16
+ constructor() {
17
+ super({
18
+ message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} must contain workspaceId and projectId string fields only.`,
19
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
20
+ });
21
+ }
22
+ };
23
+ var LocalResolutionPinReadAbortedError = class extends TaggedError("LocalResolutionPinReadAbortedError")() {
24
+ constructor(cause) {
25
+ super({
26
+ message: `Reading ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} was aborted.`,
27
+ cause,
28
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
11
29
  });
12
- const parsed = JSON.parse(raw);
13
- if (!isLocalResolutionPin(parsed)) return { kind: "invalid" };
14
- return {
30
+ }
31
+ };
32
+ async function readLocalResolutionPin(cwd, signal) {
33
+ return Result.gen(async function* () {
34
+ yield* ensureLocalResolutionPinReadNotAborted(signal);
35
+ const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal));
36
+ if (file.kind === "missing") return Result.ok({ kind: "missing" });
37
+ const parsed = yield* parseLocalResolutionPin(file.raw);
38
+ if (!isLocalResolutionPin(parsed)) return Result.err(new LocalResolutionPinInvalidShapeError());
39
+ return Result.ok({
15
40
  kind: "present",
16
41
  pin: parsed
17
- };
18
- } catch (error) {
19
- if (error.code === "ENOENT") return { kind: "missing" };
20
- if (error instanceof SyntaxError) return { kind: "invalid" };
21
- throw error;
42
+ });
43
+ });
44
+ }
45
+ function ensureLocalResolutionPinReadNotAborted(signal) {
46
+ return Result.try({
47
+ try: () => signal?.throwIfAborted(),
48
+ catch: (cause) => new LocalResolutionPinReadAbortedError(cause)
49
+ });
50
+ }
51
+ async function readLocalResolutionPinFile(cwd, signal) {
52
+ const readResult = await Result.tryPromise({
53
+ try: () => readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
54
+ encoding: "utf8",
55
+ signal
56
+ }),
57
+ catch: (cause) => signal?.aborted ? new LocalResolutionPinReadAbortedError(cause) : new UnhandledException({ cause })
58
+ });
59
+ if (readResult.isErr()) {
60
+ if (readResult.error instanceof UnhandledException && readResult.error.cause.code === "ENOENT") return Result.ok({ kind: "missing" });
61
+ return Result.err(readResult.error);
22
62
  }
63
+ return Result.ok({
64
+ kind: "present",
65
+ raw: readResult.value
66
+ });
67
+ }
68
+ function parseLocalResolutionPin(raw) {
69
+ return Result.try({
70
+ try: () => JSON.parse(raw),
71
+ catch: (cause) => cause instanceof SyntaxError ? new LocalResolutionPinInvalidJsonError(cause) : new UnhandledException({ cause })
72
+ });
23
73
  }
24
74
  async function writeLocalResolutionPin(cwd, pin, signal) {
25
75
  const prismaDir = path.join(cwd, ".prisma");
@@ -3,6 +3,7 @@ import { formatCommandArgument } from "../../shell/command-arguments.js";
3
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { matchError } from "better-result";
6
7
  //#region src/lib/project/resolution.ts
7
8
  async function resolveProjectTarget(options) {
8
9
  const localPin = await readImplicitLocalPin(options, { allowEnvProjectId: true });
@@ -224,7 +225,6 @@ async function resolveBoundProjectTarget(options, projects, settings) {
224
225
  }
225
226
  const localPin = settings.localPin;
226
227
  if (!localPin) return null;
227
- if (localPin.kind === "invalid") throw localStateStaleError();
228
228
  if (localPin.kind === "present") {
229
229
  if (localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
230
230
  pinnedWorkspaceId: localPin.pin.workspaceId,
@@ -247,7 +247,9 @@ async function resolveBoundProjectTarget(options, projects, settings) {
247
247
  }
248
248
  async function readImplicitLocalPin(options, settings) {
249
249
  if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return null;
250
- const localPin = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
250
+ const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
251
+ if (localPinResult.isErr()) throw localPinReadErrorToProjectError(localPinResult.error);
252
+ const localPin = localPinResult.value;
251
253
  if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) throw localProjectWorkspaceMismatchError({
252
254
  pinnedWorkspaceId: localPin.pin.workspaceId,
253
255
  pinnedProjectId: localPin.pin.projectId,
@@ -255,6 +257,18 @@ async function readImplicitLocalPin(options, settings) {
255
257
  });
256
258
  return localPin;
257
259
  }
260
+ function localPinReadErrorToProjectError(error) {
261
+ return matchError(error, {
262
+ LocalResolutionPinInvalidJsonError: () => localStateStaleError(),
263
+ LocalResolutionPinInvalidShapeError: () => localStateStaleError(),
264
+ LocalResolutionPinReadAbortedError: (error) => {
265
+ throw error;
266
+ },
267
+ UnhandledException: (error) => {
268
+ throw error;
269
+ }
270
+ });
271
+ }
258
272
  function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
259
273
  return {
260
274
  workspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.70.1",
3
+ "version": "3.0.0-dev.73.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,6 +39,7 @@
39
39
  "@prisma/compute-sdk": "^0.22.0",
40
40
  "@prisma/credentials-store": "^7.8.0",
41
41
  "@prisma/management-api-sdk": "^1.37.0",
42
+ "better-result": "^2.9.2",
42
43
  "c12": "4.0.0-beta.5",
43
44
  "colorette": "^2.0.20",
44
45
  "commander": "^14.0.3",