@prisma/cli 3.0.0-dev.114.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.
@@ -32,6 +32,9 @@ var MockApi = class MockApi {
32
32
  const workspaceIds = this.data.memberships.filter((membership) => membership.userId === userId).map((membership) => membership.workspaceId);
33
33
  return this.data.workspaces.filter((workspace) => workspaceIds.includes(workspace.id));
34
34
  }
35
+ listWorkspaces() {
36
+ return this.data.workspaces;
37
+ }
35
38
  getWorkspace(workspaceId) {
36
39
  return this.data.workspaces.find((workspace) => workspace.id === workspaceId);
37
40
  }
@@ -47,6 +50,36 @@ var MockApi = class MockApi {
47
50
  getProjectForWorkspace(workspaceId, projectId) {
48
51
  return this.listProjectsForWorkspace(workspaceId).find((project) => project.id === projectId);
49
52
  }
53
+ renameProject(projectId, name) {
54
+ const project = this.getProject(projectId);
55
+ if (!project) return;
56
+ project.name = name;
57
+ return project;
58
+ }
59
+ removeProject(projectId) {
60
+ const project = this.getProject(projectId);
61
+ if (!project) return { outcome: "not-found" };
62
+ if (this.data.deployments.some((deployment) => deployment.projectId === projectId)) return { outcome: "blocked" };
63
+ const removedDatabaseIds = new Set((this.data.databases ?? []).filter((database) => database.projectId === projectId).map((database) => database.id));
64
+ this.data.projects = this.data.projects.filter((candidate) => candidate.id !== projectId);
65
+ this.data.branches = this.data.branches.filter((branch) => branch.projectId !== projectId);
66
+ this.data.databases = (this.data.databases ?? []).filter((database) => database.projectId !== projectId);
67
+ this.data.databaseConnections = (this.data.databaseConnections ?? []).filter((connection) => !removedDatabaseIds.has(connection.databaseId));
68
+ return {
69
+ outcome: "removed",
70
+ project
71
+ };
72
+ }
73
+ transferProject(projectId, targetWorkspaceId) {
74
+ const project = this.getProject(projectId);
75
+ if (!project) return { outcome: "not-found" };
76
+ if (!this.getWorkspace(targetWorkspaceId)) return { outcome: "workspace-not-found" };
77
+ project.workspaceId = targetWorkspaceId;
78
+ return {
79
+ outcome: "transferred",
80
+ project
81
+ };
82
+ }
50
83
  listBranchesForProject(projectId) {
51
84
  return this.data.branches.filter((branch) => branch.projectId === projectId);
52
85
  }
@@ -77,6 +77,7 @@ var FileTokenStorage = class {
77
77
  this.signal?.throwIfAborted();
78
78
  try {
79
79
  const credentials = await this.readCredentialsFromDisk();
80
+ if (this.options.pinnedWorkspaceId) return findTokensForWorkspace(credentials, this.options.pinnedWorkspaceId);
80
81
  const context = await this.readAuthContext();
81
82
  if (context.state.activeWorkspaceId) return findTokensForWorkspace(credentials, context.state.activeWorkspaceId);
82
83
  const latest = findLatestValidTokens(credentials);
@@ -138,6 +139,9 @@ var FileTokenStorage = class {
138
139
  this.signal?.throwIfAborted();
139
140
  const credentials = await this.readCredentialsFromDisk();
140
141
  const context = await this.ensureMigratedAuthContext(credentials);
142
+ return this.workspacesFromState(credentials, context);
143
+ }
144
+ workspacesFromState(credentials, context) {
141
145
  return credentials.map((credential) => storedCredentialToTokens(credential)).filter((tokens) => tokens !== null).map((tokens) => {
142
146
  const cached = context.state.workspaces[tokens.workspaceId];
143
147
  const id = typeof cached?.id === "string" && cached.id.trim().length > 0 ? cached.id.trim() : tokens.workspaceId;
@@ -159,6 +163,24 @@ var FileTokenStorage = class {
159
163
  async useWorkspace(workspaceRef) {
160
164
  return this.withRefreshLock(() => this.useWorkspaceUnlocked(workspaceRef));
161
165
  }
166
+ /**
167
+ * Resolve a workspace ref (id, credential workspace id, or cached name)
168
+ * against the locally stored sessions without changing the active
169
+ * workspace. Read-only counterpart of useWorkspace: it reads the auth
170
+ * context as-is and never runs the legacy-state migration, so it writes
171
+ * nothing.
172
+ */
173
+ async resolveWorkspace(workspaceRef) {
174
+ const ref = workspaceRef.trim();
175
+ if (!ref) throw new WorkspaceSelectionError("missing");
176
+ this.signal?.throwIfAborted();
177
+ const credentials = await this.readCredentialsFromDisk();
178
+ const context = await this.readAuthContext();
179
+ const matches = this.workspacesFromState(credentials, context).filter((workspace) => workspaceMatchesRef(workspace, ref));
180
+ if (matches.length === 0) throw new WorkspaceSelectionError("not-found", ref);
181
+ if (matches.length > 1) throw new WorkspaceSelectionError("ambiguous", ref, matches);
182
+ return matches[0];
183
+ }
162
184
  async useWorkspaceUnlocked(workspaceRef) {
163
185
  const ref = workspaceRef.trim();
164
186
  if (!ref) throw new WorkspaceSelectionError("missing");
@@ -327,6 +349,7 @@ var FileTokenStorage = class {
327
349
  await this.writeAuthContext(context.state);
328
350
  }
329
351
  async maybeActivateWorkspaceId(workspaceId) {
352
+ if (this.options.pinnedWorkspaceId) return;
330
353
  const context = await this.readAuthContext();
331
354
  if (this.options.activateOnSetTokens === false && context.exists && context.state.activeWorkspaceId && context.state.activeWorkspaceId !== workspaceId) return;
332
355
  context.state.activeWorkspaceId = workspaceId;
@@ -2,10 +2,10 @@ import { attachCommandDescriptor } from "../../shell/command-meta.js";
2
2
  import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
3
3
  import { configureRuntimeCommand } from "../../shell/runtime.js";
4
4
  import { runCommand } from "../../shell/command-runner.js";
5
- import { runProjectCreate, runProjectLink, runProjectList, runProjectShow } from "../../controllers/project.js";
6
- import { renderProjectList, renderProjectSetup, renderProjectShow, serializeProjectList, serializeProjectSetup, serializeProjectShow } from "../../presenters/project.js";
5
+ import { runProjectCreate, runProjectLink, runProjectList, runProjectRemove, runProjectRename, runProjectShow, runProjectTransfer } from "../../controllers/project.js";
6
+ import { renderProjectList, renderProjectRemove, renderProjectRename, renderProjectSetup, renderProjectShow, renderProjectTransfer, serializeProjectList, serializeProjectRemove, serializeProjectRename, serializeProjectSetup, serializeProjectShow, serializeProjectTransfer } from "../../presenters/project.js";
7
7
  import { createEnvCommand } from "../env.js";
8
- import { Command } from "commander";
8
+ import { Command, Option } from "commander";
9
9
  //#region src/commands/project/index.ts
10
10
  function createProjectCommand(runtime) {
11
11
  const project = attachCommandDescriptor(configureRuntimeCommand(new Command("project"), runtime), "project");
@@ -14,9 +14,57 @@ function createProjectCommand(runtime) {
14
14
  project.addCommand(createProjectShowCommand(runtime));
15
15
  project.addCommand(createProjectCreateCommand(runtime));
16
16
  project.addCommand(createProjectLinkCommand(runtime));
17
+ project.addCommand(createProjectRenameCommand(runtime));
18
+ project.addCommand(createProjectRemoveCommand(runtime));
19
+ project.addCommand(createProjectTransferCommand(runtime));
17
20
  project.addCommand(createEnvCommand(runtime));
18
21
  return project;
19
22
  }
23
+ function createProjectRenameCommand(runtime) {
24
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("rename"), runtime), "project.rename");
25
+ command.argument("<name>", "New project name").addOption(new Option("--project <id-or-name>", "Project id or name"));
26
+ addGlobalFlags(command);
27
+ command.action(async (name, options) => {
28
+ const projectRef = options.project;
29
+ await runCommand(runtime, "project.rename", options, (context) => runProjectRename(context, name, { project: projectRef }), {
30
+ renderHuman: (context, descriptor, result) => renderProjectRename(context, descriptor, result),
31
+ renderJson: (result) => serializeProjectRename(result)
32
+ });
33
+ });
34
+ return command;
35
+ }
36
+ function createProjectRemoveCommand(runtime) {
37
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("remove"), runtime), "project.remove");
38
+ command.argument("<project>", "Project id or name").addOption(new Option("--confirm <project-id>", "Exact project id required to remove"));
39
+ addGlobalFlags(command);
40
+ command.action(async (projectRef, options) => {
41
+ const confirm = options.confirm;
42
+ await runCommand(runtime, "project.remove", options, (context) => runProjectRemove(context, projectRef, { confirm }), {
43
+ renderHuman: (context, descriptor, result) => renderProjectRemove(context, descriptor, result),
44
+ renderJson: (result) => serializeProjectRemove(result)
45
+ });
46
+ });
47
+ return command;
48
+ }
49
+ function createProjectTransferCommand(runtime) {
50
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("transfer"), runtime), "project.transfer");
51
+ command.argument("<project>", "Project id or name").addOption(new Option("--to-workspace <id-or-name>", "Locally authenticated workspace to receive the project")).addOption(new Option("--recipient-token <token>", "Access token for the receiving workspace")).addOption(new Option("--confirm <project-id>", "Exact project id required to transfer"));
52
+ addGlobalFlags(command);
53
+ command.action(async (projectRef, options) => {
54
+ const toWorkspace = options.toWorkspace;
55
+ const recipientToken = options.recipientToken;
56
+ const confirm = options.confirm;
57
+ await runCommand(runtime, "project.transfer", options, (context) => runProjectTransfer(context, projectRef, {
58
+ toWorkspace,
59
+ recipientToken,
60
+ confirm
61
+ }), {
62
+ renderHuman: (context, descriptor, result) => renderProjectTransfer(context, descriptor, result),
63
+ renderJson: (result) => serializeProjectTransfer(result)
64
+ });
65
+ });
66
+ return command;
67
+ }
20
68
  function createProjectCreateCommand(runtime) {
21
69
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("create"), runtime), "project.create");
22
70
  command.argument("<name>", "Project name");
@@ -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 };
@@ -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 };
@@ -86,6 +86,95 @@ function renderProjectSetup(context, _descriptor, result) {
86
86
  function serializeProjectSetup(result) {
87
87
  return result;
88
88
  }
89
+ function renderProjectRename(context, descriptor, result) {
90
+ return renderMutate({
91
+ title: "Renaming project.",
92
+ descriptor,
93
+ context: [
94
+ {
95
+ key: "workspace",
96
+ value: result.workspace.name
97
+ },
98
+ {
99
+ key: "project",
100
+ value: result.previousName
101
+ },
102
+ {
103
+ key: "id",
104
+ value: result.project.id,
105
+ tone: "dim"
106
+ }
107
+ ],
108
+ operationDescription: "Renaming project",
109
+ operationCount: 1,
110
+ details: [`The project is now named "${result.project.name}". Directory bindings pin the project id, so they stay valid.`]
111
+ }, context.ui);
112
+ }
113
+ function serializeProjectRename(result) {
114
+ return result;
115
+ }
116
+ function renderProjectRemove(context, descriptor, result) {
117
+ return renderMutate({
118
+ title: "Removing project.",
119
+ descriptor,
120
+ context: [
121
+ {
122
+ key: "workspace",
123
+ value: result.workspace.name
124
+ },
125
+ {
126
+ key: "project",
127
+ value: result.project.name
128
+ },
129
+ {
130
+ key: "id",
131
+ value: result.project.id,
132
+ tone: "dim"
133
+ }
134
+ ],
135
+ operationDescription: "Removing project",
136
+ operationCount: 1,
137
+ details: ["The project, its databases, and its apps were removed.", ...result.localPin.cleared ? ["This directory's local project binding was cleared."] : []]
138
+ }, context.ui);
139
+ }
140
+ function serializeProjectRemove(result) {
141
+ return result;
142
+ }
143
+ function renderProjectTransfer(context, descriptor, result) {
144
+ return renderMutate({
145
+ title: "Transferring project.",
146
+ descriptor,
147
+ context: [
148
+ {
149
+ key: "workspace",
150
+ value: result.workspace.name
151
+ },
152
+ {
153
+ key: "project",
154
+ value: result.project.name
155
+ },
156
+ {
157
+ key: "id",
158
+ value: result.project.id,
159
+ tone: "dim"
160
+ },
161
+ {
162
+ key: "recipient",
163
+ value: result.recipient.workspaceName ?? result.recipient.workspaceId ?? "workspace of the provided recipient token"
164
+ }
165
+ ],
166
+ operationDescription: "Transferring project",
167
+ operationCount: 1,
168
+ details: [
169
+ "The project now belongs to the recipient workspace; this workspace no longer has access.",
170
+ ...result.localPin.action === "rewritten" ? ["This directory's local project binding now points at the recipient workspace."] : [],
171
+ ...result.localPin.action === "cleared" ? ["This directory's local project binding was cleared."] : []
172
+ ]
173
+ }, context.ui);
174
+ }
175
+ function serializeProjectTransfer(result) {
176
+ return result;
177
+ }
89
178
  function renderGitConnect(context, descriptor, result) {
90
179
  const connection = result.repositoryConnection;
91
180
  return renderMutate({
@@ -171,4 +260,4 @@ function formatGitConnectionDetail(status) {
171
260
  }
172
261
  }
173
262
  //#endregion
174
- export { renderGitConnect, renderGitDisconnect, renderProjectList, renderProjectSetup, renderProjectShow, serializeProjectList, serializeProjectSetup, serializeProjectShow };
263
+ export { renderGitConnect, renderGitDisconnect, renderProjectList, renderProjectRemove, renderProjectRename, renderProjectSetup, renderProjectShow, renderProjectTransfer, serializeProjectList, serializeProjectRemove, serializeProjectRename, serializeProjectSetup, serializeProjectShow, serializeProjectTransfer };
@@ -271,6 +271,36 @@ const DESCRIPTORS = [
271
271
  "prisma-cli project link \"Acme Dashboard\" --json"
272
272
  ]
273
273
  },
274
+ {
275
+ id: "project.rename",
276
+ path: [
277
+ "prisma",
278
+ "project",
279
+ "rename"
280
+ ],
281
+ description: "Rename the resolved Project",
282
+ examples: ["prisma-cli project rename \"Acme Dashboard v2\"", "prisma-cli project rename billing-api --project proj_123"]
283
+ },
284
+ {
285
+ id: "project.remove",
286
+ path: [
287
+ "prisma",
288
+ "project",
289
+ "remove"
290
+ ],
291
+ description: "Remove a Project permanently after exact id confirmation",
292
+ examples: ["prisma-cli project remove proj_123 --confirm proj_123"]
293
+ },
294
+ {
295
+ id: "project.transfer",
296
+ path: [
297
+ "prisma",
298
+ "project",
299
+ "transfer"
300
+ ],
301
+ description: "Transfer a Project to another workspace after exact id confirmation",
302
+ examples: ["prisma-cli project transfer proj_123 --to-workspace \"Prisma Labs\" --confirm proj_123", "prisma-cli project transfer proj_123 --recipient-token <token> --confirm proj_123"]
303
+ },
274
304
  {
275
305
  id: "git.connect",
276
306
  path: [
@@ -1,5 +1,6 @@
1
1
  import { CliError, authConfigInvalidError, authRequiredError, commandCanceledError } from "./errors.js";
2
2
  import { getCommandDescriptor } from "./command-meta.js";
3
+ import { renderSummaryLine } from "./ui.js";
3
4
  import { resolveGlobalFlags } from "./global-flags.js";
4
5
  import { createCommandContext } from "./runtime.js";
5
6
  import { collectCommandDiagnostics } from "../lib/diagnostics.js";
@@ -48,11 +49,16 @@ async function writeCommandSuccess(context, descriptor, success, presenter, dura
48
49
  return;
49
50
  }
50
51
  const rendered = presenter.renderHuman(context, descriptor, success.result);
52
+ const warningLines = success.warnings.map((warning) => renderSummaryLine(context.ui, "warning", warning));
51
53
  const diagnostics = await renderBestEffortCommandDiagnostics(context, {
52
54
  enabled: context.flags.verbose && rendered.length > 0,
53
55
  durationMs
54
56
  });
55
- const humanLines = [...rendered, ...diagnostics];
57
+ const humanLines = [
58
+ ...rendered,
59
+ ...warningLines,
60
+ ...diagnostics
61
+ ];
56
62
  if (stdout.length > 0 && humanLines.length > 0) humanLines.push("");
57
63
  writeHumanLines(context.output, humanLines);
58
64
  writeStdoutLines(context, stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.114.1",
3
+ "version": "3.0.0-dev.115.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {