@prisma/cli 3.0.0-beta.1 → 3.0.0-beta.10

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.
Files changed (61) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/git.js +8 -3
  3. package/dist/adapters/local-state.js +12 -4
  4. package/dist/adapters/mock-api.js +81 -2
  5. package/dist/adapters/token-storage.js +63 -22
  6. package/dist/cli.js +17 -2
  7. package/dist/cli2.js +7 -3
  8. package/dist/commands/app/index.js +22 -10
  9. package/dist/commands/branch/index.js +2 -27
  10. package/dist/commands/database/index.js +159 -0
  11. package/dist/commands/env.js +8 -4
  12. package/dist/controllers/app-env-api.js +54 -0
  13. package/dist/controllers/app-env-file.js +181 -0
  14. package/dist/controllers/app-env.js +283 -129
  15. package/dist/controllers/app.js +247 -106
  16. package/dist/controllers/auth.js +8 -8
  17. package/dist/controllers/branch.js +78 -48
  18. package/dist/controllers/database.js +318 -0
  19. package/dist/controllers/project.js +153 -82
  20. package/dist/lib/app/branch-database-deploy.js +373 -0
  21. package/dist/lib/app/branch-database.js +316 -0
  22. package/dist/lib/app/bun-project.js +12 -5
  23. package/dist/lib/app/deploy-output.js +10 -1
  24. package/dist/lib/app/env-config.js +1 -1
  25. package/dist/lib/app/env-file.js +82 -0
  26. package/dist/lib/app/env-vars.js +28 -2
  27. package/dist/lib/app/local-dev.js +34 -18
  28. package/dist/lib/app/preview-branch-database.js +102 -0
  29. package/dist/lib/app/preview-build-settings.js +385 -0
  30. package/dist/lib/app/preview-build.js +272 -81
  31. package/dist/lib/app/preview-provider.js +163 -54
  32. package/dist/lib/app/production-deploy-gate.js +161 -0
  33. package/dist/lib/auth/auth-ops.js +30 -21
  34. package/dist/lib/auth/guard.js +3 -2
  35. package/dist/lib/auth/login.js +109 -14
  36. package/dist/lib/database/provider.js +167 -0
  37. package/dist/lib/diagnostics.js +15 -0
  38. package/dist/lib/git/local-branch.js +41 -0
  39. package/dist/lib/git/local-status.js +57 -0
  40. package/dist/lib/project/interactive-setup.js +1 -1
  41. package/dist/lib/project/local-pin.js +182 -33
  42. package/dist/lib/project/resolution.js +203 -49
  43. package/dist/lib/project/setup.js +59 -15
  44. package/dist/presenters/app-env.js +149 -14
  45. package/dist/presenters/app.js +170 -20
  46. package/dist/presenters/branch.js +37 -102
  47. package/dist/presenters/database.js +274 -0
  48. package/dist/presenters/project.js +63 -39
  49. package/dist/presenters/verbose-context.js +64 -0
  50. package/dist/shell/command-meta.js +103 -13
  51. package/dist/shell/command-runner.js +34 -6
  52. package/dist/shell/diagnostics-output.js +63 -0
  53. package/dist/shell/errors.js +12 -1
  54. package/dist/shell/output.js +10 -1
  55. package/dist/shell/runtime.js +3 -3
  56. package/dist/shell/ui.js +23 -1
  57. package/dist/shell/update-check.js +247 -0
  58. package/dist/use-cases/branch.js +20 -68
  59. package/dist/use-cases/create-cli-gateways.js +2 -17
  60. package/dist/use-cases/project.js +2 -1
  61. package/package.json +12 -10
@@ -1,39 +1,38 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
2
  import { requireComputeAuth } from "../lib/auth/guard.js";
3
- import { resolveProjectTarget } from "../lib/project/resolution.js";
3
+ import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
4
+ import { readEnvFileAssignments } from "../lib/app/env-file.js";
5
+ import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
6
+ import { readLocalGitBranch } from "../lib/git/local-branch.js";
4
7
  import { requireAuthenticatedAuthState } from "./auth.js";
5
8
  import { listRealWorkspaceProjects } from "./project.js";
6
- import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
9
+ import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
10
+ import { runEnvAddFile, runEnvUpdateFile } from "./app-env-file.js";
7
11
  //#region src/controllers/app-env.ts
8
- function defaultRoleScope() {
9
- return {
10
- kind: "role",
11
- role: "production"
12
- };
13
- }
14
12
  async function runEnvAdd(context, rawAssignment, flags) {
15
- const { key, value } = parseKeyValuePositional(rawAssignment, "add", context.runtime.env);
13
+ const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "add");
16
14
  const scope = resolveEnvScope(flags, {
17
15
  requireExplicit: true,
18
16
  command: "add"
19
17
  });
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 });
23
- if (await findVariableByNaturalKey(client, projectId, key, resolved)) throw new CliError({
18
+ 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");
19
+ const input = await resolveEnvWriteInput(context, source, "add");
20
+ const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env add");
21
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
22
+ createBranchIfMissing: true,
23
+ signal: context.runtime.signal
24
+ });
25
+ if (input.kind === "file") return runEnvAddFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
26
+ if (await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal)) throw new CliError({
24
27
  code: "ENV_VARIABLE_ALREADY_EXISTS",
25
28
  domain: "app",
26
- summary: `Variable "${key}" already exists in ${formatScopeLabel(scope)}`,
29
+ summary: `Variable "${input.key}" already exists in ${formatScopeLabel(scope)}`,
27
30
  why: "A variable with this key already exists in the targeted scope.",
28
31
  fix: "Use `prisma-cli project env update` to change an existing variable's value.",
29
32
  exitCode: 1,
30
- nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
33
+ nextSteps: [`prisma-cli project env update ${input.key}=<new-value> ${formatScopeFlag(scope)}`]
31
34
  });
32
- const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
33
- scope: {
34
- kind: "role",
35
- role: "preview"
36
- },
35
+ const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, input.key, {
37
36
  descriptor: {
38
37
  kind: "role",
39
38
  role: "preview"
@@ -42,19 +41,23 @@ async function runEnvAdd(context, rawAssignment, flags) {
42
41
  class: "preview",
43
42
  branchId: null
44
43
  }
45
- }) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
46
- const { data, error, response } = await client.POST("/v1/environment-variables", { body: {
47
- projectId,
48
- class: resolved.apiTarget.class,
49
- ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
50
- key,
51
- value
52
- } });
53
- if (error || !data) throw apiCallError(`Failed to add ${key}`, response, error);
44
+ }, context.runtime.signal) ? [`Variable "${input.key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
45
+ const { data, error, response } = await client.POST("/v1/environment-variables", {
46
+ body: {
47
+ projectId,
48
+ class: resolved.apiTarget.class,
49
+ ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
50
+ key: input.key,
51
+ value: input.value
52
+ },
53
+ signal: context.runtime.signal
54
+ });
55
+ if (error || !data) throw apiCallError(`Failed to add ${input.key}`, response, error);
54
56
  return {
55
57
  command: "project.env.add",
56
58
  result: {
57
59
  projectId,
60
+ verboseContext,
58
61
  scope: resolved.descriptor,
59
62
  variable: toMetadata(data.data, resolved.descriptor)
60
63
  },
@@ -63,33 +66,40 @@ async function runEnvAdd(context, rawAssignment, flags) {
63
66
  };
64
67
  }
65
68
  async function runEnvUpdate(context, rawAssignment, flags) {
66
- const { key, value } = parseKeyValuePositional(rawAssignment, "update", context.runtime.env);
69
+ const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "update");
67
70
  const scope = resolveEnvScope(flags, {
68
71
  requireExplicit: true,
69
72
  command: "update"
70
73
  });
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 });
74
- const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
74
+ 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");
75
+ const input = await resolveEnvWriteInput(context, source, "update");
76
+ const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env update");
77
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
78
+ createBranchIfMissing: false,
79
+ signal: context.runtime.signal
80
+ });
81
+ if (input.kind === "file") return runEnvUpdateFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
82
+ const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
75
83
  if (!existing) throw new CliError({
76
84
  code: "ENV_VARIABLE_NOT_FOUND",
77
85
  domain: "app",
78
- summary: `Variable "${key}" not found in ${formatScopeLabel(scope)}`,
86
+ summary: `Variable "${input.key}" not found in ${formatScopeLabel(scope)}`,
79
87
  why: "No variable with this key exists in the targeted scope.",
80
88
  fix: "Use `prisma-cli project env add` to create a new variable.",
81
89
  exitCode: 1,
82
- nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
90
+ nextSteps: [`prisma-cli project env add ${input.key}=<value> ${formatScopeFlag(scope)}`]
83
91
  });
84
92
  const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
85
93
  params: { path: { envVarId: existing.id } },
86
- body: { value }
94
+ body: { value: input.value },
95
+ signal: context.runtime.signal
87
96
  });
88
- if (error || !data) throw apiCallError(`Failed to update value for ${key}`, response, error);
97
+ if (error || !data) throw apiCallError(`Failed to update value for ${input.key}`, response, error);
89
98
  return {
90
99
  command: "project.env.update",
91
100
  result: {
92
101
  projectId,
102
+ verboseContext,
93
103
  scope: resolved.descriptor,
94
104
  variable: toMetadata(data.data, resolved.descriptor)
95
105
  },
@@ -97,23 +107,58 @@ async function runEnvUpdate(context, rawAssignment, flags) {
97
107
  nextSteps: []
98
108
  };
99
109
  }
110
+ function resolveEnvWriteSource(rawAssignment, filePath, command) {
111
+ if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma-cli project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
112
+ if (filePath !== void 0) {
113
+ if (filePath.length === 0) throw usageError(`prisma-cli project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
114
+ return {
115
+ kind: "file",
116
+ filePath
117
+ };
118
+ }
119
+ if (rawAssignment === void 0) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
120
+ return {
121
+ kind: "single",
122
+ rawAssignment
123
+ };
124
+ }
125
+ async function resolveEnvWriteInput(context, source, command) {
126
+ if (source.kind === "file") return {
127
+ kind: "file",
128
+ filePath: source.filePath,
129
+ assignments: await readEnvFileAssignments(context.runtime.cwd, source.filePath, command)
130
+ };
131
+ return {
132
+ kind: "single",
133
+ ...parseKeyValuePositional(source.rawAssignment, command, context.runtime.env)
134
+ };
135
+ }
100
136
  async function runEnvList(context, flags) {
101
- const scope = resolveEnvScope(flags, {
137
+ const explicit = resolveEnvScope(flags, {
102
138
  requireExplicit: false,
103
139
  command: "list"
104
- }) ?? defaultRoleScope();
105
- const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
106
- const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
107
- const variables = await listVariables(client, projectId, resolved);
140
+ });
141
+ const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env list");
142
+ const resolved = await resolveListScopeToApi(client, projectId, explicit ?? void 0, {
143
+ cwd: context.runtime.cwd,
144
+ signal: context.runtime.signal
145
+ });
146
+ const variables = resolved.kind === "scoped" ? await listVariables(client, projectId, {
147
+ scope: resolved.addScope,
148
+ descriptor: resolved.descriptor,
149
+ apiTarget: resolved.apiTarget
150
+ }, context.runtime.signal) : await listOverviewVariables(client, projectId, context.runtime.signal);
108
151
  return {
109
152
  command: "project.env.list",
110
153
  result: {
111
154
  projectId,
155
+ verboseContext,
112
156
  scope: resolved.descriptor,
157
+ target: resolved.target,
113
158
  variables: variables.map((row) => toMetadata(row, resolved.descriptor))
114
159
  },
115
160
  warnings: [],
116
- nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(scope)}`] : []
161
+ nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`] : []
117
162
  };
118
163
  }
119
164
  async function runEnvRemove(context, key, flags) {
@@ -123,9 +168,12 @@ async function runEnvRemove(context, key, flags) {
123
168
  command: "remove"
124
169
  });
125
170
  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 });
128
- const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
171
+ const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env remove");
172
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
173
+ createBranchIfMissing: false,
174
+ signal: context.runtime.signal
175
+ });
176
+ const existing = await findVariableByNaturalKey(client, projectId, key, resolved, context.runtime.signal);
129
177
  if (!existing) throw new CliError({
130
178
  code: "ENV_VARIABLE_NOT_FOUND",
131
179
  domain: "app",
@@ -135,12 +183,16 @@ async function runEnvRemove(context, key, flags) {
135
183
  exitCode: 1,
136
184
  nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`]
137
185
  });
138
- const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: existing.id } } });
186
+ const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", {
187
+ params: { path: { envVarId: existing.id } },
188
+ signal: context.runtime.signal
189
+ });
139
190
  if (error) throw apiCallError(`Failed to remove ${key}`, response, error);
140
191
  return {
141
192
  command: "project.env.remove",
142
193
  result: {
143
194
  projectId,
195
+ verboseContext,
144
196
  scope: resolved.descriptor,
145
197
  key
146
198
  },
@@ -150,18 +202,26 @@ async function runEnvRemove(context, key, flags) {
150
202
  }
151
203
  async function requireClientAndProject(context, explicitProject, commandName) {
152
204
  const authState = await requireAuthenticatedAuthState(context);
153
- const client = await requireComputeAuth(context.runtime.env);
205
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
154
206
  if (!client) throw authRequiredError(["prisma-cli auth login"]);
155
207
  if (!authState.workspace) throw workspaceRequiredError();
208
+ const targetResult = await resolveProjectTarget({
209
+ context,
210
+ workspace: authState.workspace,
211
+ explicitProject,
212
+ listProjects: () => listRealWorkspaceProjects(client, authState.workspace, context.runtime.signal),
213
+ commandName
214
+ });
215
+ if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
216
+ const target = targetResult.value;
156
217
  return {
157
218
  client,
158
- projectId: (await resolveProjectTarget({
159
- context,
219
+ projectId: target.project.id,
220
+ verboseContext: {
160
221
  workspace: authState.workspace,
161
- explicitProject,
162
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace),
163
- commandName
164
- })).project.id
222
+ project: target.project,
223
+ resolution: target.resolution
224
+ }
165
225
  };
166
226
  }
167
227
  async function resolveScopeToApi(client, projectId, scope, options) {
@@ -176,13 +236,13 @@ async function resolveScopeToApi(client, projectId, scope, options) {
176
236
  branchId: null
177
237
  }
178
238
  };
179
- const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
180
- if (branch.isDefault) throw new CliError({
239
+ const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
240
+ if (branch.role === "production") throw new CliError({
181
241
  code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
182
242
  domain: "app",
183
- summary: `Branch "${scope.branchName}" is the default branch`,
243
+ summary: `Branch "${scope.branchName}" is the production branch`,
184
244
  why: "Production variables are project-level only; branch overrides apply to preview branches.",
185
- fix: "Use --role production for the default branch.",
245
+ fix: "Use --role production for the production branch.",
186
246
  exitCode: 1,
187
247
  nextSteps: ["prisma-cli project env list --role production"]
188
248
  });
@@ -199,20 +259,140 @@ async function resolveScopeToApi(client, projectId, scope, options) {
199
259
  }
200
260
  };
201
261
  }
262
+ async function resolveListScopeToApi(client, projectId, explicit, options) {
263
+ if (explicit) {
264
+ const resolved = await resolveScopeToApi(client, projectId, explicit, {
265
+ createBranchIfMissing: false,
266
+ signal: options.signal
267
+ });
268
+ return {
269
+ kind: "scoped",
270
+ descriptor: resolved.descriptor,
271
+ target: targetFromExplicitScope(resolved.descriptor),
272
+ apiTarget: resolved.apiTarget,
273
+ addScope: resolved.scope
274
+ };
275
+ }
276
+ const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
277
+ if (gitBranch) {
278
+ const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
279
+ if (!branch) return {
280
+ kind: "scoped",
281
+ descriptor: {
282
+ kind: "role",
283
+ role: "preview"
284
+ },
285
+ target: {
286
+ source: "local-git",
287
+ branchName: gitBranch,
288
+ branchExists: false,
289
+ envMap: "preview"
290
+ },
291
+ apiTarget: {
292
+ class: "preview",
293
+ branchId: null
294
+ },
295
+ addScope: {
296
+ kind: "branch",
297
+ branchName: gitBranch
298
+ }
299
+ };
300
+ if (branch.role === "production") return {
301
+ kind: "scoped",
302
+ descriptor: {
303
+ kind: "role",
304
+ role: "production"
305
+ },
306
+ target: {
307
+ source: "local-git",
308
+ branchName: branch.gitName,
309
+ branchId: branch.id,
310
+ branchRole: branch.role,
311
+ branchExists: true,
312
+ envMap: "production"
313
+ },
314
+ apiTarget: {
315
+ class: "production",
316
+ branchId: null
317
+ },
318
+ addScope: {
319
+ kind: "role",
320
+ role: "production"
321
+ }
322
+ };
323
+ return {
324
+ kind: "scoped",
325
+ descriptor: {
326
+ kind: "branch",
327
+ branchName: branch.gitName,
328
+ branchId: branch.id
329
+ },
330
+ target: {
331
+ source: "local-git",
332
+ branchName: branch.gitName,
333
+ branchId: branch.id,
334
+ branchRole: branch.role,
335
+ branchExists: true,
336
+ envMap: "preview"
337
+ },
338
+ apiTarget: {
339
+ class: "preview",
340
+ branchId: branch.id
341
+ },
342
+ addScope: {
343
+ kind: "branch",
344
+ branchName: branch.gitName
345
+ }
346
+ };
347
+ }
348
+ return {
349
+ kind: "overview",
350
+ descriptor: { kind: "overview" },
351
+ target: {
352
+ source: "overview",
353
+ envMap: "overview"
354
+ },
355
+ addScope: {
356
+ kind: "role",
357
+ role: "preview"
358
+ }
359
+ };
360
+ }
361
+ function targetFromExplicitScope(scope) {
362
+ if (scope.kind === "branch") return {
363
+ source: "explicit",
364
+ branchName: scope.branchName,
365
+ branchId: scope.branchId,
366
+ branchRole: "preview",
367
+ branchExists: true,
368
+ envMap: "preview"
369
+ };
370
+ if (scope.kind === "role") return {
371
+ source: "explicit",
372
+ envMap: scope.role
373
+ };
374
+ return {
375
+ source: "overview",
376
+ envMap: "overview"
377
+ };
378
+ }
202
379
  function formatScopeFlag(scope) {
203
380
  if (scope.kind === "role") return `--role ${scope.role}`;
204
381
  return `--branch ${scope.branchName}`;
205
382
  }
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
- } });
383
+ async function listBranchesByName(client, projectId, branchName, signal) {
384
+ const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", {
385
+ params: {
386
+ path: { projectId },
387
+ query: { gitName: branchName }
388
+ },
389
+ signal
390
+ });
211
391
  if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
212
392
  return data.data;
213
393
  }
214
- async function resolveExistingBranch(client, projectId, branchName) {
215
- const branch = (await listBranchesByName(client, projectId, branchName))[0];
394
+ async function resolveExistingBranch(client, projectId, branchName, signal) {
395
+ const branch = (await listBranchesByName(client, projectId, branchName, signal))[0];
216
396
  if (!branch) throw new CliError({
217
397
  code: "ENV_BRANCH_NOT_FOUND",
218
398
  domain: "app",
@@ -224,10 +404,10 @@ async function resolveExistingBranch(client, projectId, branchName) {
224
404
  });
225
405
  return branch;
226
406
  }
227
- async function resolveOrCreateBranch(client, projectId, branchName) {
228
- const existing = (await listBranchesByName(client, projectId, branchName))[0];
407
+ async function resolveOrCreateBranch(client, projectId, branchName, signal) {
408
+ const existing = (await listBranchesByName(client, projectId, branchName, signal))[0];
229
409
  if (existing) return existing;
230
- if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
410
+ if (!await projectHasDefaultBranch(client, projectId, signal)) throw new CliError({
231
411
  code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
232
412
  domain: "app",
233
413
  summary: `Cannot create branch "${branchName}" from project env`,
@@ -241,67 +421,75 @@ async function resolveOrCreateBranch(client, projectId, branchName) {
241
421
  body: {
242
422
  gitName: branchName,
243
423
  isDefault: false
244
- }
424
+ },
425
+ signal
245
426
  });
246
427
  if (error || !data) {
247
428
  if (response?.status === 409) {
248
- const raced = (await listBranchesByName(client, projectId, branchName))[0];
429
+ const raced = (await listBranchesByName(client, projectId, branchName, signal))[0];
249
430
  if (raced) return raced;
250
431
  }
251
432
  throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
252
433
  }
253
434
  return data.data;
254
435
  }
255
- async function projectHasDefaultBranch(client, projectId) {
436
+ async function projectHasDefaultBranch(client, projectId, signal) {
256
437
  let cursor;
257
438
  while (true) {
258
439
  const query = {};
259
440
  if (cursor !== void 0) query.cursor = cursor;
260
- const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
261
- path: { projectId },
262
- query
263
- } });
441
+ const result = await client.GET("/v1/projects/{projectId}/branches", {
442
+ params: {
443
+ path: { projectId },
444
+ query
445
+ },
446
+ signal
447
+ });
264
448
  if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
265
449
  if (result.data.data.some((branch) => branch.isDefault)) return true;
266
450
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
267
451
  cursor = result.data.pagination.nextCursor;
268
452
  }
269
453
  }
270
- async function findVariableByNaturalKey(client, projectId, key, resolved) {
271
- const { data, error, response } = await client.GET("/v1/environment-variables", { params: { query: {
272
- projectId,
273
- class: resolved.apiTarget.class,
274
- key
275
- } } });
276
- if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
277
- return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
454
+ async function listVariables(client, projectId, resolved, signal) {
455
+ return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
456
+ className: resolved.apiTarget.class,
457
+ filter: (row) => rowMatchesScope(row, resolved)
458
+ }), resolved);
459
+ }
460
+ async function listOverviewVariables(client, projectId, signal) {
461
+ return (await collectEnvironmentVariables(client, projectId, signal, { filter: (row) => row.branchId === null && (row.class === "production" || row.class === "preview") })).sort((left, right) => {
462
+ const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
463
+ return roleOrder !== 0 ? roleOrder : left.key.localeCompare(right.key);
464
+ });
278
465
  }
279
- async function listVariables(client, projectId, resolved) {
466
+ async function collectEnvironmentVariables(client, projectId, signal, options) {
280
467
  const collected = [];
281
468
  let cursor;
282
469
  while (true) {
283
- const query = {
284
- projectId,
285
- class: resolved.apiTarget.class
286
- };
470
+ const query = { projectId };
471
+ if (options.className !== void 0) query.class = options.className;
287
472
  if (cursor !== void 0) query.cursor = cursor;
288
- const result = await client.GET("/v1/environment-variables", { params: { query } });
473
+ const result = await client.GET("/v1/environment-variables", {
474
+ params: { query },
475
+ signal
476
+ });
289
477
  if (result.error || !result.data) throw apiCallError(`Failed to list environment variables`, result.response, result.error);
290
- const page = result.data.data.filter((row) => rowMatchesScope(row, resolved));
478
+ const page = result.data.data.filter(options.filter);
291
479
  collected.push(...page);
292
480
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
293
481
  cursor = result.data.pagination.nextCursor;
294
482
  }
295
- return materializeEffectiveRows(collected, resolved);
483
+ return collected;
484
+ }
485
+ function roleSortOrder(role) {
486
+ return role === "production" ? 0 : 1;
296
487
  }
297
488
  function rowMatchesScope(row, resolved) {
298
489
  if (row.class !== resolved.apiTarget.class) return false;
299
490
  if (resolved.apiTarget.branchId === null) return row.branchId === null;
300
491
  return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
301
492
  }
302
- function rowMatchesExactScope(row, resolved) {
303
- return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
304
- }
305
493
  function materializeEffectiveRows(rows, resolved) {
306
494
  if (resolved.apiTarget.branchId === null) return rows;
307
495
  const byKey = /* @__PURE__ */ new Map();
@@ -309,39 +497,5 @@ function materializeEffectiveRows(rows, resolved) {
309
497
  for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
310
498
  return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
311
499
  }
312
- function toMetadata(row, requestedScope) {
313
- const rowScope = row.branchId === null ? {
314
- kind: "role",
315
- role: row.class
316
- } : requestedScope;
317
- return {
318
- id: row.id,
319
- key: row.key,
320
- scope: rowScope,
321
- source: formatDescriptorLabel(rowScope),
322
- isManagedBySystem: row.isManagedBySystem,
323
- updatedAt: row.updatedAt
324
- };
325
- }
326
- function formatDescriptorLabel(scope) {
327
- if (scope.kind === "role") return scope.role ?? "unknown";
328
- return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
329
- }
330
- function apiCallError(summary, response, error) {
331
- const status = response?.status ?? 0;
332
- const apiCode = error?.error?.code;
333
- const apiMessage = error?.error?.message;
334
- const apiHint = error?.error?.hint;
335
- if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
336
- return new CliError({
337
- code: apiCode ?? "ENV_API_ERROR",
338
- domain: "app",
339
- summary,
340
- why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
341
- fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
342
- exitCode: 1,
343
- nextSteps: []
344
- });
345
- }
346
500
  //#endregion
347
501
  export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };