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

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 (66) 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 +64 -23
  6. package/dist/cli.js +18 -3
  7. package/dist/cli2.js +9 -5
  8. package/dist/commands/app/index.js +84 -59
  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 +286 -131
  15. package/dist/controllers/app.js +699 -244
  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 +156 -85
  20. package/dist/lib/app/branch-database-deploy.js +325 -0
  21. package/dist/lib/app/branch-database.js +215 -0
  22. package/dist/lib/app/bun-project.js +12 -5
  23. package/dist/lib/app/compute-config.js +147 -0
  24. package/dist/lib/app/deploy-output.js +10 -1
  25. package/dist/lib/app/deploy-plan.js +58 -0
  26. package/dist/lib/app/env-config.js +1 -1
  27. package/dist/lib/app/env-file.js +82 -0
  28. package/dist/lib/app/env-vars.js +28 -2
  29. package/dist/lib/app/local-dev.js +8 -49
  30. package/dist/lib/app/preview-branch-database.js +102 -0
  31. package/dist/lib/app/preview-build-settings.js +376 -0
  32. package/dist/lib/app/preview-build.js +314 -97
  33. package/dist/lib/app/preview-provider.js +178 -65
  34. package/dist/lib/app/production-deploy-gate.js +161 -0
  35. package/dist/lib/auth/auth-ops.js +30 -21
  36. package/dist/lib/auth/guard.js +3 -2
  37. package/dist/lib/auth/login.js +116 -14
  38. package/dist/lib/database/provider.js +167 -0
  39. package/dist/lib/diagnostics.js +15 -0
  40. package/dist/lib/fs/home-path.js +24 -0
  41. package/dist/lib/git/local-branch.js +53 -0
  42. package/dist/lib/git/local-status.js +57 -0
  43. package/dist/lib/project/interactive-setup.js +1 -1
  44. package/dist/lib/project/local-pin.js +182 -33
  45. package/dist/lib/project/resolution.js +204 -50
  46. package/dist/lib/project/setup.js +66 -19
  47. package/dist/output/patterns.js +1 -1
  48. package/dist/presenters/app-env.js +149 -14
  49. package/dist/presenters/app.js +178 -23
  50. package/dist/presenters/branch.js +37 -102
  51. package/dist/presenters/database.js +274 -0
  52. package/dist/presenters/project.js +57 -39
  53. package/dist/presenters/verbose-context.js +64 -0
  54. package/dist/shell/command-meta.js +103 -13
  55. package/dist/shell/command-runner.js +57 -19
  56. package/dist/shell/diagnostics-output.js +57 -0
  57. package/dist/shell/errors.js +12 -1
  58. package/dist/shell/help.js +30 -20
  59. package/dist/shell/output.js +10 -1
  60. package/dist/shell/runtime.js +11 -7
  61. package/dist/shell/ui.js +42 -3
  62. package/dist/shell/update-check.js +247 -0
  63. package/dist/use-cases/branch.js +20 -68
  64. package/dist/use-cases/create-cli-gateways.js +2 -17
  65. package/dist/use-cases/project.js +2 -1
  66. package/package.json +26 -10
@@ -1,39 +1,38 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
+ import { projectResolutionErrorToCliError, 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";
2
5
  import { requireComputeAuth } from "../lib/auth/guard.js";
3
- import { 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,27 @@ 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
- if (!authState.workspace) throw workspaceRequiredError();
207
+ const workspace = authState.workspace;
208
+ if (!workspace) throw workspaceRequiredError();
209
+ const targetResult = await resolveProjectTarget({
210
+ context,
211
+ workspace,
212
+ explicitProject,
213
+ listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
214
+ commandName
215
+ });
216
+ if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
217
+ const target = targetResult.value;
156
218
  return {
157
219
  client,
158
- projectId: (await resolveProjectTarget({
159
- context,
160
- workspace: authState.workspace,
161
- explicitProject,
162
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace),
163
- commandName
164
- })).project.id
220
+ projectId: target.project.id,
221
+ verboseContext: {
222
+ workspace,
223
+ project: target.project,
224
+ resolution: target.resolution
225
+ }
165
226
  };
166
227
  }
167
228
  async function resolveScopeToApi(client, projectId, scope, options) {
@@ -176,13 +237,13 @@ async function resolveScopeToApi(client, projectId, scope, options) {
176
237
  branchId: null
177
238
  }
178
239
  };
179
- const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
180
- if (branch.isDefault) throw new CliError({
240
+ const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
241
+ if (branch.role === "production") throw new CliError({
181
242
  code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
182
243
  domain: "app",
183
- summary: `Branch "${scope.branchName}" is the default branch`,
244
+ summary: `Branch "${scope.branchName}" is the production branch`,
184
245
  why: "Production variables are project-level only; branch overrides apply to preview branches.",
185
- fix: "Use --role production for the default branch.",
246
+ fix: "Use --role production for the production branch.",
186
247
  exitCode: 1,
187
248
  nextSteps: ["prisma-cli project env list --role production"]
188
249
  });
@@ -199,20 +260,140 @@ async function resolveScopeToApi(client, projectId, scope, options) {
199
260
  }
200
261
  };
201
262
  }
263
+ async function resolveListScopeToApi(client, projectId, explicit, options) {
264
+ if (explicit) {
265
+ const resolved = await resolveScopeToApi(client, projectId, explicit, {
266
+ createBranchIfMissing: false,
267
+ signal: options.signal
268
+ });
269
+ return {
270
+ kind: "scoped",
271
+ descriptor: resolved.descriptor,
272
+ target: targetFromExplicitScope(resolved.descriptor),
273
+ apiTarget: resolved.apiTarget,
274
+ addScope: resolved.scope
275
+ };
276
+ }
277
+ const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
278
+ if (gitBranch) {
279
+ const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
280
+ if (!branch) return {
281
+ kind: "scoped",
282
+ descriptor: {
283
+ kind: "role",
284
+ role: "preview"
285
+ },
286
+ target: {
287
+ source: "local-git",
288
+ branchName: gitBranch,
289
+ branchExists: false,
290
+ envMap: "preview"
291
+ },
292
+ apiTarget: {
293
+ class: "preview",
294
+ branchId: null
295
+ },
296
+ addScope: {
297
+ kind: "branch",
298
+ branchName: gitBranch
299
+ }
300
+ };
301
+ if (branch.role === "production") return {
302
+ kind: "scoped",
303
+ descriptor: {
304
+ kind: "role",
305
+ role: "production"
306
+ },
307
+ target: {
308
+ source: "local-git",
309
+ branchName: branch.gitName,
310
+ branchId: branch.id,
311
+ branchRole: branch.role,
312
+ branchExists: true,
313
+ envMap: "production"
314
+ },
315
+ apiTarget: {
316
+ class: "production",
317
+ branchId: null
318
+ },
319
+ addScope: {
320
+ kind: "role",
321
+ role: "production"
322
+ }
323
+ };
324
+ return {
325
+ kind: "scoped",
326
+ descriptor: {
327
+ kind: "branch",
328
+ branchName: branch.gitName,
329
+ branchId: branch.id
330
+ },
331
+ target: {
332
+ source: "local-git",
333
+ branchName: branch.gitName,
334
+ branchId: branch.id,
335
+ branchRole: branch.role,
336
+ branchExists: true,
337
+ envMap: "preview"
338
+ },
339
+ apiTarget: {
340
+ class: "preview",
341
+ branchId: branch.id
342
+ },
343
+ addScope: {
344
+ kind: "branch",
345
+ branchName: branch.gitName
346
+ }
347
+ };
348
+ }
349
+ return {
350
+ kind: "overview",
351
+ descriptor: { kind: "overview" },
352
+ target: {
353
+ source: "overview",
354
+ envMap: "overview"
355
+ },
356
+ addScope: {
357
+ kind: "role",
358
+ role: "preview"
359
+ }
360
+ };
361
+ }
362
+ function targetFromExplicitScope(scope) {
363
+ if (scope.kind === "branch") return {
364
+ source: "explicit",
365
+ branchName: scope.branchName,
366
+ branchId: scope.branchId,
367
+ branchRole: "preview",
368
+ branchExists: true,
369
+ envMap: "preview"
370
+ };
371
+ if (scope.kind === "role") return {
372
+ source: "explicit",
373
+ envMap: scope.role
374
+ };
375
+ return {
376
+ source: "overview",
377
+ envMap: "overview"
378
+ };
379
+ }
202
380
  function formatScopeFlag(scope) {
203
381
  if (scope.kind === "role") return `--role ${scope.role}`;
204
382
  return `--branch ${scope.branchName}`;
205
383
  }
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
- } });
384
+ async function listBranchesByName(client, projectId, branchName, signal) {
385
+ const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", {
386
+ params: {
387
+ path: { projectId },
388
+ query: { gitName: branchName }
389
+ },
390
+ signal
391
+ });
211
392
  if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
212
393
  return data.data;
213
394
  }
214
- async function resolveExistingBranch(client, projectId, branchName) {
215
- const branch = (await listBranchesByName(client, projectId, branchName))[0];
395
+ async function resolveExistingBranch(client, projectId, branchName, signal) {
396
+ const branch = (await listBranchesByName(client, projectId, branchName, signal))[0];
216
397
  if (!branch) throw new CliError({
217
398
  code: "ENV_BRANCH_NOT_FOUND",
218
399
  domain: "app",
@@ -224,10 +405,10 @@ async function resolveExistingBranch(client, projectId, branchName) {
224
405
  });
225
406
  return branch;
226
407
  }
227
- async function resolveOrCreateBranch(client, projectId, branchName) {
228
- const existing = (await listBranchesByName(client, projectId, branchName))[0];
408
+ async function resolveOrCreateBranch(client, projectId, branchName, signal) {
409
+ const existing = (await listBranchesByName(client, projectId, branchName, signal))[0];
229
410
  if (existing) return existing;
230
- if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
411
+ if (!await projectHasDefaultBranch(client, projectId, signal)) throw new CliError({
231
412
  code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
232
413
  domain: "app",
233
414
  summary: `Cannot create branch "${branchName}" from project env`,
@@ -241,67 +422,75 @@ async function resolveOrCreateBranch(client, projectId, branchName) {
241
422
  body: {
242
423
  gitName: branchName,
243
424
  isDefault: false
244
- }
425
+ },
426
+ signal
245
427
  });
246
428
  if (error || !data) {
247
429
  if (response?.status === 409) {
248
- const raced = (await listBranchesByName(client, projectId, branchName))[0];
430
+ const raced = (await listBranchesByName(client, projectId, branchName, signal))[0];
249
431
  if (raced) return raced;
250
432
  }
251
433
  throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
252
434
  }
253
435
  return data.data;
254
436
  }
255
- async function projectHasDefaultBranch(client, projectId) {
437
+ async function projectHasDefaultBranch(client, projectId, signal) {
256
438
  let cursor;
257
439
  while (true) {
258
440
  const query = {};
259
441
  if (cursor !== void 0) query.cursor = cursor;
260
- const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
261
- path: { projectId },
262
- query
263
- } });
442
+ const result = await client.GET("/v1/projects/{projectId}/branches", {
443
+ params: {
444
+ path: { projectId },
445
+ query
446
+ },
447
+ signal
448
+ });
264
449
  if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
265
450
  if (result.data.data.some((branch) => branch.isDefault)) return true;
266
451
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
267
452
  cursor = result.data.pagination.nextCursor;
268
453
  }
269
454
  }
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;
455
+ async function listVariables(client, projectId, resolved, signal) {
456
+ return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
457
+ className: resolved.apiTarget.class,
458
+ filter: (row) => rowMatchesScope(row, resolved)
459
+ }), resolved);
460
+ }
461
+ async function listOverviewVariables(client, projectId, signal) {
462
+ return (await collectEnvironmentVariables(client, projectId, signal, { filter: (row) => row.branchId === null && (row.class === "production" || row.class === "preview") })).sort((left, right) => {
463
+ const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
464
+ return roleOrder !== 0 ? roleOrder : left.key.localeCompare(right.key);
465
+ });
278
466
  }
279
- async function listVariables(client, projectId, resolved) {
467
+ async function collectEnvironmentVariables(client, projectId, signal, options) {
280
468
  const collected = [];
281
469
  let cursor;
282
470
  while (true) {
283
- const query = {
284
- projectId,
285
- class: resolved.apiTarget.class
286
- };
471
+ const query = { projectId };
472
+ if (options.className !== void 0) query.class = options.className;
287
473
  if (cursor !== void 0) query.cursor = cursor;
288
- const result = await client.GET("/v1/environment-variables", { params: { query } });
474
+ const result = await client.GET("/v1/environment-variables", {
475
+ params: { query },
476
+ signal
477
+ });
289
478
  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));
479
+ const page = result.data.data.filter(options.filter);
291
480
  collected.push(...page);
292
481
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
293
482
  cursor = result.data.pagination.nextCursor;
294
483
  }
295
- return materializeEffectiveRows(collected, resolved);
484
+ return collected;
485
+ }
486
+ function roleSortOrder(role) {
487
+ return role === "production" ? 0 : 1;
296
488
  }
297
489
  function rowMatchesScope(row, resolved) {
298
490
  if (row.class !== resolved.apiTarget.class) return false;
299
491
  if (resolved.apiTarget.branchId === null) return row.branchId === null;
300
492
  return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
301
493
  }
302
- function rowMatchesExactScope(row, resolved) {
303
- return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
304
- }
305
494
  function materializeEffectiveRows(rows, resolved) {
306
495
  if (resolved.apiTarget.branchId === null) return rows;
307
496
  const byKey = /* @__PURE__ */ new Map();
@@ -309,39 +498,5 @@ function materializeEffectiveRows(rows, resolved) {
309
498
  for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
310
499
  return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
311
500
  }
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
501
  //#endregion
347
502
  export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };