@prisma/cli 3.0.0-beta.0 → 3.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +18 -3
  2. package/dist/adapters/git.js +8 -3
  3. package/dist/adapters/local-state.js +12 -4
  4. package/dist/adapters/mock-api.js +81 -2
  5. package/dist/adapters/token-storage.js +63 -22
  6. package/dist/cli.js +17 -2
  7. package/dist/cli2.js +7 -3
  8. package/dist/commands/app/index.js +26 -13
  9. package/dist/commands/branch/index.js +2 -27
  10. package/dist/commands/database/index.js +159 -0
  11. package/dist/commands/env.js +8 -4
  12. package/dist/commands/project/index.js +28 -2
  13. package/dist/controllers/app-env-api.js +54 -0
  14. package/dist/controllers/app-env-file.js +181 -0
  15. package/dist/controllers/app-env.js +284 -132
  16. package/dist/controllers/app.js +493 -344
  17. package/dist/controllers/auth.js +8 -8
  18. package/dist/controllers/branch.js +78 -48
  19. package/dist/controllers/database.js +318 -0
  20. package/dist/controllers/project.js +302 -75
  21. package/dist/lib/app/branch-database-deploy.js +373 -0
  22. package/dist/lib/app/branch-database.js +316 -0
  23. package/dist/lib/app/bun-project.js +12 -5
  24. package/dist/lib/app/deploy-output.js +10 -1
  25. package/dist/lib/app/env-config.js +1 -1
  26. package/dist/lib/app/env-file.js +82 -0
  27. package/dist/lib/app/env-vars.js +28 -2
  28. package/dist/lib/app/local-dev.js +34 -18
  29. package/dist/lib/app/preview-branch-database.js +102 -0
  30. package/dist/lib/app/preview-build-settings.js +385 -0
  31. package/dist/lib/app/preview-build.js +272 -81
  32. package/dist/lib/app/preview-provider.js +163 -54
  33. package/dist/lib/app/production-deploy-gate.js +161 -0
  34. package/dist/lib/auth/auth-ops.js +69 -19
  35. package/dist/lib/auth/guard.js +3 -2
  36. package/dist/lib/auth/login.js +109 -14
  37. package/dist/lib/database/provider.js +167 -0
  38. package/dist/lib/diagnostics.js +15 -0
  39. package/dist/lib/git/local-branch.js +41 -0
  40. package/dist/lib/git/local-status.js +57 -0
  41. package/dist/lib/project/interactive-setup.js +56 -0
  42. package/dist/lib/project/local-pin.js +182 -33
  43. package/dist/lib/project/resolution.js +287 -105
  44. package/dist/lib/project/setup.js +132 -0
  45. package/dist/presenters/app-env.js +149 -14
  46. package/dist/presenters/app.js +170 -20
  47. package/dist/presenters/auth.js +19 -6
  48. package/dist/presenters/branch.js +37 -102
  49. package/dist/presenters/database.js +274 -0
  50. package/dist/presenters/project.js +100 -47
  51. package/dist/presenters/verbose-context.js +64 -0
  52. package/dist/shell/command-arguments.js +6 -0
  53. package/dist/shell/command-meta.js +139 -16
  54. package/dist/shell/command-runner.js +38 -8
  55. package/dist/shell/diagnostics-output.js +63 -0
  56. package/dist/shell/errors.js +14 -1
  57. package/dist/shell/output.js +13 -2
  58. package/dist/shell/runtime.js +3 -3
  59. package/dist/shell/ui.js +23 -1
  60. package/dist/shell/update-check.js +247 -0
  61. package/dist/use-cases/auth.js +15 -4
  62. package/dist/use-cases/branch.js +20 -68
  63. package/dist/use-cases/create-cli-gateways.js +2 -17
  64. package/dist/use-cases/project.js +2 -1
  65. package/package.json +12 -10
@@ -1,63 +1,232 @@
1
- import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
1
+ import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
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 { resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
5
+ import { formatCommandArgument } from "../shell/command-arguments.js";
6
+ import { readLocalResolutionPin } from "../lib/project/local-pin.js";
7
+ import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectResolutionErrorToCliError, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
8
+ import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
9
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
10
+ import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
6
11
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
7
12
  import { requireAuthenticatedAuthState } from "./auth.js";
8
13
  import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
9
14
  import { createProjectUseCases } from "../use-cases/project.js";
10
15
  import open from "open";
16
+ import { matchError } from "better-result";
11
17
  //#region src/controllers/project.ts
12
18
  const GITHUB_INSTALL_POLL_INTERVAL_MS = 2e3;
13
19
  const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
14
20
  function isRealMode(context) {
15
21
  return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
16
22
  }
23
+ async function readProjectListLocalBinding(cwd, workspace, projects, signal) {
24
+ const pinResult = await readLocalResolutionPin(cwd, signal);
25
+ if (pinResult.isErr()) return localPinReadErrorToInvalidLocalBinding(pinResult.error);
26
+ const pin = pinResult.value;
27
+ if (pin.kind === "present") return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId) ? { status: "linked" } : { status: "invalid" };
28
+ return { status: "not-linked" };
29
+ }
30
+ function localPinReadErrorToInvalidLocalBinding(error) {
31
+ return matchError(error, {
32
+ LocalResolutionPinInvalidJsonError: () => ({ status: "invalid" }),
33
+ LocalResolutionPinInvalidShapeError: () => ({ status: "invalid" }),
34
+ LocalResolutionPinReadAbortedError: (error) => {
35
+ throw error;
36
+ },
37
+ UnhandledException: (error) => {
38
+ throw error;
39
+ }
40
+ });
41
+ }
17
42
  async function runProjectList(context) {
18
43
  const authState = await requireAuthenticatedAuthState(context);
19
44
  const workspace = authState.workspace;
20
45
  if (!workspace) throw workspaceRequiredError();
21
46
  if (isRealMode(context)) {
22
- const client = await requireComputeAuth(context.runtime.env);
47
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
23
48
  if (!client) throw authRequiredError();
49
+ const projects = sortProjects(await listRealWorkspaceProjects(client, workspace, context.runtime.signal));
50
+ const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, projects, context.runtime.signal);
51
+ const nextActions = buildProjectListNextActions(localBinding);
24
52
  return {
25
53
  command: "project.list",
26
54
  result: {
27
55
  workspace,
28
- projects: sortProjects(await listRealWorkspaceProjects(client, workspace)).map(toProjectSummary)
56
+ projects: projects.map(toProjectSummary),
57
+ localBinding
29
58
  },
30
59
  warnings: [],
31
- nextSteps: []
60
+ nextSteps: [],
61
+ nextActions
32
62
  };
33
63
  }
64
+ const result = await createProjectUseCases(createCliUseCaseGateways(context)).list(authState);
65
+ const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, result.projects, context.runtime.signal);
66
+ const nextActions = buildProjectListNextActions(localBinding);
34
67
  return {
35
68
  command: "project.list",
36
- result: await createProjectUseCases(createCliUseCaseGateways(context)).list(authState),
69
+ result: {
70
+ ...result,
71
+ localBinding
72
+ },
37
73
  warnings: [],
38
- nextSteps: []
74
+ nextSteps: [],
75
+ nextActions
39
76
  };
40
77
  }
78
+ function buildProjectListNextActions(localBinding) {
79
+ return localBinding?.status === "linked" ? [] : buildProjectSetupNextActions({
80
+ createCommand: "prisma-cli project create <name>",
81
+ reason: localBinding?.status === "invalid" ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands." : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory."
82
+ });
83
+ }
41
84
  async function runProjectShow(context, explicitProject) {
42
85
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
43
86
  if (!workspace) throw workspaceRequiredError();
87
+ const result = isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject);
44
88
  return {
45
89
  command: "project.show",
46
- result: isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject),
90
+ result,
47
91
  warnings: [],
48
- nextSteps: []
92
+ nextSteps: [],
93
+ nextActions: result.project === null ? buildProjectSetupNextActions({
94
+ commandName: "project show",
95
+ suggestedProjectName: result.suggestedProjectName,
96
+ reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project."
97
+ }) : []
98
+ };
99
+ }
100
+ async function runProjectCreate(context, projectName) {
101
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
102
+ if (!workspace) throw workspaceRequiredError();
103
+ if (!isValidProjectSetupName(projectName)) throw projectSetupNameRequiredError("project create");
104
+ if (!isRealMode(context)) throw featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project");
105
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
106
+ if (!client) throw authRequiredError();
107
+ const provider = createPreviewAppProvider(client);
108
+ const name = projectName.trim();
109
+ const created = await provider.createProject({
110
+ name,
111
+ signal: context.runtime.signal
112
+ }).catch((error) => {
113
+ throw projectCreateFailedError(error, name, workspace, {
114
+ nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"],
115
+ permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
116
+ fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
117
+ });
118
+ });
119
+ const bindResult = await bindProjectToDirectory(context, workspace, {
120
+ id: created.id,
121
+ name: created.name
122
+ }, "created");
123
+ if (bindResult.isErr()) throw projectDirectoryBindingErrorToCliError(bindResult.error);
124
+ return {
125
+ command: "project.create",
126
+ result: bindResult.value,
127
+ warnings: [],
128
+ nextSteps: ["prisma-cli app deploy"]
49
129
  };
50
130
  }
131
+ async function runProjectLink(context, projectRef) {
132
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
133
+ if (!workspace) throw workspaceRequiredError();
134
+ let provider = null;
135
+ let projects;
136
+ if (isRealMode(context)) {
137
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
138
+ if (!client) throw authRequiredError();
139
+ provider = createPreviewAppProvider(client);
140
+ projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
141
+ } else projects = listFixtureWorkspaceProjects(context, workspace);
142
+ let result;
143
+ if (projectRef?.trim()) result = await requireProjectDirectoryBinding(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked");
144
+ else if (canPrompt(context) && !context.flags.yes) result = await resolveInteractiveProjectLinkSetup(context, workspace, projects, provider);
145
+ else throw await projectLinkTargetRequiredError(context, projects);
146
+ return {
147
+ command: "project.link",
148
+ result,
149
+ warnings: [],
150
+ nextSteps: ["prisma-cli app deploy"]
151
+ };
152
+ }
153
+ async function resolveInteractiveProjectLinkSetup(context, workspace, projects, provider) {
154
+ const setup = await promptForProjectSetupChoice({
155
+ context,
156
+ projects,
157
+ createProject: (projectName) => {
158
+ if (!provider) throw featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project");
159
+ return createProjectForLinkSetup(provider, projectName, workspace, context.runtime.signal);
160
+ },
161
+ cancel: {
162
+ why: "Project link needs a Project before it can continue.",
163
+ fix: "Choose an existing Project or create a new one, then rerun project link.",
164
+ nextSteps: ["prisma-cli project link <id-or-name>", "prisma-cli project create <name>"]
165
+ }
166
+ });
167
+ return requireProjectDirectoryBinding(context, workspace, setup.project, setup.action);
168
+ }
169
+ async function requireProjectDirectoryBinding(context, workspace, project, action) {
170
+ const bindResult = await bindProjectToDirectory(context, workspace, project, action);
171
+ if (bindResult.isErr()) throw projectDirectoryBindingErrorToCliError(bindResult.error);
172
+ return bindResult.value;
173
+ }
174
+ async function createProjectForLinkSetup(provider, projectName, workspace, signal) {
175
+ const created = await provider.createProject({
176
+ name: projectName,
177
+ signal
178
+ }).catch((error) => {
179
+ throw projectCreateFailedError(error, projectName, workspace, {
180
+ nextSteps: [
181
+ "prisma-cli project list",
182
+ "prisma-cli project link <id-or-name>",
183
+ `prisma-cli project create ${formatCommandArgument(projectName)}`
184
+ ],
185
+ permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
186
+ fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
187
+ });
188
+ });
189
+ return {
190
+ id: created.id,
191
+ name: created.name,
192
+ workspace
193
+ };
194
+ }
195
+ async function projectLinkTargetRequiredError(context, projects) {
196
+ const suggestedName = await inferTargetName(context.runtime.cwd, context.runtime.signal);
197
+ const createCommand = `prisma-cli project create ${formatCommandArgument(suggestedName.name)}`;
198
+ const recoveryCommands = ["prisma-cli project link <id-or-name>", createCommand];
199
+ return new CliError({
200
+ code: "PROJECT_LINK_TARGET_REQUIRED",
201
+ domain: "project",
202
+ summary: "Choose a Project to link this directory",
203
+ why: "This directory is not linked to a Prisma Project. Existing Projects are candidates until the user chooses one, and package or directory names are suggestions only.",
204
+ fix: "Run prisma-cli project link in a TTY to choose from the setup list, pass a Project id or name, or create a new Project.",
205
+ meta: {
206
+ suggestedProjectName: suggestedName.name,
207
+ suggestedProjectNameSource: suggestedName.source,
208
+ candidates: sortProjects(projects).map(toProjectSummary),
209
+ recoveryCommands
210
+ },
211
+ exitCode: 2,
212
+ nextSteps: ["prisma-cli project list", ...recoveryCommands],
213
+ nextActions: buildProjectSetupNextActions({
214
+ suggestedProjectName: suggestedName.name,
215
+ createCommand,
216
+ reason: "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections."
217
+ })
218
+ });
219
+ }
51
220
  async function runGitConnect(context, gitUrl, options = {}) {
52
221
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
53
222
  if (!workspace) throw workspaceRequiredError();
54
223
  if (isRealMode(context)) {
55
- const client = await requireComputeAuth(context.runtime.env);
224
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
56
225
  if (!client) throw authRequiredError();
57
- const target = await resolveProjectShowInRealMode(context, workspace, options.project);
226
+ const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git connect");
58
227
  const repository = await resolveRepositoryForConnect(context, gitUrl);
59
228
  const api = client;
60
- const existing = await readFirstSourceRepository(api, target.project.id);
229
+ const existing = await readFirstSourceRepository(api, target.project.id, context.runtime.signal);
61
230
  if (existing) {
62
231
  const existingConnection = toRepositoryConnection(existing);
63
232
  if (repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) return {
@@ -72,12 +241,15 @@ async function runGitConnect(context, gitUrl, options = {}) {
72
241
  throw repoAlreadyConnectedError(existingConnection.repository.fullName);
73
242
  }
74
243
  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
- } });
244
+ const { data, error, response } = await api.POST("/v1/source-repositories", {
245
+ body: {
246
+ projectId: target.project.id,
247
+ provider: "github",
248
+ providerRepositoryId: resolvedRepository.repository.id,
249
+ installationId: resolvedRepository.installation.id
250
+ },
251
+ signal: context.runtime.signal
252
+ });
81
253
  if (error || !data) throw repoConnectionApiError("Failed to connect GitHub repository", response, error);
82
254
  return {
83
255
  command: "git.connect",
@@ -89,7 +261,7 @@ async function runGitConnect(context, gitUrl, options = {}) {
89
261
  nextSteps: []
90
262
  };
91
263
  }
92
- const target = await resolveProjectShowInFixtureMode(context, workspace, options.project);
264
+ const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git connect");
93
265
  const repository = await resolveRepositoryForConnect(context, gitUrl);
94
266
  const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
95
267
  if (existingConnection) {
@@ -120,13 +292,16 @@ async function runGitDisconnect(context, options = {}) {
120
292
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
121
293
  if (!workspace) throw workspaceRequiredError();
122
294
  if (isRealMode(context)) {
123
- const client = await requireComputeAuth(context.runtime.env);
295
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
124
296
  if (!client) throw authRequiredError();
125
- const target = await resolveProjectShowInRealMode(context, workspace, options.project);
297
+ const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git disconnect");
126
298
  const api = client;
127
- const existing = await readFirstSourceRepository(api, target.project.id);
299
+ const existing = await readFirstSourceRepository(api, target.project.id, context.runtime.signal);
128
300
  if (!existing) throw repoNotConnectedError();
129
- const { error, response } = await api.DELETE("/v1/source-repositories/{id}", { params: { path: { id: existing.id } } });
301
+ const { error, response } = await api.DELETE("/v1/source-repositories/{id}", {
302
+ params: { path: { id: existing.id } },
303
+ signal: context.runtime.signal
304
+ });
130
305
  if (error) throw repoConnectionApiError("Failed to disconnect GitHub repository", response, error);
131
306
  return {
132
307
  command: "git.disconnect",
@@ -138,7 +313,7 @@ async function runGitDisconnect(context, options = {}) {
138
313
  nextSteps: []
139
314
  };
140
315
  }
141
- const target = await resolveProjectShowInFixtureMode(context, workspace, options.project);
316
+ const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git disconnect");
142
317
  const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
143
318
  if (!existingConnection) throw repoNotConnectedError();
144
319
  await context.stateStore.clearRepositoryConnection(target.project.id);
@@ -153,30 +328,59 @@ async function runGitDisconnect(context, options = {}) {
153
328
  };
154
329
  }
155
330
  async function resolveProjectShowInRealMode(context, workspace, explicitProject) {
156
- const client = await requireComputeAuth(context.runtime.env);
331
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
157
332
  if (!client) throw authRequiredError();
158
- return resolveProjectTarget({
333
+ const result = await inspectProjectBinding({
159
334
  context,
160
335
  workspace,
161
336
  explicitProject,
162
- listProjects: () => listRealWorkspaceProjects(client, workspace),
163
- remember: false
337
+ listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
338
+ commandName: "project show"
164
339
  });
340
+ if (result.isErr()) throw projectResolutionErrorToCliError(result.error);
341
+ return result.value;
342
+ }
343
+ async function resolveRequiredProjectInRealMode(context, workspace, explicitProject, commandName) {
344
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
345
+ if (!client) throw authRequiredError();
346
+ const result = await resolveProjectTarget({
347
+ context,
348
+ workspace,
349
+ explicitProject,
350
+ listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
351
+ commandName
352
+ });
353
+ if (result.isErr()) throw projectResolutionErrorToCliError(result.error);
354
+ return result.value;
165
355
  }
166
356
  async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
167
- return resolveProjectTarget({
357
+ const result = await inspectProjectBinding({
168
358
  context,
169
359
  workspace,
170
360
  explicitProject,
171
361
  listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
172
- remember: false
362
+ commandName: "project show"
173
363
  });
364
+ if (result.isErr()) throw projectResolutionErrorToCliError(result.error);
365
+ return result.value;
174
366
  }
175
- async function listRealWorkspaceProjects(client, workspace) {
176
- const { data } = await client.GET("/v1/projects", {});
367
+ async function resolveRequiredProjectInFixtureMode(context, workspace, explicitProject, commandName) {
368
+ const result = await resolveProjectTarget({
369
+ context,
370
+ workspace,
371
+ explicitProject,
372
+ listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
373
+ commandName
374
+ });
375
+ if (result.isErr()) throw projectResolutionErrorToCliError(result.error);
376
+ return result.value;
377
+ }
378
+ async function listRealWorkspaceProjects(client, workspace, signal) {
379
+ const { data } = await client.GET("/v1/projects", { signal });
177
380
  return sortProjects((data?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
178
381
  id: project.id,
179
382
  name: project.name,
383
+ ..."url" in project && typeof project.url === "string" ? { url: project.url } : {},
180
384
  slug: "slug" in project && typeof project.slug === "string" ? project.slug : null,
181
385
  workspace: {
182
386
  id: project.workspace.id,
@@ -188,21 +392,22 @@ function listFixtureWorkspaceProjects(context, workspace) {
188
392
  return sortProjects(context.api.listProjectsForWorkspace(workspace.id).map((project) => ({
189
393
  id: project.id,
190
394
  name: project.name,
395
+ ...project.url ? { url: project.url } : {},
191
396
  slug: project.slug,
192
397
  workspace
193
398
  })));
194
399
  }
195
400
  async function resolveRepositoryForConnect(context, gitUrl) {
196
- const remoteUrl = gitUrl ?? await readGitOriginRemote(context.runtime.cwd);
401
+ const remoteUrl = gitUrl ?? await readGitOriginRemote(context.runtime.cwd, context.runtime.signal);
197
402
  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
403
  const repository = parseGitHubRepositoryUrl(remoteUrl);
199
404
  if (!repository) throw unsupportedRepositoryProviderError();
200
405
  return repository;
201
406
  }
202
407
  async function resolveInstalledRepository(context, api, workspaceId, repository) {
203
- const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId), repository);
408
+ const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId, context.runtime.signal), repository, context.runtime.signal);
204
409
  if (lookup.match) return lookup.match;
205
- const installUrl = await createGitHubInstallIntent(api, workspaceId);
410
+ const installUrl = await createGitHubInstallIntent(api, workspaceId, context.runtime.signal);
206
411
  const canWait = canPrompt(context);
207
412
  const opened = await openInstallUrlIfInteractive(context, installUrl);
208
413
  if (!canWait) {
@@ -215,11 +420,11 @@ async function resolveInstalledRepository(context, api, workspaceId, repository)
215
420
  if (result.inspectableInstallationCount > 0) throw repoNotAccessibleError(repository, installUrl, opened);
216
421
  throw repoInstallationRequiredError(repository, installUrl, opened);
217
422
  }
218
- async function findRepositoryInInstallations(api, installations, repository) {
423
+ async function findRepositoryInInstallations(api, installations, repository, signal) {
219
424
  let inspectableInstallationCount = 0;
220
425
  for (const installation of installations) {
221
426
  if (installation.provider !== "github" || installation.suspended) continue;
222
- const matchedRepository = await findRepositoryInInstallationIfAvailable(api, installation.id, repository);
427
+ const matchedRepository = await findRepositoryInInstallationIfAvailable(api, installation.id, repository, signal);
223
428
  if (matchedRepository === "unavailable") continue;
224
429
  inspectableInstallationCount += 1;
225
430
  if (matchedRepository) return {
@@ -241,7 +446,8 @@ async function waitForInstalledRepository(context, api, workspaceId, repository)
241
446
  const deadline = Date.now() + timeoutMs;
242
447
  let inspectableInstallationCount = 0;
243
448
  while (Date.now() <= deadline) {
244
- const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId), repository);
449
+ context.runtime.signal.throwIfAborted();
450
+ const lookup = await findRepositoryInInstallations(api, await listScmInstallations(api, workspaceId, context.runtime.signal), repository, context.runtime.signal);
245
451
  inspectableInstallationCount = lookup.inspectableInstallationCount;
246
452
  if (lookup.match) return {
247
453
  match: lookup.match,
@@ -249,7 +455,7 @@ async function waitForInstalledRepository(context, api, workspaceId, repository)
249
455
  };
250
456
  const remainingMs = deadline - Date.now();
251
457
  if (remainingMs <= 0) break;
252
- await sleep(Math.min(intervalMs, remainingMs));
458
+ await sleep(Math.min(intervalMs, remainingMs), context.runtime.signal);
253
459
  }
254
460
  return {
255
461
  match: null,
@@ -267,37 +473,54 @@ function writeInstallWaitStatus(context, opened, installUrl) {
267
473
  if (!opened) lines.push(installUrl);
268
474
  context.output.stderr.write(`${lines.join("\n")}\n`);
269
475
  }
270
- function sleep(ms) {
271
- return new Promise((resolve) => setTimeout(resolve, ms));
476
+ function sleep(ms, signal) {
477
+ signal.throwIfAborted();
478
+ return new Promise((resolve, reject) => {
479
+ const onAbort = () => {
480
+ clearTimeout(timeout);
481
+ reject(signal.reason);
482
+ };
483
+ const timeout = setTimeout(() => {
484
+ signal.removeEventListener("abort", onAbort);
485
+ resolve();
486
+ }, ms);
487
+ signal.addEventListener("abort", onAbort, { once: true });
488
+ });
272
489
  }
273
- async function listScmInstallations(api, workspaceId) {
490
+ async function listScmInstallations(api, workspaceId, signal) {
274
491
  const installations = [];
275
492
  let cursor;
276
493
  const seenCursors = /* @__PURE__ */ new Set();
277
494
  do {
278
- const { data, error, response } = await api.GET("/v1/scm-installations", { params: { query: {
279
- workspaceId,
280
- limit: 100,
281
- ...cursor ? { cursor } : {}
282
- } } });
495
+ const { data, error, response } = await api.GET("/v1/scm-installations", {
496
+ params: { query: {
497
+ workspaceId,
498
+ limit: 100,
499
+ ...cursor ? { cursor } : {}
500
+ } },
501
+ signal
502
+ });
283
503
  if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub App installations", response, error);
284
504
  installations.push(...data.data);
285
505
  cursor = readNextPaginationCursor(data.pagination, seenCursors, "Failed to inspect GitHub App installations", response);
286
506
  } while (cursor);
287
507
  return installations;
288
508
  }
289
- async function findRepositoryInInstallation(api, installationId, repository) {
509
+ async function findRepositoryInInstallation(api, installationId, repository, signal) {
290
510
  const expectedFullName = repository.fullName.toLowerCase();
291
511
  let cursor;
292
512
  const seenCursors = /* @__PURE__ */ new Set();
293
513
  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
- } });
514
+ const { data, error, response } = await api.GET("/v1/scm-installations/{installationId}/repositories", {
515
+ params: {
516
+ path: { installationId },
517
+ query: {
518
+ limit: 100,
519
+ ...cursor ? { cursor } : {}
520
+ }
521
+ },
522
+ signal
523
+ });
301
524
  if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub repositories", response, error);
302
525
  const matchedRepository = data.data.find((candidate) => candidate.fullName.toLowerCase() === expectedFullName);
303
526
  if (matchedRepository) return matchedRepository;
@@ -312,10 +535,11 @@ function readNextPaginationCursor(pagination, seenCursors, summary, response) {
312
535
  seenCursors.add(nextCursor);
313
536
  return nextCursor;
314
537
  }
315
- async function findRepositoryInInstallationIfAvailable(api, installationId, repository) {
538
+ async function findRepositoryInInstallationIfAvailable(api, installationId, repository, signal) {
316
539
  try {
317
- return await findRepositoryInInstallation(api, installationId, repository);
540
+ return await findRepositoryInInstallation(api, installationId, repository, signal);
318
541
  } catch (error) {
542
+ if (signal.aborted) throw error;
319
543
  if (isUnavailableScmInstallationError(error)) return "unavailable";
320
544
  throw error;
321
545
  }
@@ -324,28 +548,37 @@ function isUnavailableScmInstallationError(error) {
324
548
  if (!(error instanceof CliError) || error.code !== "REPO_CONNECTION_FAILED") return false;
325
549
  return error.meta.status === 404 || error.meta.status === 422;
326
550
  }
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
- } });
551
+ async function createGitHubInstallIntent(api, workspaceId, signal) {
552
+ const { data, error, response } = await api.POST("/v1/scm-installations/install-intents", {
553
+ body: {
554
+ provider: "github",
555
+ workspaceId
556
+ },
557
+ signal
558
+ });
332
559
  if (error || !data) throw repoConnectionApiError("Failed to create GitHub App installation link", response, error);
333
560
  return data.data.installUrl;
334
561
  }
335
562
  async function openInstallUrlIfInteractive(context, installUrl) {
336
563
  if (!canPrompt(context)) return false;
337
564
  try {
565
+ context.runtime.signal.throwIfAborted();
338
566
  await open(installUrl);
567
+ context.runtime.signal.throwIfAborted();
339
568
  return true;
340
- } catch {
569
+ } catch (error) {
570
+ if (context.runtime.signal.aborted) throw error;
341
571
  return false;
342
572
  }
343
573
  }
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
- } } });
574
+ async function readFirstSourceRepository(api, projectId, signal) {
575
+ const { data, error, response } = await api.GET("/v1/source-repositories", {
576
+ params: { query: {
577
+ projectId,
578
+ limit: 1
579
+ } },
580
+ signal
581
+ });
349
582
  if (error || !data) throw repoConnectionApiError("Failed to inspect GitHub repository connection", response, error);
350
583
  return data.data[0] ?? null;
351
584
  }
@@ -494,11 +727,5 @@ function repoConnectionFixForStatus(status) {
494
727
  if (status === 422) return "Make sure the GitHub App installation has access to this repository.";
495
728
  return "Re-run with --trace for the underlying API response details.";
496
729
  }
497
- function toProjectSummary(project) {
498
- return {
499
- id: project.id,
500
- name: project.name
501
- };
502
- }
503
730
  //#endregion
504
- export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectList, runProjectShow };
731
+ export { listFixtureWorkspaceProjects, listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectCreate, runProjectLink, runProjectList, runProjectShow };