@prisma/cli 3.0.0-dev.97.1 → 8.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +3 -14
  2. package/dist/adapters/local-state.js +14 -3
  3. package/dist/{lib/auth → auth}/client.js +11 -3
  4. package/dist/auth/errors.js +50 -0
  5. package/dist/{lib/auth → auth}/guard.js +7 -6
  6. package/dist/{lib/auth → auth}/login.js +147 -6
  7. package/dist/{lib/auth/auth-ops.js → auth/operations.js} +63 -13
  8. package/dist/auth/recipient.js +42 -0
  9. package/dist/{adapters → auth}/token-storage.js +28 -7
  10. package/dist/auth/workspaces.js +171 -0
  11. package/dist/cli-command.js +14 -0
  12. package/dist/cli-name.js +12 -0
  13. package/dist/cli.js +1 -1
  14. package/dist/cli2.js +25 -8
  15. package/dist/command-arguments.js +12 -0
  16. package/dist/commands/agent/index.js +60 -0
  17. package/dist/commands/app/index.js +11 -6
  18. package/dist/commands/auth/index.js +2 -3
  19. package/dist/commands/bucket/index.js +123 -0
  20. package/dist/commands/build/index.js +29 -0
  21. package/dist/commands/database/index.js +92 -2
  22. package/dist/commands/feedback/index.js +20 -0
  23. package/dist/commands/git/index.js +1 -1
  24. package/dist/commands/init/index.js +33 -0
  25. package/dist/commands/project/index.js +54 -5
  26. package/dist/controllers/agent-setup.js +52 -0
  27. package/dist/controllers/agent.js +228 -0
  28. package/dist/controllers/app-env-api.js +3 -2
  29. package/dist/controllers/app-env-file.js +1 -1
  30. package/dist/controllers/app-env.js +5 -5
  31. package/dist/controllers/app.js +423 -358
  32. package/dist/controllers/auth.js +62 -254
  33. package/dist/controllers/branch.js +5 -16
  34. package/dist/controllers/bucket.js +184 -0
  35. package/dist/controllers/build.js +88 -0
  36. package/dist/controllers/database.js +230 -85
  37. package/dist/controllers/feedback.js +90 -0
  38. package/dist/controllers/init.js +814 -0
  39. package/dist/controllers/project.js +345 -184
  40. package/dist/controllers/select-prompt-port.js +1 -0
  41. package/dist/{shell/errors.js → errors.js} +8 -40
  42. package/dist/lib/agent/cli-command.js +20 -0
  43. package/dist/lib/agent/constants.js +12 -0
  44. package/dist/lib/agent/package-manager.js +99 -0
  45. package/dist/lib/agent/setup-status.js +83 -0
  46. package/dist/lib/app/app-provider.js +168 -95
  47. package/dist/lib/app/branch-database-deploy.js +70 -42
  48. package/dist/lib/app/branch-database.js +39 -20
  49. package/dist/lib/app/build-settings.js +8 -3
  50. package/dist/lib/app/build.js +16 -14
  51. package/dist/lib/app/bun-project.js +1 -1
  52. package/dist/lib/app/compute-config.js +29 -31
  53. package/dist/lib/app/deploy-plan.js +1 -0
  54. package/dist/lib/app/deploy-progress.js +7 -7
  55. package/dist/lib/app/env-config.js +1 -1
  56. package/dist/lib/app/env-file.js +2 -2
  57. package/dist/lib/app/env-vars.js +1 -1
  58. package/dist/lib/app/local-dev.js +1 -1
  59. package/dist/lib/app/production-deploy-gate.js +3 -2
  60. package/dist/lib/bucket/provider.js +139 -0
  61. package/dist/lib/database/provider.js +235 -17
  62. package/dist/lib/diagnostics.js +2 -1
  63. package/dist/lib/feedback.js +15 -0
  64. package/dist/lib/git/local-branch.js +1 -1
  65. package/dist/lib/project/interactive-setup.js +6 -5
  66. package/dist/lib/project/local-pin.js +1 -1
  67. package/dist/lib/project/provider.js +93 -0
  68. package/dist/lib/project/resolution.js +11 -8
  69. package/dist/lib/project/setup.js +9 -6
  70. package/dist/lib/version.js +3 -2
  71. package/dist/lib/workspace-id.js +18 -0
  72. package/dist/payload-B88Qvzxk-CrtTXSOe.js +34 -0
  73. package/dist/presenters/agent.js +74 -0
  74. package/dist/presenters/app.js +33 -7
  75. package/dist/presenters/auth.js +3 -2
  76. package/dist/presenters/bucket.js +174 -0
  77. package/dist/presenters/database.js +193 -5
  78. package/dist/presenters/feedback.js +26 -0
  79. package/dist/presenters/init.js +30 -0
  80. package/dist/presenters/project.js +98 -4
  81. package/dist/shell/cli-command.js +2 -0
  82. package/dist/shell/command-meta.js +303 -4
  83. package/dist/shell/command-runner.js +13 -6
  84. package/dist/shell/help.js +6 -5
  85. package/dist/shell/output.js +65 -3
  86. package/dist/shell/prompt.js +12 -5
  87. package/dist/shell/runtime.js +4 -21
  88. package/dist/state-dir.js +12 -0
  89. package/dist/{shell/update-check.js → update-check.js} +6 -5
  90. package/dist/v8/cli.js +14699 -0
  91. package/dist/v8/sender.js +192 -0
  92. package/package.json +21 -13
  93. package/dist/adapters/mock-api.js +0 -136
  94. package/dist/lib/app/app-interaction.js +0 -5
  95. package/dist/shell/command-arguments.js +0 -6
  96. package/dist/use-cases/auth.js +0 -145
  97. package/dist/use-cases/branch.js +0 -47
  98. package/dist/use-cases/create-cli-gateways.js +0 -68
  99. package/dist/use-cases/project.js +0 -30
@@ -1,6 +1,18 @@
1
- import { CliError } from "../../shell/errors.js";
1
+ import { formatPrismaCliCommand } from "../../cli-command.js";
2
+ import { CliError } from "../../errors.js";
3
+ import "../../shell/cli-command.js";
2
4
  //#region src/lib/database/provider.ts
3
- function createManagementDatabaseProvider(client) {
5
+ const SUBSCRIPTION_LOOKUP_TIMEOUT_MS = 3e3;
6
+ function createManagementDatabaseProvider(client, options) {
7
+ const formatCommand = options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
8
+ const toDatabaseApiError = (summary, response, error, signal) => databaseApiError({
9
+ client,
10
+ workspaceId: options?.workspaceId,
11
+ summary,
12
+ response,
13
+ error,
14
+ signal
15
+ });
4
16
  return {
5
17
  async listDatabases(options) {
6
18
  const databases = [];
@@ -14,7 +26,7 @@ function createManagementDatabaseProvider(client) {
14
26
  } },
15
27
  signal: options.signal
16
28
  });
17
- if (result.error || !result.data) throw databaseApiError("Failed to list databases", result.response, result.error);
29
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list databases", result.response, result.error, options.signal);
18
30
  databases.push(...result.data.data);
19
31
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
20
32
  cursor = result.data.pagination.nextCursor;
@@ -26,8 +38,8 @@ function createManagementDatabaseProvider(client) {
26
38
  params: { path: { databaseId } },
27
39
  signal: options?.signal
28
40
  });
29
- if (result.response?.status === 404) return null;
30
- if (result.error || !result.data) throw databaseApiError("Failed to show database", result.response, result.error);
41
+ if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) return null;
42
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to show database", result.response, result.error, options?.signal);
31
43
  const database = result.data.data;
32
44
  return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId));
33
45
  },
@@ -42,7 +54,7 @@ function createManagementDatabaseProvider(client) {
42
54
  },
43
55
  signal: options.signal
44
56
  });
45
- if (result.error || !result.data) throw databaseApiError("Failed to create database", result.response, result.error);
57
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database", result.response, result.error, options.signal);
46
58
  return normalizeCreatedDatabase(result.data.data, options.projectId);
47
59
  },
48
60
  async removeDatabase(databaseId, options) {
@@ -50,14 +62,14 @@ function createManagementDatabaseProvider(client) {
50
62
  params: { path: { databaseId } },
51
63
  signal: options?.signal
52
64
  });
53
- if (result.error) throw databaseApiError("Failed to remove database", result.response, result.error);
65
+ if (result.error) throw await toDatabaseApiError("Failed to remove database", result.response, result.error, options?.signal);
54
66
  },
55
67
  async listConnections(databaseId, options) {
56
68
  const result = await client.GET("/v1/databases/{databaseId}/connections", {
57
69
  params: { path: { databaseId } },
58
70
  signal: options?.signal
59
71
  });
60
- if (result.error || !result.data) throw databaseApiError("Failed to list database connections", result.response, result.error);
72
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database connections", result.response, result.error, options?.signal);
61
73
  return result.data.data.map((connection) => normalizeConnection(connection, databaseId));
62
74
  },
63
75
  async createConnection(options) {
@@ -66,7 +78,7 @@ function createManagementDatabaseProvider(client) {
66
78
  body: { name: options.name },
67
79
  signal: options.signal
68
80
  });
69
- if (result.error || !result.data) throw databaseApiError("Failed to create database connection", result.response, result.error);
81
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to create database connection", result.response, result.error, options.signal);
70
82
  return normalizeCreatedConnection(result.data.data, options.databaseId);
71
83
  },
72
84
  async removeConnection(connectionId, options) {
@@ -74,7 +86,56 @@ function createManagementDatabaseProvider(client) {
74
86
  params: { path: { id: connectionId } },
75
87
  signal: options?.signal
76
88
  });
77
- if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
89
+ if (result.error) throw await toDatabaseApiError("Failed to remove database connection", result.response, result.error, options?.signal);
90
+ },
91
+ async getUsage(databaseId, options) {
92
+ const result = await client.GET("/v1/databases/{databaseId}/usage", {
93
+ params: {
94
+ path: { databaseId },
95
+ query: {
96
+ ...options?.from ? { startDate: options.from } : {},
97
+ ...options?.to ? { endDate: options.to } : {}
98
+ }
99
+ },
100
+ signal: options?.signal
101
+ });
102
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to fetch database usage", result.response, result.error, options?.signal);
103
+ return normalizeUsage(result.data);
104
+ },
105
+ async listBackups(databaseId, options) {
106
+ const result = await client.GET("/v1/databases/{databaseId}/backups", {
107
+ params: {
108
+ path: { databaseId },
109
+ query: { ...options?.limit !== void 0 ? { limit: options.limit } : {} }
110
+ },
111
+ signal: options?.signal
112
+ });
113
+ if (result.response?.status === 422 && !isPlanLimitApiError(result.error)) throw backupsUnsupportedError(databaseId, result.error);
114
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to list database backups", result.response, result.error, options?.signal);
115
+ return normalizeBackupList(result.data);
116
+ },
117
+ async restoreDatabase(options) {
118
+ const result = await client.POST("/v1/databases/{targetDatabaseId}/restore", {
119
+ params: { path: { targetDatabaseId: options.targetDatabaseId } },
120
+ body: { source: {
121
+ type: "backup",
122
+ databaseId: options.sourceDatabaseId,
123
+ backupId: options.backupId
124
+ } },
125
+ signal: options.signal
126
+ });
127
+ if (result.response?.status === 409 && !isPlanLimitApiError(result.error)) throw restoreConflictError(options.targetDatabaseId, result.error, formatCommand);
128
+ if (result.response?.status === 404 && !isPlanLimitApiError(result.error)) throw restoreBackupNotFoundError(options, result.error, formatCommand);
129
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to restore database", result.response, result.error, options.signal);
130
+ return normalizeDatabase(result.data.data, options.projectId);
131
+ },
132
+ async rotateConnection(connectionId, options) {
133
+ const result = await client.POST("/v1/connections/{id}/rotate", {
134
+ params: { path: { id: connectionId } },
135
+ signal: options?.signal
136
+ });
137
+ if (result.error || !result.data) throw await toDatabaseApiError("Failed to rotate database connection", result.response, result.error, options?.signal);
138
+ return normalizeRotatedConnection(result.data.data);
78
139
  }
79
140
  };
80
141
  }
@@ -151,17 +212,174 @@ function requireDatabaseProjectId(database, fallbackProjectId) {
151
212
  function extractConnectionString(connection) {
152
213
  return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
153
214
  }
154
- function databaseApiError(summary, response, error) {
155
- const status = response?.status ?? 0;
215
+ /** Absence is carried, not filled in. A `0` here reached the card, the
216
+ * stdout lane and the json record as though the API had measured it, and
217
+ * "you used nothing" is the answer a user acts on. The unit is the same
218
+ * problem an order of magnitude worse: `GiB` was a guess, and a value the
219
+ * API denominates in bytes would print against it unchanged. */
220
+ function normalizeUsageMetric(metric) {
221
+ return {
222
+ used: metric?.used ?? null,
223
+ unit: metric?.unit ?? null
224
+ };
225
+ }
226
+ function normalizeUsage(usage) {
227
+ return {
228
+ period: {
229
+ start: usage.period?.start ?? null,
230
+ end: usage.period?.end ?? null
231
+ },
232
+ metrics: {
233
+ operations: normalizeUsageMetric(usage.metrics?.operations),
234
+ storage: normalizeUsageMetric(usage.metrics?.storage)
235
+ },
236
+ generatedAt: usage.generatedAt ?? null
237
+ };
238
+ }
239
+ function normalizeBackupList(body) {
240
+ return {
241
+ backups: (body.data ?? []).map((backup) => ({
242
+ id: backup.id,
243
+ backupType: backup.backupType ?? "",
244
+ status: backup.status ?? "",
245
+ size: backup.size ?? null,
246
+ createdAt: backup.createdAt ?? ""
247
+ })),
248
+ retentionDays: body.meta?.backupRetentionDays ?? null,
249
+ hasMore: body.pagination?.hasMore ?? false
250
+ };
251
+ }
252
+ function normalizeRotatedConnection(connection) {
253
+ const connectionString = extractConnectionString(connection);
254
+ if (!connectionString) throw new CliError({
255
+ code: "DATABASE_CONNECTION_STRING_MISSING",
256
+ domain: "database",
257
+ summary: "Rotated connection did not return a connection string",
258
+ why: "Rotated connection strings are one-time-view secrets, but the Management API did not include one in this rotate response.",
259
+ fix: "Re-run the rotation, or create a replacement connection and store the returned URL immediately.",
260
+ exitCode: 1,
261
+ nextSteps: []
262
+ });
263
+ const database = connection.database?.id && connection.database?.name ? {
264
+ id: connection.database.id,
265
+ name: connection.database.name
266
+ } : null;
267
+ return {
268
+ connection: normalizeConnection(connection, connection.database?.id ?? connection.databaseId ?? ""),
269
+ database,
270
+ connectionString
271
+ };
272
+ }
273
+ function backupsUnsupportedError(databaseId, error) {
156
274
  return new CliError({
157
- code: error?.error?.code ?? "DATABASE_API_ERROR",
275
+ code: "DATABASE_BACKUPS_UNSUPPORTED",
158
276
  domain: "database",
159
- summary,
160
- why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
161
- fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
277
+ summary: "Backups are not available for this database",
278
+ why: error?.error?.message ?? `The platform does not manage backups for database "${databaseId}", for example because it is a remote/BYO database.`,
279
+ fix: "Use your own backup tooling for externally managed databases.",
280
+ exitCode: 1,
281
+ nextSteps: []
282
+ });
283
+ }
284
+ function restoreBackupNotFoundError(options, error, formatCommand) {
285
+ const listCommand = formatCommand([
286
+ "database",
287
+ "backup",
288
+ "list",
289
+ options.sourceDatabaseId
290
+ ]);
291
+ return new CliError({
292
+ code: "DATABASE_BACKUP_NOT_FOUND",
293
+ domain: "database",
294
+ summary: "Database backup not found",
295
+ why: error?.error?.message ?? `No backup matched "${options.backupId}" for database "${options.sourceDatabaseId}".`,
296
+ fix: `Pass a backup id from ${listCommand}.`,
297
+ exitCode: 1,
298
+ nextSteps: [listCommand]
299
+ });
300
+ }
301
+ function restoreConflictError(targetDatabaseId, error, formatCommand) {
302
+ return new CliError({
303
+ code: "DATABASE_RESTORE_CONFLICT",
304
+ domain: "database",
305
+ summary: "Database cannot be restored right now",
306
+ why: error?.error?.message ?? `Database "${targetDatabaseId}" is provisioning or already recovering.`,
307
+ fix: "Wait for the database to become ready, then retry the restore.",
308
+ exitCode: 1,
309
+ nextSteps: [formatCommand([
310
+ "database",
311
+ "show",
312
+ targetDatabaseId
313
+ ])]
314
+ });
315
+ }
316
+ async function databaseApiError(options) {
317
+ if (isPlanLimitApiError(options.error)) return planLimitReachedError(options);
318
+ const status = options.response?.status ?? 0;
319
+ return new CliError({
320
+ code: options.error?.error?.code ?? "DATABASE_API_ERROR",
321
+ domain: "database",
322
+ summary: options.summary,
323
+ why: options.error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
324
+ fix: options.error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
162
325
  exitCode: 1,
163
326
  nextSteps: []
164
327
  });
165
328
  }
329
+ async function planLimitReachedError(options) {
330
+ const subscription = options.workspaceId ? await readWorkspaceSubscription(options.client, options.workspaceId, options.signal) : null;
331
+ const workspaceLine = options.workspaceId ? `Workspace: ${options.workspaceId}` : "Workspace: unavailable";
332
+ const planName = subscription?.planName || null;
333
+ const usageBlocked = subscription?.usageBlocked ?? null;
334
+ const upgradeUrl = subscription?.upgradeUrl || null;
335
+ const recoveryLines = [...planName ? [`Current plan: ${planName}`] : [], upgradeUrl ? `Upgrade: ${upgradeUrl}` : "Upgrade: Open Prisma Console and upgrade the affected workspace plan."];
336
+ return new CliError({
337
+ code: "PLAN_LIMIT_REACHED",
338
+ domain: "database",
339
+ summary: "Workspace plan limit reached",
340
+ why: "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
341
+ fix: upgradeUrl ? `Upgrade the workspace plan at ${upgradeUrl}.` : "Open Prisma Console and upgrade the affected workspace plan.",
342
+ meta: {
343
+ workspaceId: options.workspaceId ?? null,
344
+ blockedFeature: null,
345
+ planName,
346
+ usageBlocked,
347
+ upgradeUrl
348
+ },
349
+ exitCode: 1,
350
+ nextSteps: [],
351
+ humanLines: [
352
+ "Workspace plan limit reached [PLAN_LIMIT_REACHED]",
353
+ "",
354
+ "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.",
355
+ "",
356
+ workspaceLine,
357
+ ...recoveryLines
358
+ ]
359
+ });
360
+ }
361
+ function isPlanLimitApiError(error) {
362
+ return error?.error?.code === "planLimitReached";
363
+ }
364
+ async function readWorkspaceSubscription(client, workspaceId, signal) {
365
+ signal?.throwIfAborted();
366
+ const timeoutController = new AbortController();
367
+ const timeout = setTimeout(() => timeoutController.abort(), SUBSCRIPTION_LOOKUP_TIMEOUT_MS);
368
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
369
+ try {
370
+ const result = await client.GET("/v1/workspaces/{id}/subscription", {
371
+ params: { path: { id: workspaceId } },
372
+ signal: requestSignal
373
+ });
374
+ signal?.throwIfAborted();
375
+ if (result.error) return null;
376
+ return result.data?.data ?? null;
377
+ } catch {
378
+ signal?.throwIfAborted();
379
+ return null;
380
+ } finally {
381
+ clearTimeout(timeout);
382
+ }
383
+ }
166
384
  //#endregion
167
- export { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase };
385
+ export { createManagementDatabaseProvider };
@@ -1,5 +1,6 @@
1
1
  import { resolveLocalStateFilePath } from "../adapters/local-state.js";
2
- import { resolveStateDir } from "../shell/runtime.js";
2
+ import { resolveStateDir } from "../state-dir.js";
3
+ import "../shell/runtime.js";
3
4
  import { readLocalGitState } from "./git/local-status.js";
4
5
  //#region src/lib/diagnostics.ts
5
6
  async function collectCommandDiagnostics(context, options = {}) {
@@ -0,0 +1,15 @@
1
+ //#region src/lib/feedback.ts
2
+ const FEEDBACK_TIMEOUT_MS = 3e3;
3
+ const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1e3} seconds.`;
4
+ function isTimeout(error) {
5
+ return error instanceof Error && error.name === "TimeoutError";
6
+ }
7
+ function unreachableDetail(error) {
8
+ if (isTimeout(error)) return TIMEOUT_DETAIL;
9
+ return `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`;
10
+ }
11
+ function unreadableBodyDetail(error) {
12
+ return isTimeout(error) ? TIMEOUT_DETAIL : "The feedback service response could not be read.";
13
+ }
14
+ //#endregion
15
+ export { FEEDBACK_TIMEOUT_MS, unreachableDetail, unreadableBodyDetail };
@@ -1,5 +1,5 @@
1
- import { access, readFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { access, readFile } from "node:fs/promises";
3
3
  //#region src/lib/git/local-branch.ts
4
4
  /**
5
5
  * Resolves the checked-out branch the way git does: the nearest `.git`
@@ -1,4 +1,4 @@
1
- import { usageError } from "../../shell/errors.js";
1
+ import { usageError } from "../../errors.js";
2
2
  import { selectPrompt, textPrompt } from "../../shell/prompt.js";
3
3
  import { inferTargetName, sortProjects } from "./resolution.js";
4
4
  import { toProjectSummary, validateProjectSetupNameText } from "./setup.js";
@@ -12,6 +12,10 @@ async function promptForProjectSetupChoice(options) {
12
12
  output: options.context.runtime.stderr,
13
13
  message: "Which Project should this directory use?",
14
14
  choices: [
15
+ {
16
+ label: "+ Create a new Project",
17
+ value: { kind: "create" }
18
+ },
15
19
  ...sortedProjects.map((project) => ({
16
20
  label: duplicateNames.has(project.name) ? `${project.name} (${project.id})` : project.name,
17
21
  value: {
@@ -19,10 +23,6 @@ async function promptForProjectSetupChoice(options) {
19
23
  project
20
24
  }
21
25
  })),
22
- {
23
- label: "Create a new Project",
24
- value: { kind: "create" }
25
- },
26
26
  {
27
27
  label: "Cancel",
28
28
  value: { kind: "cancel" }
@@ -40,6 +40,7 @@ async function promptForProjectSetupChoice(options) {
40
40
  const rawName = await textPrompt({
41
41
  input: options.context.runtime.stdin,
42
42
  output: options.context.runtime.stderr,
43
+ signal: options.context.runtime.signal,
43
44
  message: "Project name",
44
45
  placeholder: suggestedName.name,
45
46
  validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
@@ -1,5 +1,5 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { Result, TaggedError, UnhandledException } from "better-result";
4
4
  //#region src/lib/project/local-pin.ts
5
5
  const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
@@ -0,0 +1,93 @@
1
+ import { formatPrismaCliCommand } from "../../cli-command.js";
2
+ import { CliError } from "../../errors.js";
3
+ import "../../shell/cli-command.js";
4
+ //#region src/lib/project/provider.ts
5
+ function createManagementProjectProvider(client) {
6
+ return {
7
+ async renameProject(options) {
8
+ const result = await client.PATCH("/v1/projects/{id}", {
9
+ params: { path: { id: options.projectId } },
10
+ body: { name: options.name },
11
+ signal: options.signal
12
+ });
13
+ const status = result.response?.status ?? 0;
14
+ if (status === 400 || status === 422) throw projectRenameFailedError(options.name, result.error);
15
+ if (result.error || !result.data) throw projectApiError("Failed to rename project", result.response, result.error);
16
+ const project = result.data.data;
17
+ return {
18
+ id: project.id,
19
+ name: project.name,
20
+ ...project.url ? { url: project.url } : {}
21
+ };
22
+ },
23
+ async removeProject(options) {
24
+ const result = await client.DELETE("/v1/projects/{id}", {
25
+ params: { path: { id: options.projectId } },
26
+ signal: options.signal
27
+ });
28
+ if (result.response?.status === 400) throw projectRemoveBlockedError(options.projectId, result.error);
29
+ if (result.error) throw projectApiError("Failed to remove project", result.response, result.error);
30
+ },
31
+ async transferProject(options) {
32
+ const result = await client.POST("/v1/projects/{id}/transfer", {
33
+ params: { path: { id: options.projectId } },
34
+ body: { recipientAccessToken: options.recipientAccessToken },
35
+ signal: options.signal
36
+ });
37
+ if (result.response?.status === 400) throw projectTransferRejectedError(options.projectId, result.error);
38
+ if (result.error) throw projectApiError("Failed to transfer project", result.response, result.error);
39
+ }
40
+ };
41
+ }
42
+ function projectRenameFailedError(name, error) {
43
+ return new CliError({
44
+ code: "PROJECT_RENAME_FAILED",
45
+ domain: "project",
46
+ summary: "Project rename failed",
47
+ why: error?.error?.message ?? `The platform rejected the name "${name}".`,
48
+ fix: error?.error?.hint ?? "Pass a different project name and retry the rename.",
49
+ exitCode: 1,
50
+ nextSteps: []
51
+ });
52
+ }
53
+ function projectRemoveBlockedError(projectId, error) {
54
+ return new CliError({
55
+ code: "PROJECT_REMOVE_BLOCKED",
56
+ domain: "project",
57
+ summary: "Project cannot be removed yet",
58
+ why: error?.error?.message ?? `Project "${projectId}" still has active deployments.`,
59
+ fix: "Remove the project's apps first, then retry the removal.",
60
+ exitCode: 1,
61
+ nextSteps: [formatPrismaCliCommand([
62
+ "app",
63
+ "remove",
64
+ "--app",
65
+ "<name>"
66
+ ])]
67
+ });
68
+ }
69
+ function projectTransferRejectedError(projectId, error) {
70
+ return new CliError({
71
+ code: "PROJECT_TRANSFER_REJECTED",
72
+ domain: "project",
73
+ summary: "Project transfer was rejected",
74
+ why: error?.error?.message ?? `The platform rejected the transfer of project "${projectId}", for example because the recipient token is invalid or expired.`,
75
+ fix: "Check the recipient workspace session or token and retry the transfer.",
76
+ exitCode: 1,
77
+ nextSteps: []
78
+ });
79
+ }
80
+ function projectApiError(summary, response, error) {
81
+ const status = response?.status ?? 0;
82
+ return new CliError({
83
+ code: error?.error?.code ?? "PROJECT_API_ERROR",
84
+ domain: "project",
85
+ summary,
86
+ why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
87
+ fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
88
+ exitCode: 1,
89
+ nextSteps: []
90
+ });
91
+ }
92
+ //#endregion
93
+ export { createManagementProjectProvider, projectApiError };
@@ -1,8 +1,9 @@
1
- import { CliError } from "../../shell/errors.js";
1
+ import { formatCommandArgument } from "../../command-arguments.js";
2
+ import { CliError } from "../../errors.js";
3
+ import { sameWorkspaceId } from "../workspace-id.js";
2
4
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
3
- import { formatCommandArgument } from "../../shell/command-arguments.js";
4
- import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { readFile } from "node:fs/promises";
6
7
  import { Result, TaggedError, matchError } from "better-result";
7
8
  //#region src/lib/project/resolution.ts
8
9
  var ProjectNotFoundError = class extends TaggedError("ProjectNotFoundError")() {
@@ -283,8 +284,9 @@ async function inferTargetName(cwd, signal) {
283
284
  source: "directory-name"
284
285
  };
285
286
  }
287
+ const INFERRED_TARGET_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
286
288
  function isValidInferredTargetName(value) {
287
- return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value);
289
+ return INFERRED_TARGET_NAME.test(value);
288
290
  }
289
291
  function sortProjects(projects) {
290
292
  return projects.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
@@ -321,7 +323,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
321
323
  const localPin = settings.localPin;
322
324
  if (!localPin) return Result.ok(null);
323
325
  if (localPin.kind === "present") {
324
- if (localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({
326
+ if (!sameWorkspaceId(localPin.pin.workspaceId, options.workspace.id)) return Result.err(new LocalProjectWorkspaceMismatchError({
325
327
  pinnedWorkspaceId: localPin.pin.workspaceId,
326
328
  pinnedProjectId: localPin.pin.projectId,
327
329
  activeWorkspace: options.workspace
@@ -334,7 +336,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
334
336
  }));
335
337
  }
336
338
  const platformMapping = await resolveDurablePlatformMapping();
337
- if (platformMapping && platformMapping.workspace.id === options.workspace.id) return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
339
+ if (platformMapping && sameWorkspaceId(platformMapping.workspace.id, options.workspace.id)) return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
338
340
  targetName: platformMapping.name,
339
341
  targetNameSource: "platform-mapping"
340
342
  }));
@@ -345,7 +347,7 @@ async function readImplicitLocalPin(options, settings) {
345
347
  const localPinResult = await readLocalResolutionPin(options.projectDir ?? options.context.runtime.cwd, options.context.runtime.signal);
346
348
  if (localPinResult.isErr()) return Result.err(localPinReadErrorToProjectError(localPinResult.error));
347
349
  const localPin = localPinResult.value;
348
- if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({
350
+ if (localPin.kind === "present" && !sameWorkspaceId(localPin.pin.workspaceId, options.workspace.id)) return Result.err(new LocalProjectWorkspaceMismatchError({
349
351
  pinnedWorkspaceId: localPin.pin.workspaceId,
350
352
  pinnedProjectId: localPin.pin.projectId,
351
353
  activeWorkspace: options.workspace
@@ -379,7 +381,8 @@ function toProjectSummary(project) {
379
381
  return {
380
382
  id: project.id,
381
383
  name: project.name,
382
- ...project.url ? { url: project.url } : {}
384
+ ...project.url ? { url: project.url } : {},
385
+ ...project.defaultRegion != null ? { defaultRegion: project.defaultRegion } : {}
383
386
  };
384
387
  }
385
388
  //#endregion
@@ -1,7 +1,6 @@
1
- import { CliError, usageError } from "../../shell/errors.js";
1
+ import { CliError, usageError } from "../../errors.js";
2
2
  import { shortenHomePath } from "../fs/home-path.js";
3
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
4
- import "../../shell/command-arguments.js";
5
4
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
6
5
  import path from "node:path";
7
6
  import { Result, matchError } from "better-result";
@@ -15,8 +14,9 @@ function validateProjectSetupNameText(value, fallback) {
15
14
  }
16
15
  function resolveProjectForSetup(projectRef, projects, workspace) {
17
16
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
18
- if (matches.length === 1) return matches[0];
19
17
  if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
18
+ const match = matches[0];
19
+ if (match !== void 0) return match;
20
20
  throw projectNotFoundError(projectRef, workspace);
21
21
  }
22
22
  async function bindProjectToDirectory(context, workspace, project, action, directory = context.runtime.cwd) {
@@ -82,7 +82,8 @@ function toProjectSummary(project) {
82
82
  return {
83
83
  id: project.id,
84
84
  name: project.name,
85
- ...project.url ? { url: project.url } : {}
85
+ ...project.url ? { url: project.url } : {},
86
+ ...project.defaultRegion != null ? { defaultRegion: project.defaultRegion } : {}
86
87
  };
87
88
  }
88
89
  function projectSetupNameRequiredError(command) {
@@ -111,18 +112,20 @@ function projectCreateFailedError(error, projectName, workspace, options) {
111
112
  nextSteps: options.nextSteps
112
113
  });
113
114
  }
115
+ const PATH_SEPARATORS = /[\\/]/;
114
116
  function formatSetupDirectory(directory, context) {
115
117
  if (path.resolve(directory) !== path.resolve(context.runtime.cwd)) return shortenHomePath(directory, context.runtime.env);
116
- const basename = directory.split(/[\\/]/).filter(Boolean).pop();
118
+ const basename = directory.split(PATH_SEPARATORS).filter(Boolean).pop();
117
119
  return basename ? `./${basename}` : ".";
118
120
  }
121
+ const HTTP_STATUS_IN_MESSAGE = /\(HTTP (\d{3})\)/;
119
122
  function extractHttpStatus(error) {
120
123
  if (!error || typeof error !== "object") return null;
121
124
  const candidate = error;
122
125
  if (typeof candidate.statusCode === "number") return candidate.statusCode;
123
126
  if (typeof candidate.status === "number") return candidate.status;
124
127
  if (typeof candidate.message === "string") {
125
- const match = /\(HTTP (\d{3})\)/.exec(candidate.message);
128
+ const match = HTTP_STATUS_IN_MESSAGE.exec(candidate.message);
126
129
  if (match) return Number.parseInt(match[1], 10);
127
130
  }
128
131
  return null;
@@ -1,4 +1,5 @@
1
- import { CliError } from "../shell/errors.js";
1
+ import { CliError } from "../errors.js";
2
+ import { CLI_NAME } from "../cli-name.js";
2
3
  import { createRequire } from "node:module";
3
4
  import process from "node:process";
4
5
  //#region src/lib/version.ts
@@ -23,7 +24,7 @@ function getCliVersion() {
23
24
  return pkg.version;
24
25
  }
25
26
  function getCliName() {
26
- return "prisma-cli";
27
+ return CLI_NAME;
27
28
  }
28
29
  function detectInvocation(env, argv) {
29
30
  if (env.npm_config_user_agent?.startsWith("bun")) return "bunx";
@@ -0,0 +1,18 @@
1
+ //#region src/lib/workspace-id.ts
2
+ /**
3
+ * A workspace reaches the CLI under two ids for the same workspace: a
4
+ * credential's `workspace_id` claim carries the bare id, while the
5
+ * management API and anything derived from it carry the same id behind
6
+ * a `wksp_` prefix. Comparing the two forms directly silently matches
7
+ * nothing, so every comparison between workspace ids of different
8
+ * origin goes through here.
9
+ */
10
+ const WORKSPACE_ID_PREFIX = "wksp_";
11
+ function stripWorkspacePrefix(value) {
12
+ return value.startsWith(WORKSPACE_ID_PREFIX) ? value.slice(5) : value;
13
+ }
14
+ function sameWorkspaceId(left, right) {
15
+ return stripWorkspacePrefix(left) === stripWorkspacePrefix(right);
16
+ }
17
+ //#endregion
18
+ export { sameWorkspaceId };
@@ -0,0 +1,34 @@
1
+ //#region ../cli-telemetry/dist/payload-B88Qvzxk.js
2
+ function isNonEmptyString(value) {
3
+ return typeof value === "string" && value.length > 0;
4
+ }
5
+ function isStringArray(value) {
6
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
7
+ }
8
+ /**
9
+ * Runtime validation for {@link ParentToSenderPayload}. The child sender
10
+ * uses this before `postEvent` so a payload missing a required field
11
+ * cannot silently produce a degraded telemetry event downstream.
12
+ *
13
+ * Same semantics as the ORM CLI's arktype schema (this repo carries no
14
+ * arktype dependency, so the checks are spelled out): required scalars
15
+ * must be non-empty strings; the optional `databaseTarget` override is
16
+ * a `string` when present (no `null` — see the type's doc-block); the
17
+ * string array is validated element-by-element. Size caps are enforced
18
+ * by the backend, not here — IPC is structured-cloned and the
19
+ * parent/child agree on the schema by version-coupling.
20
+ */
21
+ function isParentToSenderPayload(value) {
22
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
23
+ const record = value;
24
+ if (!isNonEmptyString(record.installationId)) return false;
25
+ if (!isNonEmptyString(record.version)) return false;
26
+ if (!isNonEmptyString(record.command)) return false;
27
+ if (!isStringArray(record.flags)) return false;
28
+ if (!isNonEmptyString(record.projectRoot)) return false;
29
+ if (!isNonEmptyString(record.endpoint)) return false;
30
+ if ("databaseTarget" in record && typeof record.databaseTarget !== "string") return false;
31
+ return true;
32
+ }
33
+ //#endregion
34
+ export { isParentToSenderPayload as t };