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