@prisma/cli 3.0.0-beta.2 → 3.0.0-beta.4

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