@prisma/cli 3.0.0-alpha.9 → 3.0.0-beta.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.
@@ -1,7 +1,6 @@
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 { createSelectPromptPort } from "./select-prompt-port.js";
5
4
  import { requireAuthenticatedAuthState } from "./auth.js";
6
5
  import { listRealWorkspaceProjects } from "./project.js";
7
6
  import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
@@ -18,9 +17,9 @@ async function runEnvAdd(context, rawAssignment, flags) {
18
17
  requireExplicit: true,
19
18
  command: "add"
20
19
  });
21
- if (!scope) throw usageError(`prisma-cli project env add requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
22
- const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
23
- const resolved = resolveScopeToApi(scope);
20
+ if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
21
+ const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env add");
22
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: true });
24
23
  if (await findVariableByNaturalKey(client, projectId, key, resolved)) throw new CliError({
25
24
  code: "ENV_VARIABLE_ALREADY_EXISTS",
26
25
  domain: "app",
@@ -28,11 +27,26 @@ async function runEnvAdd(context, rawAssignment, flags) {
28
27
  why: "A variable with this key already exists in the targeted scope.",
29
28
  fix: "Use `prisma-cli project env update` to change an existing variable's value.",
30
29
  exitCode: 1,
31
- nextSteps: [`prisma-cli project env update ${key}=<new-value> --role ${scope.role}`]
30
+ nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
32
31
  });
32
+ const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
33
+ scope: {
34
+ kind: "role",
35
+ role: "preview"
36
+ },
37
+ descriptor: {
38
+ kind: "role",
39
+ role: "preview"
40
+ },
41
+ apiTarget: {
42
+ class: "preview",
43
+ branchId: null
44
+ }
45
+ }) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
33
46
  const { data, error, response } = await client.POST("/v1/environment-variables", { body: {
34
47
  projectId,
35
48
  class: resolved.apiTarget.class,
49
+ ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
36
50
  key,
37
51
  value
38
52
  } });
@@ -44,7 +58,7 @@ async function runEnvAdd(context, rawAssignment, flags) {
44
58
  scope: resolved.descriptor,
45
59
  variable: toMetadata(data.data, resolved.descriptor)
46
60
  },
47
- warnings: [],
61
+ warnings,
48
62
  nextSteps: []
49
63
  };
50
64
  }
@@ -54,9 +68,9 @@ async function runEnvUpdate(context, rawAssignment, flags) {
54
68
  requireExplicit: true,
55
69
  command: "update"
56
70
  });
57
- if (!scope) throw usageError(`prisma-cli project env update requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
58
- const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
59
- const resolved = resolveScopeToApi(scope);
71
+ if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
72
+ const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env update");
73
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
60
74
  const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
61
75
  if (!existing) throw new CliError({
62
76
  code: "ENV_VARIABLE_NOT_FOUND",
@@ -65,7 +79,7 @@ async function runEnvUpdate(context, rawAssignment, flags) {
65
79
  why: "No variable with this key exists in the targeted scope.",
66
80
  fix: "Use `prisma-cli project env add` to create a new variable.",
67
81
  exitCode: 1,
68
- nextSteps: [`prisma-cli project env add ${key}=<value> --role ${scope.role}`]
82
+ nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
69
83
  });
70
84
  const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
71
85
  params: { path: { envVarId: existing.id } },
@@ -88,8 +102,8 @@ async function runEnvList(context, flags) {
88
102
  requireExplicit: false,
89
103
  command: "list"
90
104
  }) ?? defaultRoleScope();
91
- const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
92
- const resolved = resolveScopeToApi(scope);
105
+ const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
106
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
93
107
  const variables = await listVariables(client, projectId, resolved);
94
108
  return {
95
109
  command: "project.env.list",
@@ -99,18 +113,18 @@ async function runEnvList(context, flags) {
99
113
  variables: variables.map((row) => toMetadata(row, resolved.descriptor))
100
114
  },
101
115
  warnings: [],
102
- nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value --role ${scope.role}`] : []
116
+ nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(scope)}`] : []
103
117
  };
104
118
  }
105
- async function runEnvRm(context, key, flags) {
106
- if (!key) throw usageError("prisma-cli project env rm requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env rm STRIPE_KEY --role production"], "app");
119
+ async function runEnvRemove(context, key, flags) {
120
+ if (!key) throw usageError("prisma-cli project env remove requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env remove STRIPE_KEY --role production"], "app");
107
121
  const scope = resolveEnvScope(flags, {
108
122
  requireExplicit: true,
109
- command: "rm"
123
+ command: "remove"
110
124
  });
111
- if (!scope) throw usageError("prisma-cli project env rm requires --role", "Writing without an explicit scope is rejected.", "Pass --role production or --role preview.", [`prisma-cli project env rm ${key} --role production`], "app");
112
- const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
113
- const resolved = resolveScopeToApi(scope);
125
+ if (!scope) throw usageError("prisma-cli project env remove requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env remove ${key} --role production`], "app");
126
+ const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env remove");
127
+ const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
114
128
  const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
115
129
  if (!existing) throw new CliError({
116
130
  code: "ENV_VARIABLE_NOT_FOUND",
@@ -119,12 +133,12 @@ async function runEnvRm(context, key, flags) {
119
133
  why: "No variable with this key exists in the targeted scope, so there is nothing to remove.",
120
134
  fix: "Run prisma-cli project env list with the same scope to see the available variables.",
121
135
  exitCode: 1,
122
- nextSteps: [`prisma-cli project env list --role ${scope.role}`]
136
+ nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`]
123
137
  });
124
138
  const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: existing.id } } });
125
139
  if (error) throw apiCallError(`Failed to remove ${key}`, response, error);
126
140
  return {
127
- command: "project.env.rm",
141
+ command: "project.env.remove",
128
142
  result: {
129
143
  projectId,
130
144
  scope: resolved.descriptor,
@@ -134,7 +148,7 @@ async function runEnvRm(context, key, flags) {
134
148
  nextSteps: []
135
149
  };
136
150
  }
137
- async function requireClientAndProject(context, explicitProject) {
151
+ async function requireClientAndProject(context, explicitProject, commandName) {
138
152
  const authState = await requireAuthenticatedAuthState(context);
139
153
  const client = await requireComputeAuth(context.runtime.env);
140
154
  if (!client) throw authRequiredError(["prisma-cli auth login"]);
@@ -146,13 +160,12 @@ async function requireClientAndProject(context, explicitProject) {
146
160
  workspace: authState.workspace,
147
161
  explicitProject,
148
162
  listProjects: () => listRealWorkspaceProjects(client, authState.workspace),
149
- prompt: createSelectPromptPort(context),
150
- remember: true
163
+ commandName
151
164
  })).project.id
152
165
  };
153
166
  }
154
- function resolveScopeToApi(scope) {
155
- return {
167
+ async function resolveScopeToApi(client, projectId, scope, options) {
168
+ if (scope.kind === "role") return {
156
169
  scope,
157
170
  descriptor: {
158
171
  kind: "role",
@@ -163,6 +176,96 @@ function resolveScopeToApi(scope) {
163
176
  branchId: null
164
177
  }
165
178
  };
179
+ const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
180
+ if (branch.isDefault) throw new CliError({
181
+ code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
182
+ domain: "app",
183
+ summary: `Branch "${scope.branchName}" is the default branch`,
184
+ why: "Production variables are project-level only; branch overrides apply to preview branches.",
185
+ fix: "Use --role production for the default branch.",
186
+ exitCode: 1,
187
+ nextSteps: ["prisma-cli project env list --role production"]
188
+ });
189
+ return {
190
+ scope,
191
+ descriptor: {
192
+ kind: "branch",
193
+ branchName: branch.gitName,
194
+ branchId: branch.id
195
+ },
196
+ apiTarget: {
197
+ class: "preview",
198
+ branchId: branch.id
199
+ }
200
+ };
201
+ }
202
+ function formatScopeFlag(scope) {
203
+ if (scope.kind === "role") return `--role ${scope.role}`;
204
+ return `--branch ${scope.branchName}`;
205
+ }
206
+ async function listBranchesByName(client, projectId, branchName) {
207
+ const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", { params: {
208
+ path: { projectId },
209
+ query: { gitName: branchName }
210
+ } });
211
+ if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
212
+ return data.data;
213
+ }
214
+ async function resolveExistingBranch(client, projectId, branchName) {
215
+ const branch = (await listBranchesByName(client, projectId, branchName))[0];
216
+ if (!branch) throw new CliError({
217
+ code: "ENV_BRANCH_NOT_FOUND",
218
+ domain: "app",
219
+ summary: `Branch "${branchName}" not found`,
220
+ why: "Branch update, list, and remove commands only target existing preview branches.",
221
+ fix: "Create the branch by deploying it, or use `project env add --branch` to create its first override.",
222
+ exitCode: 1,
223
+ nextSteps: [`prisma-cli project env add KEY=value --branch ${branchName}`]
224
+ });
225
+ return branch;
226
+ }
227
+ async function resolveOrCreateBranch(client, projectId, branchName) {
228
+ const existing = (await listBranchesByName(client, projectId, branchName))[0];
229
+ if (existing) return existing;
230
+ if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
231
+ code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
232
+ domain: "app",
233
+ summary: `Cannot create branch "${branchName}" from project env`,
234
+ why: "Creating the first branch would make it the project default, but branch overrides are preview-only.",
235
+ fix: "Create or deploy the default branch first, then add the branch override.",
236
+ exitCode: 1,
237
+ nextSteps: ["prisma-cli app deploy --branch main"]
238
+ });
239
+ const { data, error, response } = await client.POST("/v1/projects/{projectId}/branches", {
240
+ params: { path: { projectId } },
241
+ body: {
242
+ gitName: branchName,
243
+ isDefault: false
244
+ }
245
+ });
246
+ if (error || !data) {
247
+ if (response?.status === 409) {
248
+ const raced = (await listBranchesByName(client, projectId, branchName))[0];
249
+ if (raced) return raced;
250
+ }
251
+ throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
252
+ }
253
+ return data.data;
254
+ }
255
+ async function projectHasDefaultBranch(client, projectId) {
256
+ let cursor;
257
+ while (true) {
258
+ const query = {};
259
+ if (cursor !== void 0) query.cursor = cursor;
260
+ const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
261
+ path: { projectId },
262
+ query
263
+ } });
264
+ if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
265
+ if (result.data.data.some((branch) => branch.isDefault)) return true;
266
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
267
+ cursor = result.data.pagination.nextCursor;
268
+ }
166
269
  }
167
270
  async function findVariableByNaturalKey(client, projectId, key, resolved) {
168
271
  const { data, error, response } = await client.GET("/v1/environment-variables", { params: { query: {
@@ -171,7 +274,7 @@ async function findVariableByNaturalKey(client, projectId, key, resolved) {
171
274
  key
172
275
  } } });
173
276
  if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
174
- return data.data.filter((row) => rowMatchesScope(row, resolved))[0] ?? null;
277
+ return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
175
278
  }
176
279
  async function listVariables(client, projectId, resolved) {
177
280
  const collected = [];
@@ -189,20 +292,41 @@ async function listVariables(client, projectId, resolved) {
189
292
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
190
293
  cursor = result.data.pagination.nextCursor;
191
294
  }
192
- return collected;
295
+ return materializeEffectiveRows(collected, resolved);
193
296
  }
194
297
  function rowMatchesScope(row, resolved) {
195
- return row.branchId === null && row.class === resolved.apiTarget.class;
298
+ if (row.class !== resolved.apiTarget.class) return false;
299
+ if (resolved.apiTarget.branchId === null) return row.branchId === null;
300
+ return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
196
301
  }
197
- function toMetadata(row, scope) {
302
+ function rowMatchesExactScope(row, resolved) {
303
+ return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
304
+ }
305
+ function materializeEffectiveRows(rows, resolved) {
306
+ if (resolved.apiTarget.branchId === null) return rows;
307
+ const byKey = /* @__PURE__ */ new Map();
308
+ for (const row of rows) if (row.branchId === null && !byKey.has(row.key)) byKey.set(row.key, row);
309
+ for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
310
+ return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
311
+ }
312
+ function toMetadata(row, requestedScope) {
313
+ const rowScope = row.branchId === null ? {
314
+ kind: "role",
315
+ role: row.class
316
+ } : requestedScope;
198
317
  return {
199
318
  id: row.id,
200
319
  key: row.key,
201
- scope,
320
+ scope: rowScope,
321
+ source: formatDescriptorLabel(rowScope),
202
322
  isManagedBySystem: row.isManagedBySystem,
203
323
  updatedAt: row.updatedAt
204
324
  };
205
325
  }
326
+ function formatDescriptorLabel(scope) {
327
+ if (scope.kind === "role") return scope.role ?? "unknown";
328
+ return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
329
+ }
206
330
  function apiCallError(summary, response, error) {
207
331
  const status = response?.status ?? 0;
208
332
  const apiCode = error?.error?.code;
@@ -220,4 +344,4 @@ function apiCallError(summary, response, error) {
220
344
  });
221
345
  }
222
346
  //#endregion
223
- export { runEnvAdd, runEnvList, runEnvRm, runEnvUpdate };
347
+ export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };