@prisma/cli 3.0.0-alpha.3 → 3.0.0-alpha.5

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,214 +1,504 @@
1
- import { UnsafeConfigWriteError, readLinkedProjectId, writeLinkedProjectId } from "../adapters/config.js";
2
- import { CliError, authRequiredError, usageError } from "../shell/errors.js";
1
+ import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
+ import { renderSummaryLine } from "../shell/ui.js";
3
3
  import { canPrompt } from "../shell/runtime.js";
4
4
  import { requireComputeAuth } from "../lib/auth/guard.js";
5
- import { createProjectUseCases, projectNotFoundError } from "../use-cases/project.js";
6
- import { createSelectPromptPort } from "./select-prompt-port.js";
7
- import { createAuthUseCases } from "../use-cases/auth.js";
5
+ import { resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
8
6
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
9
- import { readAuthState } from "../lib/auth/auth-ops.js";
10
7
  import { requireAuthenticatedAuthState } from "./auth.js";
8
+ import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
9
+ import { createProjectUseCases } from "../use-cases/project.js";
10
+ import open from "open";
11
11
  //#region src/controllers/project.ts
12
+ const GITHUB_INSTALL_POLL_INTERVAL_MS = 2e3;
13
+ const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
12
14
  function isRealMode(context) {
13
15
  return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
14
16
  }
15
17
  async function runProjectList(context) {
18
+ const authState = await requireAuthenticatedAuthState(context);
19
+ const workspace = authState.workspace;
20
+ if (!workspace) throw workspaceRequiredError();
16
21
  if (isRealMode(context)) {
17
- const authState = await requireAuthenticatedAuthState(context);
18
22
  const client = await requireComputeAuth(context.runtime.env);
19
- const workspace = authState.workspace;
20
- if (!client || !workspace) throw authRequiredError();
21
- const { data: projectsData } = await client.GET("/v1/projects", {});
23
+ if (!client) throw authRequiredError();
22
24
  return {
23
25
  command: "project.list",
24
26
  result: {
25
27
  workspace,
26
- linkedProjectId: await readLinkedProjectId(context.runtime.cwd),
27
- projects: (projectsData?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
28
- id: project.id,
29
- name: project.name
30
- })).sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))
28
+ projects: sortProjects(await listRealWorkspaceProjects(client, workspace)).map(toProjectSummary)
31
29
  },
32
30
  warnings: [],
33
- nextSteps: ["prisma-cli project link"]
31
+ nextSteps: []
34
32
  };
35
33
  }
36
- const authState = await requireAuthenticatedAuthState(context);
37
34
  return {
38
35
  command: "project.list",
39
36
  result: await createProjectUseCases(createCliUseCaseGateways(context)).list(authState),
40
37
  warnings: [],
41
- nextSteps: ["prisma-cli project link"]
38
+ nextSteps: []
39
+ };
40
+ }
41
+ async function runProjectShow(context, explicitProject) {
42
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
43
+ if (!workspace) throw workspaceRequiredError();
44
+ return {
45
+ command: "project.show",
46
+ result: isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject),
47
+ warnings: [],
48
+ nextSteps: []
42
49
  };
43
50
  }
44
- async function runProjectShow(context) {
51
+ async function runGitConnect(context, gitUrl, options = {}) {
52
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
53
+ if (!workspace) throw workspaceRequiredError();
45
54
  if (isRealMode(context)) {
46
- const linkedProjectId = await readLinkedProjectId(context.runtime.cwd);
47
- if (!linkedProjectId) return {
48
- command: "project.show",
49
- result: {
50
- linkedProjectId: null,
51
- workspace: null,
52
- project: null
53
- },
54
- warnings: [],
55
- nextSteps: ["prisma-cli project link"]
56
- };
57
- const authState = await readAuthState(context.runtime.env);
58
- if (!authState.authenticated || !authState.workspace) return {
59
- command: "project.show",
55
+ const client = await requireComputeAuth(context.runtime.env);
56
+ if (!client) throw authRequiredError();
57
+ const target = await resolveProjectShowInRealMode(context, workspace, options.project);
58
+ const repository = await resolveRepositoryForConnect(context, gitUrl);
59
+ const api = client;
60
+ const existing = await readFirstSourceRepository(api, target.project.id);
61
+ if (existing) {
62
+ const existingConnection = toRepositoryConnection(existing);
63
+ if (repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) return {
64
+ command: "git.connect",
65
+ result: {
66
+ ...target,
67
+ repositoryConnection: existingConnection
68
+ },
69
+ warnings: [],
70
+ nextSteps: []
71
+ };
72
+ throw repoAlreadyConnectedError(existingConnection.repository.fullName);
73
+ }
74
+ const resolvedRepository = await resolveInstalledRepository(context, api, workspace.id, repository);
75
+ const { data, error, response } = await api.POST("/v1/source-repositories", { body: {
76
+ projectId: target.project.id,
77
+ provider: "github",
78
+ providerRepositoryId: resolvedRepository.repository.id,
79
+ installationId: resolvedRepository.installation.id
80
+ } });
81
+ if (error || !data) throw repoConnectionApiError("Failed to connect GitHub repository", response, error);
82
+ return {
83
+ command: "git.connect",
60
84
  result: {
61
- linkedProjectId,
62
- workspace: null,
63
- project: null
85
+ ...target,
86
+ repositoryConnection: toRepositoryConnection(data.data)
64
87
  },
65
88
  warnings: [],
66
- nextSteps: ["prisma-cli auth login"]
89
+ nextSteps: []
67
90
  };
68
- const client = await requireComputeAuth(context.runtime.env);
69
- if (!client) return {
70
- command: "project.show",
91
+ }
92
+ const target = await resolveProjectShowInFixtureMode(context, workspace, options.project);
93
+ const repository = await resolveRepositoryForConnect(context, gitUrl);
94
+ const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
95
+ if (existingConnection) {
96
+ if (repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) return {
97
+ command: "git.connect",
71
98
  result: {
72
- linkedProjectId,
73
- workspace: null,
74
- project: null
99
+ ...target,
100
+ repositoryConnection: existingConnection
75
101
  },
76
102
  warnings: [],
77
- nextSteps: ["prisma-cli auth login"]
103
+ nextSteps: []
78
104
  };
79
- try {
80
- const { data } = await client.GET("/v1/projects/{id}", { params: { path: { id: linkedProjectId } } });
81
- const project = data?.data;
82
- if (!project || project.workspace.id !== authState.workspace.id) return {
83
- command: "project.show",
84
- result: {
85
- linkedProjectId,
86
- workspace: null,
87
- project: null
88
- },
89
- warnings: [],
90
- nextSteps: []
91
- };
92
- return {
93
- command: "project.show",
94
- result: {
95
- linkedProjectId,
96
- workspace: {
97
- id: project.workspace.id,
98
- name: project.workspace.name
99
- },
100
- project: {
101
- id: project.id,
102
- name: project.name
103
- }
104
- },
105
- warnings: [],
106
- nextSteps: []
107
- };
108
- } catch {
109
- return {
110
- command: "project.show",
111
- result: {
112
- linkedProjectId,
113
- workspace: null,
114
- project: null
115
- },
116
- warnings: [],
117
- nextSteps: []
118
- };
119
- }
105
+ throw repoAlreadyConnectedError(existingConnection.repository.fullName);
120
106
  }
121
- const gateways = createCliUseCaseGateways(context);
122
- const authUseCases = createAuthUseCases(gateways);
123
- const projectUseCases = createProjectUseCases(gateways);
124
- const authState = await authUseCases.whoami();
125
- const result = await projectUseCases.show(authState);
107
+ const connection = createPendingRepositoryConnection(repository);
108
+ await context.stateStore.setRepositoryConnection(target.project.id, connection);
126
109
  return {
127
- command: "project.show",
128
- result,
110
+ command: "git.connect",
111
+ result: {
112
+ ...target,
113
+ repositoryConnection: connection
114
+ },
129
115
  warnings: [],
130
- nextSteps: result.linkedProjectId ? authState.authenticated ? [] : ["prisma-cli auth login"] : ["prisma-cli project link"]
116
+ nextSteps: []
131
117
  };
132
118
  }
133
- async function runProjectLink(context, projectId) {
134
- if (!projectId && !canPrompt(context)) throw projectSelectionRequiredError();
119
+ async function runGitDisconnect(context, options = {}) {
120
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
121
+ if (!workspace) throw workspaceRequiredError();
135
122
  if (isRealMode(context)) {
136
- const authState = await requireAuthenticatedAuthState(context);
137
123
  const client = await requireComputeAuth(context.runtime.env);
138
- const workspace = authState.workspace;
139
- if (!client || !workspace) throw authRequiredError();
140
- let selectedProject;
141
- if (projectId) try {
142
- const { data } = await client.GET("/v1/projects/{id}", { params: { path: { id: projectId } } });
143
- if (!data?.data || data.data.workspace.id !== workspace.id) throw projectNotFoundError(`The project "${projectId}" does not exist in workspace "${workspace.name}".`, "Run prisma-cli project list and choose a project id from the active workspace.");
144
- selectedProject = data.data;
145
- } catch (error) {
146
- if (error instanceof CliError) throw error;
147
- throw projectNotFoundError(`The project "${projectId}" does not exist in workspace "${workspace.name}".`, "Run prisma-cli project list and choose a project id from the active workspace.");
148
- }
149
- else {
150
- const { data: projectsData } = await client.GET("/v1/projects", {});
151
- const projects = (projectsData?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
152
- id: project.id,
153
- name: project.name,
154
- workspace: project.workspace
155
- })).sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
156
- if (projects.length === 0) throw projectNotFoundError(`No projects are available in workspace "${workspace.name}".`, "Use prisma-cli app deploy to create project context, or switch workspaces and try again.", []);
157
- selectedProject = await createSelectPromptPort(context).select({
158
- message: "Select a project",
159
- choices: projects.map((project) => ({
160
- label: `${project.name} (${project.id})`,
161
- value: project
162
- }))
163
- });
164
- }
165
- try {
166
- await writeLinkedProjectId(context.runtime.cwd, selectedProject.id);
167
- } catch (error) {
168
- if (error instanceof UnsafeConfigWriteError) throw usageError("Project link requires a writable Prisma config", error.message, "Update prisma.config.ts to use a recognizable project field, or remove it and rerun prisma-cli project link.", ["prisma-cli project link proj_123"], "project");
169
- throw error;
170
- }
124
+ if (!client) throw authRequiredError();
125
+ const target = await resolveProjectShowInRealMode(context, workspace, options.project);
126
+ const api = client;
127
+ const existing = await readFirstSourceRepository(api, target.project.id);
128
+ if (!existing) throw repoNotConnectedError();
129
+ const { error, response } = await api.DELETE("/v1/source-repositories/{id}", { params: { path: { id: existing.id } } });
130
+ if (error) throw repoConnectionApiError("Failed to disconnect GitHub repository", response, error);
171
131
  return {
172
- command: "project.link",
132
+ command: "git.disconnect",
173
133
  result: {
174
- linkedProjectId: selectedProject.id,
175
- workspace: {
176
- id: selectedProject.workspace.id,
177
- name: selectedProject.workspace.name
178
- },
179
- project: {
180
- id: selectedProject.id,
181
- name: selectedProject.name
182
- }
134
+ ...target,
135
+ repositoryConnection: toRepositoryConnection(existing)
183
136
  },
184
137
  warnings: [],
185
- nextSteps: ["prisma-cli project show", "prisma-cli app deploy"]
138
+ nextSteps: []
186
139
  };
187
140
  }
188
- const projectUseCases = createProjectUseCases(createCliUseCaseGateways(context));
189
- const authState = await requireAuthenticatedAuthState(context);
190
- const resolvedProjectId = projectId ?? await resolveProjectIdForLink(context, authState, projectUseCases);
141
+ const target = await resolveProjectShowInFixtureMode(context, workspace, options.project);
142
+ const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
143
+ if (!existingConnection) throw repoNotConnectedError();
144
+ await context.stateStore.clearRepositoryConnection(target.project.id);
191
145
  return {
192
- command: "project.link",
193
- result: await projectUseCases.link(authState, resolvedProjectId),
146
+ command: "git.disconnect",
147
+ result: {
148
+ ...target,
149
+ repositoryConnection: existingConnection
150
+ },
194
151
  warnings: [],
195
- nextSteps: ["prisma-cli project show", "prisma-cli app deploy"]
152
+ nextSteps: []
196
153
  };
197
154
  }
198
- async function resolveProjectIdForLink(context, authState, projectUseCases) {
199
- if (!authState.workspace) throw projectSelectionRequiredError();
200
- const projects = await projectUseCases.listProjectsForWorkspace(authState.workspace.id);
201
- if (projects.length === 0) throw projectNotFoundError(`No projects are available in workspace "${authState.workspace.name}".`, "Use prisma-cli app deploy to create project context, or switch workspaces and try again.", []);
202
- return (await createSelectPromptPort(context).select({
203
- message: "Select a project",
204
- choices: projects.map((project) => ({
205
- label: `${project.name} (${project.id})`,
206
- value: project
207
- }))
208
- })).id;
209
- }
210
- function projectSelectionRequiredError() {
211
- return usageError("Project link requires a project target in non-interactive mode", "This command cannot prompt for project selection in the current mode.", "Re-run prisma-cli project link in a TTY, or pass a project id explicitly.", ["prisma-cli project list"], "project");
155
+ async function resolveProjectShowInRealMode(context, workspace, explicitProject) {
156
+ const client = await requireComputeAuth(context.runtime.env);
157
+ if (!client) throw authRequiredError();
158
+ return resolveProjectTarget({
159
+ context,
160
+ workspace,
161
+ explicitProject,
162
+ listProjects: () => listRealWorkspaceProjects(client, workspace),
163
+ remember: false
164
+ });
165
+ }
166
+ async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
167
+ return resolveProjectTarget({
168
+ context,
169
+ workspace,
170
+ explicitProject,
171
+ listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
172
+ remember: false
173
+ });
174
+ }
175
+ async function listRealWorkspaceProjects(client, workspace) {
176
+ const { data } = await client.GET("/v1/projects", {});
177
+ return sortProjects((data?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
178
+ id: project.id,
179
+ name: project.name,
180
+ slug: "slug" in project && typeof project.slug === "string" ? project.slug : null,
181
+ workspace: {
182
+ id: project.workspace.id,
183
+ name: project.workspace.name
184
+ }
185
+ })));
186
+ }
187
+ function listFixtureWorkspaceProjects(context, workspace) {
188
+ return sortProjects(context.api.listProjectsForWorkspace(workspace.id).map((project) => ({
189
+ id: project.id,
190
+ name: project.name,
191
+ slug: project.slug,
192
+ workspace
193
+ })));
194
+ }
195
+ async function resolveRepositoryForConnect(context, gitUrl) {
196
+ const remoteUrl = gitUrl ?? await readGitOriginRemote(context.runtime.cwd);
197
+ if (!remoteUrl) throw usageError("Repository connection requires a GitHub repository URL", "No git-url was provided and the local repo does not have an origin remote.", "Pass a GitHub repository URL, or add a GitHub origin remote and rerun prisma-cli git connect.", ["prisma-cli git connect git@github.com:prisma/prisma-cli.git"], "project");
198
+ const repository = parseGitHubRepositoryUrl(remoteUrl);
199
+ if (!repository) throw unsupportedRepositoryProviderError();
200
+ return repository;
201
+ }
202
+ async function resolveInstalledRepository(context, api, workspaceId, repository) {
203
+ const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId), repository);
204
+ if (lookup.match) return lookup.match;
205
+ const installUrl = await createGitHubInstallIntent(api, workspaceId);
206
+ const canWait = canPrompt(context);
207
+ const opened = await openInstallUrlIfInteractive(context, installUrl);
208
+ if (!canWait) {
209
+ if (lookup.inspectableInstallationCount > 0) throw repoNotAccessibleError(repository, installUrl, opened);
210
+ throw repoInstallationRequiredError(repository, installUrl, opened);
211
+ }
212
+ writeInstallWaitStatus(context, opened, installUrl);
213
+ const result = await waitForInstalledRepository(context, api, workspaceId, repository);
214
+ if (result.match) return result.match;
215
+ if (result.inspectableInstallationCount > 0) throw repoNotAccessibleError(repository, installUrl, opened);
216
+ throw repoInstallationRequiredError(repository, installUrl, opened);
217
+ }
218
+ async function findRepositoryInInstallations(api, installations, repository) {
219
+ let inspectableInstallationCount = 0;
220
+ for (const installation of installations) {
221
+ if (installation.provider !== "github" || installation.suspended) continue;
222
+ const matchedRepository = await findRepositoryInInstallationIfAvailable(api, installation.id, repository);
223
+ if (matchedRepository === "unavailable") continue;
224
+ inspectableInstallationCount += 1;
225
+ if (matchedRepository) return {
226
+ match: {
227
+ installation,
228
+ repository: matchedRepository
229
+ },
230
+ inspectableInstallationCount
231
+ };
232
+ }
233
+ return {
234
+ match: null,
235
+ inspectableInstallationCount
236
+ };
237
+ }
238
+ async function waitForInstalledRepository(context, api, workspaceId, repository) {
239
+ const timeoutMs = readPositiveIntegerEnv(context.runtime.env.PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS, GITHUB_INSTALL_POLL_TIMEOUT_MS);
240
+ const intervalMs = readPositiveIntegerEnv(context.runtime.env.PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS, GITHUB_INSTALL_POLL_INTERVAL_MS);
241
+ const deadline = Date.now() + timeoutMs;
242
+ let inspectableInstallationCount = 0;
243
+ while (Date.now() <= deadline) {
244
+ const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId), repository);
245
+ inspectableInstallationCount = lookup.inspectableInstallationCount;
246
+ if (lookup.match) return {
247
+ match: lookup.match,
248
+ inspectableInstallationCount
249
+ };
250
+ const remainingMs = deadline - Date.now();
251
+ if (remainingMs <= 0) break;
252
+ await sleep(Math.min(intervalMs, remainingMs));
253
+ }
254
+ return {
255
+ match: null,
256
+ inspectableInstallationCount
257
+ };
258
+ }
259
+ function readPositiveIntegerEnv(value, fallback) {
260
+ if (value === void 0) return fallback;
261
+ const parsed = Number(value);
262
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
263
+ }
264
+ function writeInstallWaitStatus(context, opened, installUrl) {
265
+ if (context.flags.quiet) return;
266
+ const lines = [renderSummaryLine(context.ui, "info", opened ? "Waiting for GitHub App installation or repository access approval..." : "Waiting for GitHub App installation or repository access approval. Open the install URL in your browser.")];
267
+ if (!opened) lines.push(installUrl);
268
+ context.output.stderr.write(`${lines.join("\n")}\n`);
269
+ }
270
+ function sleep(ms) {
271
+ return new Promise((resolve) => setTimeout(resolve, ms));
272
+ }
273
+ async function listScmInstallations(api, workspaceId) {
274
+ const installations = [];
275
+ let cursor;
276
+ const seenCursors = /* @__PURE__ */ new Set();
277
+ do {
278
+ const { data, error, response } = await api.GET("/v1/scm-installations", { params: { query: {
279
+ workspaceId,
280
+ limit: 100,
281
+ ...cursor ? { cursor } : {}
282
+ } } });
283
+ if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub App installations", response, error);
284
+ installations.push(...data.data);
285
+ cursor = readNextPaginationCursor(data.pagination, seenCursors, "Failed to inspect GitHub App installations", response);
286
+ } while (cursor);
287
+ return installations;
288
+ }
289
+ async function findRepositoryInInstallation(api, installationId, repository) {
290
+ const expectedFullName = repository.fullName.toLowerCase();
291
+ let cursor;
292
+ const seenCursors = /* @__PURE__ */ new Set();
293
+ do {
294
+ const { data, error, response } = await api.GET("/v1/scm-installations/{installationId}/repositories", { params: {
295
+ path: { installationId },
296
+ query: {
297
+ limit: 100,
298
+ ...cursor ? { cursor } : {}
299
+ }
300
+ } });
301
+ if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub repositories", response, error);
302
+ const matchedRepository = data.data.find((candidate) => candidate.fullName.toLowerCase() === expectedFullName);
303
+ if (matchedRepository) return matchedRepository;
304
+ cursor = readNextPaginationCursor(data.pagination, seenCursors, "Failed to inspect GitHub repositories", response);
305
+ } while (cursor);
306
+ return null;
307
+ }
308
+ function readNextPaginationCursor(pagination, seenCursors, summary, response) {
309
+ const nextCursor = pagination.hasMore && pagination.nextCursor ? pagination.nextCursor : void 0;
310
+ if (!nextCursor) return;
311
+ if (seenCursors.has(nextCursor)) throw repoConnectionApiError(summary, response, { error: { message: "Pagination cursor did not advance." } });
312
+ seenCursors.add(nextCursor);
313
+ return nextCursor;
314
+ }
315
+ async function findRepositoryInInstallationIfAvailable(api, installationId, repository) {
316
+ try {
317
+ return await findRepositoryInInstallation(api, installationId, repository);
318
+ } catch (error) {
319
+ if (isUnavailableScmInstallationError(error)) return "unavailable";
320
+ throw error;
321
+ }
322
+ }
323
+ function isUnavailableScmInstallationError(error) {
324
+ if (!(error instanceof CliError) || error.code !== "REPO_CONNECTION_FAILED") return false;
325
+ return error.meta.status === 404 || error.meta.status === 422;
326
+ }
327
+ async function createGitHubInstallIntent(api, workspaceId) {
328
+ const { data, error, response } = await api.POST("/v1/scm-installations/install-intents", { body: {
329
+ provider: "github",
330
+ workspaceId
331
+ } });
332
+ if (error || !data) throw repoConnectionApiError("Failed to create GitHub App installation link", response, error);
333
+ return data.data.installUrl;
334
+ }
335
+ async function openInstallUrlIfInteractive(context, installUrl) {
336
+ if (!canPrompt(context)) return false;
337
+ try {
338
+ await open(installUrl);
339
+ return true;
340
+ } catch {
341
+ return false;
342
+ }
343
+ }
344
+ async function readFirstSourceRepository(api, projectId) {
345
+ const { data, error, response } = await api.GET("/v1/source-repositories", { params: { query: {
346
+ projectId,
347
+ limit: 1
348
+ } } });
349
+ if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub repository connection", response, error);
350
+ return data.data[0] ?? null;
351
+ }
352
+ function createPendingRepositoryConnection(repository) {
353
+ return {
354
+ id: null,
355
+ provider: "github",
356
+ repoId: null,
357
+ repository,
358
+ defaultBranch: null,
359
+ isPrivate: null,
360
+ status: "pending",
361
+ installation: {
362
+ id: null,
363
+ status: "pending"
364
+ },
365
+ automation: {
366
+ branches: false,
367
+ pullRequests: false,
368
+ comments: false
369
+ },
370
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
371
+ updatedAt: null
372
+ };
373
+ }
374
+ function toRepositoryConnection(record) {
375
+ const [owner = "", name = ""] = record.repoFullName.split("/");
376
+ return {
377
+ id: record.id,
378
+ provider: "github",
379
+ repoId: record.repoId,
380
+ repository: {
381
+ owner,
382
+ name,
383
+ fullName: record.repoFullName,
384
+ url: `https://github.com/${record.repoFullName}`
385
+ },
386
+ defaultBranch: record.defaultBranch,
387
+ isPrivate: record.isPrivate,
388
+ status: record.status,
389
+ installation: {
390
+ id: record.installationId,
391
+ status: "connected"
392
+ },
393
+ automation: {
394
+ branches: record.status === "active",
395
+ pullRequests: false,
396
+ comments: false
397
+ },
398
+ connectedAt: record.createdAt,
399
+ updatedAt: record.updatedAt
400
+ };
401
+ }
402
+ function unsupportedRepositoryProviderError() {
403
+ return new CliError({
404
+ code: "REPO_PROVIDER_UNSUPPORTED",
405
+ domain: "project",
406
+ summary: "Repository provider is not supported",
407
+ why: "Repository connection supports GitHub repository URLs only.",
408
+ fix: "Pass a GitHub repository URL such as git@github.com:prisma/prisma-cli.git.",
409
+ exitCode: 2,
410
+ nextSteps: ["prisma-cli git connect git@github.com:owner/repo.git"]
411
+ });
412
+ }
413
+ function repoNotConnectedError() {
414
+ return new CliError({
415
+ code: "REPO_NOT_CONNECTED",
416
+ domain: "project",
417
+ summary: "No GitHub repository connected",
418
+ why: "The resolved project does not have an active GitHub repository connection.",
419
+ fix: "Run prisma-cli git connect before disconnecting.",
420
+ exitCode: 1,
421
+ nextSteps: ["prisma-cli git connect"]
422
+ });
423
+ }
424
+ function repoInstallationRequiredError(repository, installUrl, opened) {
425
+ return new CliError({
426
+ code: "REPO_INSTALLATION_REQUIRED",
427
+ domain: "project",
428
+ summary: "GitHub App installation required",
429
+ why: `The selected workspace does not have a GitHub App installation that can be used to link ${repository.fullName}.`,
430
+ fix: opened ? "Finish installing the GitHub App in the browser, then rerun prisma-cli git connect." : "Open the GitHub App installation URL, approve access, then rerun prisma-cli git connect.",
431
+ meta: {
432
+ repository: repository.fullName,
433
+ installUrl,
434
+ opened
435
+ },
436
+ exitCode: 1,
437
+ nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`]
438
+ });
439
+ }
440
+ function repoNotAccessibleError(repository, installUrl, opened) {
441
+ return new CliError({
442
+ code: "REPO_NOT_ACCESSIBLE",
443
+ domain: "project",
444
+ summary: "GitHub repository is not accessible",
445
+ why: `The GitHub App installations connected to this workspace do not expose ${repository.fullName}.`,
446
+ fix: "Open the GitHub App installation URL, grant access to this repository, then rerun prisma-cli git connect.",
447
+ meta: {
448
+ repository: repository.fullName,
449
+ installUrl,
450
+ opened
451
+ },
452
+ exitCode: 1,
453
+ nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`]
454
+ });
455
+ }
456
+ function repoAlreadyConnectedError(repositoryFullName) {
457
+ return new CliError({
458
+ code: "REPO_ALREADY_CONNECTED",
459
+ domain: "project",
460
+ summary: "Project already has a GitHub repository connected",
461
+ why: `The resolved project is already connected to ${repositoryFullName}.`,
462
+ fix: "Disconnect the existing repository before connecting a different one.",
463
+ meta: { repository: repositoryFullName },
464
+ exitCode: 1,
465
+ nextSteps: ["prisma-cli git disconnect"]
466
+ });
467
+ }
468
+ function repositoryFullNamesMatch(left, right) {
469
+ return left.toLowerCase() === right.toLowerCase();
470
+ }
471
+ function repoConnectionApiError(summary, response, error) {
472
+ const status = response?.status ?? 0;
473
+ const apiCode = error?.error?.code;
474
+ const apiMessage = error?.error?.message;
475
+ const apiHint = error?.error?.hint;
476
+ if (status === 401 || status === 403) return authRequiredError(["prisma-cli auth login"]);
477
+ return new CliError({
478
+ code: "REPO_CONNECTION_FAILED",
479
+ domain: "project",
480
+ summary,
481
+ why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
482
+ fix: apiHint ?? repoConnectionFixForStatus(status),
483
+ meta: {
484
+ status,
485
+ ...apiCode ? { apiCode } : {}
486
+ },
487
+ exitCode: 1,
488
+ nextSteps: ["prisma-cli project show"]
489
+ });
490
+ }
491
+ function repoConnectionFixForStatus(status) {
492
+ if (status === 404) return "Install the GitHub App for this workspace, then rerun prisma-cli git connect.";
493
+ if (status === 409) return "This project or repository is already linked. Disconnect the old link first, then try again.";
494
+ if (status === 422) return "Make sure the GitHub App installation has access to this repository.";
495
+ return "Re-run with --trace for the underlying API response details.";
496
+ }
497
+ function toProjectSummary(project) {
498
+ return {
499
+ id: project.id,
500
+ name: project.name
501
+ };
212
502
  }
213
503
  //#endregion
214
- export { runProjectLink, runProjectList, runProjectShow };
504
+ export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectList, runProjectShow };
@@ -0,0 +1,12 @@
1
+ import { buildVersionResult } from "../lib/version.js";
2
+ //#region src/controllers/version.ts
3
+ async function runVersion(context) {
4
+ return {
5
+ command: "version",
6
+ result: buildVersionResult(context.runtime.env, context.runtime.argv),
7
+ warnings: [],
8
+ nextSteps: []
9
+ };
10
+ }
11
+ //#endregion
12
+ export { runVersion };