@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.
- package/dist/adapters/mock-api.js +33 -0
- package/dist/adapters/token-storage.js +23 -0
- package/dist/commands/project/index.js +51 -3
- package/dist/controllers/project.js +326 -3
- package/dist/lib/auth/recipient.js +42 -0
- package/dist/lib/project/provider.js +92 -0
- package/dist/presenters/project.js +90 -1
- package/dist/shell/command-meta.js +30 -0
- package/dist/shell/command-runner.js +7 -1
- package/package.json +1 -1
|
@@ -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 = [
|
|
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);
|