@prisma/cli 3.0.0-dev.112.1 → 3.0.0-dev.115.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.
@@ -1,9 +1,12 @@
1
+ import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command.js";
1
2
  import { formatCommandArgument } from "../shell/command-arguments.js";
2
- import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors.js";
3
+ import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceAmbiguousError, workspaceNotAuthenticatedError, workspaceRequiredError } from "../shell/errors.js";
3
4
  import { renderSummaryLine } from "../shell/ui.js";
4
5
  import { canPrompt } from "../shell/runtime.js";
6
+ import { SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client.js";
7
+ import { WorkspaceSelectionError } from "../adapters/token-storage.js";
5
8
  import { createAppProvider } from "../lib/app/app-provider.js";
6
- import { readLocalResolutionPin } from "../lib/project/local-pin.js";
9
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin, writeLocalResolutionPin } from "../lib/project/local-pin.js";
7
10
  import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectResolutionErrorToCliError, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
8
11
  import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
9
12
  import { requireComputeAuth } from "../lib/auth/guard.js";
@@ -11,7 +14,11 @@ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js
11
14
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
12
15
  import { requireAuthenticatedAuthState } from "./auth.js";
13
16
  import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
17
+ import { RecipientSessionInvalidError, resolveRecipientWorkspaceSession } from "../lib/auth/recipient.js";
18
+ import { createManagementProjectProvider, projectRemoveBlockedError, projectRenameFailedError, projectTransferRejectedError } from "../lib/project/provider.js";
14
19
  import { createProjectUseCases } from "../use-cases/project.js";
20
+ import path from "node:path";
21
+ import { unlink } from "node:fs/promises";
15
22
  import { matchError } from "better-result";
16
23
  import open from "open";
17
24
  //#region src/controllers/project.ts
@@ -217,6 +224,322 @@ async function projectLinkTargetRequiredError(context, projects) {
217
224
  })
218
225
  });
219
226
  }
227
+ async function runProjectRename(context, newName, options) {
228
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
229
+ if (!workspace) throw workspaceRequiredError();
230
+ const name = newName.trim();
231
+ if (!isValidProjectSetupName(name)) throw projectSetupNameRequiredError("project rename");
232
+ const { provider, target } = await requireProjectCommandContext(context, workspace, options.project, "project rename");
233
+ const previousName = target.project.name;
234
+ return {
235
+ command: "project.rename",
236
+ result: {
237
+ workspace,
238
+ project: await provider.renameProject({
239
+ projectId: target.project.id,
240
+ name,
241
+ signal: context.runtime.signal
242
+ }),
243
+ previousName
244
+ },
245
+ warnings: [],
246
+ nextSteps: []
247
+ };
248
+ }
249
+ async function runProjectRemove(context, projectRef, options) {
250
+ const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
251
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
252
+ if (!workspace) throw workspaceRequiredError();
253
+ const { provider, projects } = await requireProjectMutationContext(context, workspace);
254
+ const project = toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace));
255
+ requireProjectExactConfirmation({
256
+ id: project.id,
257
+ confirm: options.confirm,
258
+ summary: "Confirm project removal",
259
+ why: "Removing a project is permanent, deletes its databases, and stops its apps, so it requires the exact project id.",
260
+ nextStep: formatCommand([
261
+ "project",
262
+ "remove",
263
+ project.id,
264
+ "--confirm",
265
+ project.id
266
+ ])
267
+ });
268
+ await provider.removeProject({
269
+ projectId: project.id,
270
+ signal: context.runtime.signal
271
+ });
272
+ const warnings = [];
273
+ return {
274
+ command: "project.remove",
275
+ result: {
276
+ workspace,
277
+ project,
278
+ localPin: { cleared: await cleanupLocalPinForProject(context, project.id, { onError: (message) => warnings.push(message) }) }
279
+ },
280
+ warnings,
281
+ nextSteps: []
282
+ };
283
+ }
284
+ async function runProjectTransfer(context, projectRef, options) {
285
+ const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
286
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
287
+ if (!workspace) throw workspaceRequiredError();
288
+ if (options.toWorkspace && options.recipientToken) throw usageError("Choose one transfer recipient source", "--to-workspace and --recipient-token are mutually exclusive.", "Pass either --to-workspace <id-or-name> or --recipient-token <token>.", [formatCommand([
289
+ "project",
290
+ "transfer",
291
+ "<project>",
292
+ "--to-workspace",
293
+ "<id-or-name>",
294
+ "--confirm",
295
+ "<project-id>"
296
+ ])], "project");
297
+ if (!options.toWorkspace?.trim() && !options.recipientToken?.trim()) throw transferRecipientRequiredError(formatCommand);
298
+ const { provider, projects } = await requireProjectMutationContext(context, workspace);
299
+ const project = toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace));
300
+ requireProjectExactConfirmation({
301
+ id: project.id,
302
+ confirm: options.confirm,
303
+ summary: "Confirm project transfer",
304
+ why: "Transferring moves the project to another workspace and this workspace loses access, so it requires the exact project id.",
305
+ nextStep: `${formatCommand([
306
+ "project",
307
+ "transfer",
308
+ project.id
309
+ ])} ${options.toWorkspace ? `--to-workspace ${formatCommandArgument(options.toWorkspace)}` : "--recipient-token <token>"} --confirm ${project.id}`
310
+ });
311
+ const recipient = await resolveTransferRecipient(context, options);
312
+ await provider.transferProject({
313
+ projectId: project.id,
314
+ recipientAccessToken: recipient.accessToken,
315
+ signal: context.runtime.signal
316
+ });
317
+ const warnings = [];
318
+ const pinAction = await rewriteOrClearLocalPinForProject(context, project.id, recipient.workspaceId, { onError: (message) => warnings.push(message) });
319
+ return {
320
+ command: "project.transfer",
321
+ result: {
322
+ workspace,
323
+ project,
324
+ recipient: {
325
+ workspaceId: recipient.workspaceId,
326
+ workspaceName: recipient.workspaceName,
327
+ source: recipient.source
328
+ },
329
+ localPin: { action: pinAction }
330
+ },
331
+ warnings,
332
+ nextSteps: options.toWorkspace ? [`${formatCommand([
333
+ "auth",
334
+ "workspace",
335
+ "use"
336
+ ])} ${formatCommandArgument(options.toWorkspace)}`] : []
337
+ };
338
+ }
339
+ async function resolveTransferRecipient(context, options) {
340
+ const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
341
+ const recipientToken = options.recipientToken?.trim();
342
+ if (recipientToken) return {
343
+ accessToken: recipientToken,
344
+ workspaceId: isRealMode(context) ? null : recipientToken,
345
+ workspaceName: null,
346
+ source: "recipient-token"
347
+ };
348
+ const workspaceRef = options.toWorkspace?.trim();
349
+ if (!workspaceRef) throw transferRecipientRequiredError(formatCommand);
350
+ if (!isRealMode(context)) {
351
+ const matches = context.api.listWorkspaces().filter((candidate) => candidate.id === workspaceRef || candidate.name.toLowerCase() === workspaceRef.toLowerCase());
352
+ if (matches.length === 0) throw workspaceNotAuthenticatedError(workspaceRef);
353
+ if (matches.length > 1) throw workspaceAmbiguousError(workspaceRef, matches.map((match) => ({
354
+ id: match.id,
355
+ name: match.name,
356
+ credentialWorkspaceId: match.id
357
+ })));
358
+ return {
359
+ accessToken: matches[0].id,
360
+ workspaceId: matches[0].id,
361
+ workspaceName: matches[0].name,
362
+ source: "workspace-session"
363
+ };
364
+ }
365
+ if (context.runtime.env["PRISMA_SERVICE_TOKEN"] !== void 0) throw transferRecipientUnavailableError(formatCommand);
366
+ try {
367
+ const session = await resolveRecipientWorkspaceSession(workspaceRef, context.runtime.env, context.runtime.signal);
368
+ return {
369
+ accessToken: session.accessToken,
370
+ workspaceId: session.workspace.id,
371
+ workspaceName: session.workspace.name,
372
+ source: "workspace-session"
373
+ };
374
+ } catch (error) {
375
+ if (error instanceof WorkspaceSelectionError) {
376
+ if (error.reason === "ambiguous") throw workspaceAmbiguousError(error.workspaceRef ?? workspaceRef, error.matches.map((match) => ({
377
+ id: match.id,
378
+ name: match.name,
379
+ credentialWorkspaceId: match.credentialWorkspaceId
380
+ })));
381
+ throw workspaceNotAuthenticatedError(error.workspaceRef ?? workspaceRef);
382
+ }
383
+ if (error instanceof RecipientSessionInvalidError) throw workspaceNotAuthenticatedError(error.workspaceRef);
384
+ throw error;
385
+ }
386
+ }
387
+ async function requireProjectMutationContext(context, workspace) {
388
+ if (isRealMode(context)) {
389
+ const client = await requireProjectClient(context);
390
+ return {
391
+ provider: createManagementProjectProvider(client),
392
+ projects: await listRealWorkspaceProjects(client, workspace, context.runtime.signal)
393
+ };
394
+ }
395
+ return {
396
+ provider: createFixtureProjectProvider(context),
397
+ projects: listFixtureWorkspaceProjects(context, workspace)
398
+ };
399
+ }
400
+ async function requireProjectCommandContext(context, workspace, explicitProject, commandName) {
401
+ const client = isRealMode(context) ? await requireProjectClient(context) : null;
402
+ const listProjects = async () => client ? listRealWorkspaceProjects(client, workspace, context.runtime.signal) : listFixtureWorkspaceProjects(context, workspace);
403
+ const targetResult = await resolveProjectTarget({
404
+ context,
405
+ workspace,
406
+ explicitProject,
407
+ listProjects,
408
+ commandName
409
+ });
410
+ if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
411
+ return {
412
+ provider: client ? createManagementProjectProvider(client) : createFixtureProjectProvider(context),
413
+ target: targetResult.value
414
+ };
415
+ }
416
+ async function requireProjectClient(context) {
417
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
418
+ if (!client) throw authRequiredError();
419
+ return client;
420
+ }
421
+ function createFixtureProjectProvider(context) {
422
+ const fixtureFormatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
423
+ return {
424
+ async renameProject(options) {
425
+ const renamed = context.api.renameProject(options.projectId, options.name);
426
+ if (!renamed) throw projectRenameFailedError(options.name, void 0);
427
+ return {
428
+ id: renamed.id,
429
+ name: renamed.name,
430
+ ...renamed.url ? { url: renamed.url } : {}
431
+ };
432
+ },
433
+ async removeProject(options) {
434
+ const removed = context.api.removeProject(options.projectId);
435
+ if (removed.outcome === "blocked") throw projectRemoveBlockedError(options.projectId, void 0);
436
+ if (removed.outcome === "not-found") throw new CliError({
437
+ code: "PROJECT_NOT_FOUND",
438
+ domain: "project",
439
+ summary: "Project not found",
440
+ why: `No project matched "${options.projectId}".`,
441
+ fix: `Pass a project id or name from ${fixtureFormatCommand(["project", "list"])}.`,
442
+ exitCode: 1,
443
+ nextSteps: [fixtureFormatCommand(["project", "list"])]
444
+ });
445
+ },
446
+ async transferProject(options) {
447
+ if (context.api.transferProject(options.projectId, options.recipientAccessToken).outcome !== "transferred") throw projectTransferRejectedError(options.projectId, void 0);
448
+ }
449
+ };
450
+ }
451
+ function requireProjectExactConfirmation(options) {
452
+ if (options.confirm === options.id) return;
453
+ throw new CliError({
454
+ code: "CONFIRMATION_REQUIRED",
455
+ domain: "project",
456
+ summary: options.summary,
457
+ why: options.why,
458
+ fix: `Rerun with --confirm ${options.id}.`,
459
+ exitCode: 2,
460
+ nextSteps: [options.nextStep],
461
+ meta: {
462
+ expectedConfirm: options.id,
463
+ receivedConfirm: options.confirm ?? null
464
+ }
465
+ });
466
+ }
467
+ function transferRecipientRequiredError(formatCommand) {
468
+ return new CliError({
469
+ code: "TRANSFER_RECIPIENT_REQUIRED",
470
+ domain: "project",
471
+ summary: "Transfer recipient required",
472
+ why: "Project transfer needs the receiving workspace.",
473
+ fix: "Pass --to-workspace <id-or-name> for a locally authenticated workspace, or --recipient-token <token> for a cross-account transfer.",
474
+ exitCode: 2,
475
+ nextSteps: [formatCommand([
476
+ "auth",
477
+ "workspace",
478
+ "list"
479
+ ]), formatCommand([
480
+ "project",
481
+ "transfer",
482
+ "<project>",
483
+ "--to-workspace",
484
+ "<id-or-name>",
485
+ "--confirm",
486
+ "<project-id>"
487
+ ])]
488
+ });
489
+ }
490
+ function transferRecipientUnavailableError(formatCommand) {
491
+ return new CliError({
492
+ code: "TRANSFER_RECIPIENT_UNAVAILABLE",
493
+ domain: "project",
494
+ summary: "Local workspace sessions are unavailable",
495
+ why: `--to-workspace resolves locally stored OAuth sessions, but ${SERVICE_TOKEN_ENV_VAR} is set and service-token mode does not read them.`,
496
+ fix: "Pass --recipient-token <token> with an access token for the receiving workspace, or unset the service token.",
497
+ exitCode: 1,
498
+ nextSteps: [formatCommand([
499
+ "project",
500
+ "transfer",
501
+ "<project>",
502
+ "--recipient-token",
503
+ "<token>",
504
+ "--confirm",
505
+ "<project-id>"
506
+ ])]
507
+ });
508
+ }
509
+ async function cleanupLocalPinForProject(context, projectId, hooks) {
510
+ const pinResult = await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
511
+ if (pinResult.isErr()) return false;
512
+ const pin = pinResult.value;
513
+ if (pin.kind !== "present" || pin.pin.projectId !== projectId) return false;
514
+ try {
515
+ await unlink(path.join(context.runtime.cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
516
+ return true;
517
+ } catch {
518
+ hooks.onError(`The local pin ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} points at the removed project but could not be deleted.`);
519
+ return false;
520
+ }
521
+ }
522
+ async function rewriteOrClearLocalPinForProject(context, projectId, recipientWorkspaceId, hooks) {
523
+ const pinResult = await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
524
+ if (pinResult.isErr()) return "none";
525
+ const pin = pinResult.value;
526
+ if (pin.kind !== "present" || pin.pin.projectId !== projectId) return "none";
527
+ if (recipientWorkspaceId) {
528
+ if ((await writeLocalResolutionPin(context.runtime.cwd, {
529
+ workspaceId: recipientWorkspaceId,
530
+ projectId
531
+ }, context.runtime.signal)).isOk()) return "rewritten";
532
+ hooks.onError(`The local pin ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} points at the transferred project but could not be rewritten.`);
533
+ return "none";
534
+ }
535
+ try {
536
+ await unlink(path.join(context.runtime.cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH));
537
+ return "cleared";
538
+ } catch {
539
+ hooks.onError(`The local pin ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} points at the transferred project but could not be cleared.`);
540
+ return "none";
541
+ }
542
+ }
220
543
  async function runGitConnect(context, gitUrl, options = {}) {
221
544
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
222
545
  if (!workspace) throw workspaceRequiredError();
@@ -728,4 +1051,4 @@ function repoConnectionFixForStatus(status) {
728
1051
  return "Re-run with --trace for the underlying API response details.";
729
1052
  }
730
1053
  //#endregion
731
- export { listFixtureWorkspaceProjects, listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectCreate, runProjectLink, runProjectList, runProjectShow };
1054
+ export { listFixtureWorkspaceProjects, listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectCreate, runProjectLink, runProjectList, runProjectRemove, runProjectRename, runProjectShow, runProjectTransfer };
@@ -0,0 +1,42 @@
1
+ import { getApiBaseUrl } from "./client.js";
2
+ import { FileTokenStorage } from "../../adapters/token-storage.js";
3
+ import { createManagementApiSdk } from "@prisma/management-api-sdk";
4
+ //#region src/lib/auth/recipient.ts
5
+ var RecipientSessionInvalidError = class extends Error {
6
+ constructor(workspaceRef) {
7
+ super(`The stored session for workspace "${workspaceRef}" could not be validated.`);
8
+ this.workspaceRef = workspaceRef;
9
+ this.name = "RecipientSessionInvalidError";
10
+ }
11
+ };
12
+ /**
13
+ * Resolve a locally stored OAuth workspace session and return a validated
14
+ * access token for it, refreshing through the SDK when the stored token has
15
+ * expired. The active workspace pointer is never touched.
16
+ *
17
+ * Throws WorkspaceSelectionError when the ref does not match exactly one
18
+ * stored session, and RecipientSessionInvalidError when the session cannot
19
+ * be validated or refreshed.
20
+ */
21
+ async function resolveRecipientWorkspaceSession(workspaceRef, env = process.env, signal) {
22
+ const workspace = await new FileTokenStorage(env, signal).resolveWorkspace(workspaceRef);
23
+ const pinnedStorage = new FileTokenStorage(env, signal, {
24
+ activateOnSetTokens: false,
25
+ lockSetTokens: false,
26
+ pinnedWorkspaceId: workspace.credentialWorkspaceId
27
+ });
28
+ if ((await createManagementApiSdk({
29
+ clientId: "cmm3lndn701oo0uefvxzo0ivw",
30
+ redirectUri: "http://localhost:0/auth/callback",
31
+ tokenStorage: pinnedStorage,
32
+ apiBaseUrl: getApiBaseUrl(env)
33
+ }).client.GET("/v1/workspaces", { signal })).error) throw new RecipientSessionInvalidError(workspaceRef);
34
+ const tokens = await pinnedStorage.getTokens();
35
+ if (!tokens) throw new RecipientSessionInvalidError(workspaceRef);
36
+ return {
37
+ workspace,
38
+ accessToken: tokens.accessToken
39
+ };
40
+ }
41
+ //#endregion
42
+ export { RecipientSessionInvalidError, resolveRecipientWorkspaceSession };
@@ -1,6 +1,8 @@
1
+ import { formatPrismaCliCommand } from "../../shell/cli-command.js";
1
2
  import { CliError } from "../../shell/errors.js";
2
3
  //#region src/lib/database/provider.ts
3
- function createManagementDatabaseProvider(client) {
4
+ function createManagementDatabaseProvider(client, options) {
5
+ const formatCommand = options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
4
6
  return {
5
7
  async listDatabases(options) {
6
8
  const databases = [];
@@ -75,6 +77,55 @@ function createManagementDatabaseProvider(client) {
75
77
  signal: options?.signal
76
78
  });
77
79
  if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
80
+ },
81
+ async getUsage(databaseId, options) {
82
+ const result = await client.GET("/v1/databases/{databaseId}/usage", {
83
+ params: {
84
+ path: { databaseId },
85
+ query: {
86
+ ...options?.from ? { startDate: options.from } : {},
87
+ ...options?.to ? { endDate: options.to } : {}
88
+ }
89
+ },
90
+ signal: options?.signal
91
+ });
92
+ if (result.error || !result.data) throw databaseApiError("Failed to fetch database usage", result.response, result.error);
93
+ return normalizeUsage(result.data);
94
+ },
95
+ async listBackups(databaseId, options) {
96
+ const result = await client.GET("/v1/databases/{databaseId}/backups", {
97
+ params: {
98
+ path: { databaseId },
99
+ query: { ...options?.limit !== void 0 ? { limit: options.limit } : {} }
100
+ },
101
+ signal: options?.signal
102
+ });
103
+ if (result.response?.status === 422) throw backupsUnsupportedError(databaseId, result.error);
104
+ if (result.error || !result.data) throw databaseApiError("Failed to list database backups", result.response, result.error);
105
+ return normalizeBackupList(result.data);
106
+ },
107
+ async restoreDatabase(options) {
108
+ const result = await client.POST("/v1/databases/{targetDatabaseId}/restore", {
109
+ params: { path: { targetDatabaseId: options.targetDatabaseId } },
110
+ body: { source: {
111
+ type: "backup",
112
+ databaseId: options.sourceDatabaseId,
113
+ backupId: options.backupId
114
+ } },
115
+ signal: options.signal
116
+ });
117
+ if (result.response?.status === 409) throw restoreConflictError(options.targetDatabaseId, result.error, formatCommand);
118
+ if (result.response?.status === 404) throw restoreBackupNotFoundError(options, result.error, formatCommand);
119
+ if (result.error || !result.data) throw databaseApiError("Failed to restore database", result.response, result.error);
120
+ return normalizeDatabase(result.data.data, options.projectId);
121
+ },
122
+ async rotateConnection(connectionId, options) {
123
+ const result = await client.POST("/v1/connections/{id}/rotate", {
124
+ params: { path: { id: connectionId } },
125
+ signal: options?.signal
126
+ });
127
+ if (result.error || !result.data) throw databaseApiError("Failed to rotate database connection", result.response, result.error);
128
+ return normalizeRotatedConnection(result.data.data);
78
129
  }
79
130
  };
80
131
  }
@@ -151,6 +202,102 @@ function requireDatabaseProjectId(database, fallbackProjectId) {
151
202
  function extractConnectionString(connection) {
152
203
  return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
153
204
  }
205
+ function normalizeUsage(usage) {
206
+ return {
207
+ period: {
208
+ start: usage.period?.start ?? "",
209
+ end: usage.period?.end ?? ""
210
+ },
211
+ metrics: {
212
+ operations: {
213
+ used: usage.metrics?.operations?.used ?? 0,
214
+ unit: usage.metrics?.operations?.unit ?? "ops"
215
+ },
216
+ storage: {
217
+ used: usage.metrics?.storage?.used ?? 0,
218
+ unit: usage.metrics?.storage?.unit ?? "GiB"
219
+ }
220
+ },
221
+ generatedAt: usage.generatedAt ?? ""
222
+ };
223
+ }
224
+ function normalizeBackupList(body) {
225
+ return {
226
+ backups: (body.data ?? []).map((backup) => ({
227
+ id: backup.id,
228
+ backupType: backup.backupType ?? "unknown",
229
+ status: backup.status ?? "unknown",
230
+ size: backup.size ?? null,
231
+ createdAt: backup.createdAt ?? ""
232
+ })),
233
+ retentionDays: body.meta?.backupRetentionDays ?? null,
234
+ hasMore: body.pagination?.hasMore ?? false
235
+ };
236
+ }
237
+ function normalizeRotatedConnection(connection) {
238
+ const connectionString = extractConnectionString(connection);
239
+ if (!connectionString) throw new CliError({
240
+ code: "DATABASE_CONNECTION_STRING_MISSING",
241
+ domain: "database",
242
+ summary: "Rotated connection did not return a connection string",
243
+ why: "Rotated connection strings are one-time-view secrets, but the Management API did not include one in this rotate response.",
244
+ fix: "Re-run the rotation, or create a replacement connection and store the returned URL immediately.",
245
+ exitCode: 1,
246
+ nextSteps: []
247
+ });
248
+ const database = connection.database?.id && connection.database?.name ? {
249
+ id: connection.database.id,
250
+ name: connection.database.name
251
+ } : null;
252
+ return {
253
+ connection: normalizeConnection(connection, connection.database?.id ?? connection.databaseId ?? ""),
254
+ database,
255
+ connectionString
256
+ };
257
+ }
258
+ function backupsUnsupportedError(databaseId, error) {
259
+ return new CliError({
260
+ code: "DATABASE_BACKUPS_UNSUPPORTED",
261
+ domain: "database",
262
+ summary: "Backups are not available for this database",
263
+ why: error?.error?.message ?? `The platform does not manage backups for database "${databaseId}", for example because it is a remote/BYO database.`,
264
+ fix: "Use your own backup tooling for externally managed databases.",
265
+ exitCode: 1,
266
+ nextSteps: []
267
+ });
268
+ }
269
+ function restoreBackupNotFoundError(options, error, formatCommand) {
270
+ const listCommand = formatCommand([
271
+ "database",
272
+ "backup",
273
+ "list",
274
+ options.sourceDatabaseId
275
+ ]);
276
+ return new CliError({
277
+ code: "DATABASE_BACKUP_NOT_FOUND",
278
+ domain: "database",
279
+ summary: "Database backup not found",
280
+ why: error?.error?.message ?? `No backup matched "${options.backupId}" for database "${options.sourceDatabaseId}".`,
281
+ fix: `Pass a backup id from ${listCommand}.`,
282
+ exitCode: 1,
283
+ nextSteps: [listCommand]
284
+ });
285
+ }
286
+ function restoreConflictError(targetDatabaseId, error, formatCommand) {
287
+ return new CliError({
288
+ code: "DATABASE_RESTORE_CONFLICT",
289
+ domain: "database",
290
+ summary: "Database cannot be restored right now",
291
+ why: error?.error?.message ?? `Database "${targetDatabaseId}" is provisioning or already recovering.`,
292
+ fix: "Wait for the database to become ready, then retry the restore.",
293
+ exitCode: 1,
294
+ nextSteps: [formatCommand([
295
+ "database",
296
+ "show",
297
+ targetDatabaseId
298
+ ])]
299
+ });
300
+ }
154
301
  function databaseApiError(summary, response, error) {
155
302
  const status = response?.status ?? 0;
156
303
  return new CliError({
@@ -0,0 +1,92 @@
1
+ import { formatPrismaCliCommand } from "../../shell/cli-command.js";
2
+ import { CliError } from "../../shell/errors.js";
3
+ //#region src/lib/project/provider.ts
4
+ function createManagementProjectProvider(client) {
5
+ return {
6
+ async renameProject(options) {
7
+ const result = await client.PATCH("/v1/projects/{id}", {
8
+ params: { path: { id: options.projectId } },
9
+ body: { name: options.name },
10
+ signal: options.signal
11
+ });
12
+ const status = result.response?.status ?? 0;
13
+ if (status === 400 || status === 422) throw projectRenameFailedError(options.name, result.error);
14
+ if (result.error || !result.data) throw projectApiError("Failed to rename project", result.response, result.error);
15
+ const project = result.data.data;
16
+ return {
17
+ id: project.id,
18
+ name: project.name,
19
+ ...project.url ? { url: project.url } : {}
20
+ };
21
+ },
22
+ async removeProject(options) {
23
+ const result = await client.DELETE("/v1/projects/{id}", {
24
+ params: { path: { id: options.projectId } },
25
+ signal: options.signal
26
+ });
27
+ if (result.response?.status === 400) throw projectRemoveBlockedError(options.projectId, result.error);
28
+ if (result.error) throw projectApiError("Failed to remove project", result.response, result.error);
29
+ },
30
+ async transferProject(options) {
31
+ const result = await client.POST("/v1/projects/{id}/transfer", {
32
+ params: { path: { id: options.projectId } },
33
+ body: { recipientAccessToken: options.recipientAccessToken },
34
+ signal: options.signal
35
+ });
36
+ if (result.response?.status === 400) throw projectTransferRejectedError(options.projectId, result.error);
37
+ if (result.error) throw projectApiError("Failed to transfer project", result.response, result.error);
38
+ }
39
+ };
40
+ }
41
+ function projectRenameFailedError(name, error) {
42
+ return new CliError({
43
+ code: "PROJECT_RENAME_FAILED",
44
+ domain: "project",
45
+ summary: "Project rename failed",
46
+ why: error?.error?.message ?? `The platform rejected the name "${name}".`,
47
+ fix: error?.error?.hint ?? "Pass a different project name and retry the rename.",
48
+ exitCode: 1,
49
+ nextSteps: []
50
+ });
51
+ }
52
+ function projectRemoveBlockedError(projectId, error) {
53
+ return new CliError({
54
+ code: "PROJECT_REMOVE_BLOCKED",
55
+ domain: "project",
56
+ summary: "Project cannot be removed yet",
57
+ why: error?.error?.message ?? `Project "${projectId}" still has active deployments.`,
58
+ fix: "Remove the project's apps first, then retry the removal.",
59
+ exitCode: 1,
60
+ nextSteps: [formatPrismaCliCommand([
61
+ "app",
62
+ "remove",
63
+ "--app",
64
+ "<name>"
65
+ ])]
66
+ });
67
+ }
68
+ function projectTransferRejectedError(projectId, error) {
69
+ return new CliError({
70
+ code: "PROJECT_TRANSFER_REJECTED",
71
+ domain: "project",
72
+ summary: "Project transfer was rejected",
73
+ why: error?.error?.message ?? `The platform rejected the transfer of project "${projectId}", for example because the recipient token is invalid or expired.`,
74
+ fix: "Check the recipient workspace session or token and retry the transfer.",
75
+ exitCode: 1,
76
+ nextSteps: []
77
+ });
78
+ }
79
+ function projectApiError(summary, response, error) {
80
+ const status = response?.status ?? 0;
81
+ return new CliError({
82
+ code: error?.error?.code ?? "PROJECT_API_ERROR",
83
+ domain: "project",
84
+ summary,
85
+ why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
86
+ fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
87
+ exitCode: 1,
88
+ nextSteps: []
89
+ });
90
+ }
91
+ //#endregion
92
+ export { createManagementProjectProvider, projectRemoveBlockedError, projectRenameFailedError, projectTransferRejectedError };