@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,76 +1,100 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { canPrompt } from "../../shell/runtime.js";
2
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
3
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import path from "node:path";
6
+ import { Result, TaggedError, matchError } from "better-result";
5
7
  //#region src/lib/project/resolution.ts
6
- async function resolveProjectTarget(options) {
7
- const projects = await options.listProjects();
8
- const inferredName = await inferTargetName(options.context.runtime.cwd);
9
- if (options.explicitProject) return rememberIfRequested(options, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
10
- targetName: options.explicitProject,
11
- targetNameSource: "explicit"
12
- });
13
- const platformMapping = await resolveDurablePlatformMapping();
14
- if (platformMapping) return rememberIfRequested(options, platformMapping, "platform-mapping");
15
- let staleRemembered = false;
16
- if (!options.allowCreate) {
17
- const rememberedResult = await resolveRememberedProject(options, projects);
18
- if (rememberedResult.target) return rememberedResult.target;
19
- staleRemembered = rememberedResult.stale;
8
+ var ProjectNotFoundError = class extends TaggedError("ProjectNotFoundError")() {
9
+ constructor(projectRef, workspace) {
10
+ super({
11
+ message: `Project "${projectRef}" was not found in workspace "${workspace.name}".`,
12
+ projectRef,
13
+ workspace
14
+ });
20
15
  }
21
- const packageName = inferredName.source === "package-name" ? inferredName.name : null;
22
- if (packageName) {
23
- const matches = projects.filter((project) => projectMatchesPackageName(project, packageName));
24
- if (matches.length === 1) return rememberIfRequested(options, matches[0], "package-name", {
25
- targetName: packageName,
26
- targetNameSource: "package-name"
16
+ };
17
+ var ProjectAmbiguousError = class extends TaggedError("ProjectAmbiguousError")() {
18
+ constructor(projectRef, matches) {
19
+ super({
20
+ message: projectRef ? `Multiple projects matched "${projectRef}".` : "Multiple projects matched the current directory context.",
21
+ projectRef,
22
+ matches
27
23
  });
28
- if (matches.length > 1) return resolveAmbiguousProject(options, matches, packageName, "package-name");
29
24
  }
30
- if (options.allowCreate && options.createProject) {
31
- if (inferredName.name) {
32
- const existing = projects.filter((project) => projectMatchesPackageName(project, inferredName.name));
33
- if (existing.length === 1) return rememberIfRequested(options, existing[0], inferredName.source, {
34
- targetName: inferredName.name,
35
- targetNameSource: inferredName.source
36
- });
37
- if (existing.length > 1) return resolveAmbiguousProject(options, existing, inferredName.name, inferredName.source);
38
- return rememberIfRequested(options, await options.createProject(inferredName.name), "created", {
39
- targetName: inferredName.name,
40
- targetNameSource: inferredName.source
41
- });
42
- }
25
+ };
26
+ var LocalStateStaleError = class extends TaggedError("LocalStateStaleError")() {
27
+ constructor() {
28
+ super({
29
+ message: `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.`,
30
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
31
+ });
43
32
  }
44
- if (options.prompt && canPrompt(options.context) && projects.length > 0) return rememberIfRequested(options, await options.prompt.select({
45
- message: "Select a project",
46
- choices: sortProjects(projects).map((project) => ({
47
- label: `${project.name} (${project.id})`,
48
- value: project
49
- }))
50
- }), "prompt");
51
- if (staleRemembered && projects.length > 1) throw localStateStaleError();
52
- throw projectUnresolvedError();
53
- }
54
- async function resolveRememberedProject(options, projects) {
55
- const remembered = await options.context.stateStore.readRememberedProject(options.workspace.id);
56
- if (!remembered) return {
57
- target: null,
58
- stale: false
59
- };
60
- const matched = projects.find((project) => project.id === remembered.id);
61
- if (!matched) return {
62
- target: null,
63
- stale: true
64
- };
65
- return {
66
- target: await rememberIfRequested(options, matched, "remembered-local", {
67
- targetName: remembered.name,
68
- targetNameSource: "remembered-local"
69
- }),
70
- stale: false
71
- };
33
+ };
34
+ var LocalProjectWorkspaceMismatchError = class extends TaggedError("LocalProjectWorkspaceMismatchError")() {
35
+ constructor(options) {
36
+ super({
37
+ message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} links this directory to project ${options.pinnedProjectId} in workspace ${options.pinnedWorkspaceId}, but the active workspace is "${options.activeWorkspace.name}" (${options.activeWorkspace.id}).`,
38
+ pinnedWorkspaceId: options.pinnedWorkspaceId,
39
+ pinnedProjectId: options.pinnedProjectId,
40
+ activeWorkspace: options.activeWorkspace
41
+ });
42
+ }
43
+ };
44
+ var ProjectSetupRequiredError = class extends TaggedError("ProjectSetupRequiredError")() {
45
+ constructor(options) {
46
+ const commandLabel = options.commandName ? `prisma-cli ${options.commandName}` : "this command";
47
+ super({
48
+ message: `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.`,
49
+ commandName: options.commandName,
50
+ suggestion: options.suggestion
51
+ });
52
+ }
53
+ };
54
+ async function resolveProjectTarget(options) {
55
+ return Result.gen(async function* () {
56
+ const localPin = yield* Result.await(readImplicitLocalPin(options, { allowEnvProjectId: true }));
57
+ const projects = await options.listProjects();
58
+ const target = yield* Result.await(resolveBoundProjectTarget(options, projects, {
59
+ allowEnvProjectId: true,
60
+ localPin
61
+ }));
62
+ if (target) return Result.ok(target);
63
+ return Result.err(await projectSetupRequiredError({
64
+ cwd: options.context.runtime.cwd,
65
+ projects,
66
+ commandName: options.commandName,
67
+ signal: options.context.runtime.signal
68
+ }));
69
+ });
70
+ }
71
+ async function inspectProjectBinding(options) {
72
+ return Result.gen(async function* () {
73
+ const localPin = yield* Result.await(readImplicitLocalPin(options, { allowEnvProjectId: false }));
74
+ const projects = await options.listProjects();
75
+ const target = yield* Result.await(resolveBoundProjectTarget(options, projects, {
76
+ allowEnvProjectId: false,
77
+ localPin
78
+ }));
79
+ if (target) return Result.ok(target);
80
+ return Result.ok({
81
+ workspace: options.workspace,
82
+ project: null,
83
+ localBinding: { status: "not-linked" },
84
+ resolution: { projectSource: "unbound" },
85
+ ...await buildProjectSetupSuggestion({
86
+ cwd: options.context.runtime.cwd,
87
+ projects,
88
+ commandName: options.commandName ?? "project show",
89
+ signal: options.context.runtime.signal
90
+ })
91
+ });
92
+ });
72
93
  }
73
94
  function projectNotFoundError(projectRef, workspace) {
95
+ return projectResolutionErrorToCliError(new ProjectNotFoundError(projectRef, workspace));
96
+ }
97
+ function projectNotFoundCliError(projectRef, workspace) {
74
98
  return new CliError({
75
99
  code: "PROJECT_NOT_FOUND",
76
100
  domain: "project",
@@ -82,6 +106,9 @@ function projectNotFoundError(projectRef, workspace) {
82
106
  });
83
107
  }
84
108
  function projectAmbiguousError(projectRef, matches) {
109
+ return projectResolutionErrorToCliError(new ProjectAmbiguousError(projectRef, matches));
110
+ }
111
+ function projectAmbiguousCliError(projectRef, matches) {
85
112
  const firstMatch = matches[0];
86
113
  const nextSteps = ["prisma-cli project list"];
87
114
  if (firstMatch) nextSteps.push(`prisma-cli app deploy --project ${firstMatch.id}`);
@@ -99,31 +126,139 @@ function projectAmbiguousError(projectRef, matches) {
99
126
  nextSteps
100
127
  });
101
128
  }
102
- function projectUnresolvedError() {
129
+ function localStateStaleCliError() {
103
130
  return new CliError({
104
- code: "PROJECT_UNRESOLVED",
131
+ code: "LOCAL_STATE_STALE",
105
132
  domain: "project",
106
- summary: "No project is resolved for this directory",
107
- why: "No project could be resolved from explicit input, platform mappings, remembered local context, or package metadata.",
108
- fix: "Pass --project <id-or-name> on the command that needs a project, or add a package.json name that matches an accessible project.",
133
+ summary: "Local project binding is stale",
134
+ why: `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.`,
135
+ fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}, then choose a Project explicitly.`,
136
+ meta: { pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH },
109
137
  exitCode: 1,
110
- nextSteps: ["prisma-cli project list", "prisma-cli project show --project <id-or-name>"]
138
+ nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
111
139
  });
112
140
  }
113
- function localStateStaleError() {
141
+ function localProjectWorkspaceMismatchCliError(options) {
114
142
  return new CliError({
115
- code: "LOCAL_STATE_STALE",
143
+ code: "LOCAL_PROJECT_WORKSPACE_MISMATCH",
116
144
  domain: "project",
117
- summary: "Remembered project context is stale",
118
- why: "The remembered project is no longer available in the selected workspace, and automatic resolution would be ambiguous.",
119
- fix: "Pass --project <id-or-name> to choose the project explicitly.",
145
+ summary: "Project link uses another workspace",
146
+ why: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} links this directory to project ${options.pinnedProjectId} in workspace ${options.pinnedWorkspaceId}, but your current CLI session is workspace "${options.activeWorkspace.name}" (${options.activeWorkspace.id}).`,
147
+ fix: "Sign in to the linked workspace, or relink this directory to a project in the current workspace.",
148
+ meta: {
149
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
150
+ pinnedWorkspaceId: options.pinnedWorkspaceId,
151
+ pinnedProjectId: options.pinnedProjectId,
152
+ activeWorkspaceId: options.activeWorkspace.id,
153
+ activeWorkspaceName: options.activeWorkspace.name
154
+ },
120
155
  exitCode: 1,
121
- nextSteps: ["prisma-cli project list"]
156
+ nextSteps: [
157
+ "prisma-cli auth login",
158
+ "prisma-cli project list",
159
+ "prisma-cli project link <id-or-name>"
160
+ ]
161
+ });
162
+ }
163
+ /**
164
+ * Converts expected project-resolution variants to command-boundary CliErrors.
165
+ * `LocalResolutionPinReadAbortedError` and `UnhandledException` intentionally
166
+ * propagate as exceptions; callers such as `resolveProjectShowInRealMode`
167
+ * throw this helper's result, so passthrough variants should keep bubbling.
168
+ */
169
+ function projectResolutionErrorToCliError(error) {
170
+ return matchError(error, {
171
+ ProjectNotFoundError: (error) => projectNotFoundCliError(error.projectRef, error.workspace),
172
+ ProjectAmbiguousError: (error) => projectAmbiguousCliError(error.projectRef, error.matches),
173
+ ProjectSetupRequiredError: (error) => projectSetupRequiredCliError(error),
174
+ LocalStateStaleError: () => localStateStaleCliError(),
175
+ LocalProjectWorkspaceMismatchError: (error) => localProjectWorkspaceMismatchCliError({
176
+ pinnedWorkspaceId: error.pinnedWorkspaceId,
177
+ pinnedProjectId: error.pinnedProjectId,
178
+ activeWorkspace: error.activeWorkspace
179
+ }),
180
+ LocalResolutionPinReadAbortedError: (error) => {
181
+ throw error;
182
+ },
183
+ UnhandledException: (error) => {
184
+ throw error;
185
+ }
186
+ });
187
+ }
188
+ async function buildProjectSetupSuggestion(options) {
189
+ const suggestedName = await inferTargetName(options.cwd, options.signal);
190
+ const candidates = sortProjects(options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name))).map(toProjectSummary);
191
+ return {
192
+ suggestedProjectName: suggestedName.name,
193
+ suggestedProjectNameSource: suggestedName.source,
194
+ candidates,
195
+ recoveryCommands: buildProjectRecoveryCommands(options.commandName)
196
+ };
197
+ }
198
+ async function projectSetupRequiredError(options) {
199
+ const suggestion = await buildProjectSetupSuggestion(options);
200
+ return new ProjectSetupRequiredError({
201
+ commandName: options.commandName,
202
+ suggestion
122
203
  });
123
204
  }
124
- async function readPackageName(cwd) {
205
+ function projectSetupRequiredCliError(error) {
206
+ const suggestion = error.suggestion;
207
+ return new CliError({
208
+ code: "PROJECT_SETUP_REQUIRED",
209
+ domain: "project",
210
+ summary: "Choose a Project before running this command",
211
+ why: error.message,
212
+ fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
213
+ meta: { ...suggestion },
214
+ exitCode: 1,
215
+ nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
216
+ nextActions: buildProjectSetupNextActions({
217
+ commandName: error.commandName,
218
+ suggestedProjectName: suggestion.suggestedProjectName
219
+ })
220
+ });
221
+ }
222
+ function buildProjectSetupNextActions(options = {}) {
223
+ const recoveryCommands = buildProjectRecoveryCommands(options.commandName);
224
+ const linkCommand = recoveryCommands[0] ?? "prisma-cli project link <id-or-name>";
225
+ const retryCommand = recoveryCommands[1];
226
+ const actions = [{
227
+ kind: "user-choice",
228
+ journey: "project-setup",
229
+ label: "Ask the user whether to link an existing Project or create a new one",
230
+ commands: ["prisma-cli project list", ...recoveryCommands],
231
+ reason: options.reason ?? "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection."
232
+ }, {
233
+ kind: "run-command",
234
+ journey: "project-setup",
235
+ label: "Link the chosen Project",
236
+ command: linkCommand,
237
+ reason: "Linking writes the durable local Project binding for this directory."
238
+ }];
239
+ const createCommand = options.createCommand ?? (options.suggestedProjectName ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` : void 0);
240
+ if (createCommand) actions.push({
241
+ kind: "run-command",
242
+ journey: "project-setup",
243
+ label: "Create and link a new Project",
244
+ command: createCommand,
245
+ reason: "Use this when the user wants a new Prisma Project instead of an existing one."
246
+ });
247
+ if (options.commandName) actions.push({
248
+ kind: "run-command",
249
+ journey: "recover",
250
+ label: "Retry with an explicit Project",
251
+ command: retryCommand ?? `prisma-cli ${options.commandName} --project <id-or-name>`
252
+ });
253
+ return actions;
254
+ }
255
+ async function readPackageName(cwd, signal) {
256
+ signal?.throwIfAborted();
125
257
  try {
126
- const raw = await readFile(path.join(cwd, "package.json"), "utf8");
258
+ const raw = await readFile(path.join(cwd, "package.json"), {
259
+ encoding: "utf8",
260
+ signal
261
+ });
127
262
  const parsed = JSON.parse(raw);
128
263
  if (!parsed || typeof parsed !== "object") return null;
129
264
  const packageName = "name" in parsed ? parsed.name : null;
@@ -134,8 +269,8 @@ async function readPackageName(cwd) {
134
269
  throw error;
135
270
  }
136
271
  }
137
- async function inferTargetName(cwd) {
138
- const packageName = await readPackageName(cwd);
272
+ async function inferTargetName(cwd, signal) {
273
+ const packageName = await readPackageName(cwd, signal);
139
274
  if (packageName && isValidInferredTargetName(packageName)) return {
140
275
  name: packageName,
141
276
  source: "package-name"
@@ -153,37 +288,78 @@ function sortProjects(projects) {
153
288
  }
154
289
  function resolveExplicitProject(projectRef, projects, workspace) {
155
290
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
156
- if (matches.length === 1) return matches[0];
157
- if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
158
- throw projectNotFoundError(projectRef, workspace);
159
- }
160
- function resolveAmbiguousProject(options, matches, projectRef, targetNameSource) {
161
- if (options.prompt && canPrompt(options.context)) return options.prompt.select({
162
- message: "Select a project",
163
- choices: sortProjects(matches).map((project) => ({
164
- label: `${project.name} (${project.id})`,
165
- value: project
166
- }))
167
- }).then((selected) => rememberIfRequested(options, selected, "prompt", {
168
- targetName: projectRef,
169
- targetNameSource
170
- }));
171
- throw projectAmbiguousError(projectRef, matches);
291
+ if (matches.length === 1) return Result.ok(matches[0]);
292
+ if (matches.length > 1) return Result.err(new ProjectAmbiguousError(projectRef, matches));
293
+ return Result.err(new ProjectNotFoundError(projectRef, workspace));
172
294
  }
173
- function projectMatchesPackageName(project, packageName) {
174
- return project.id === packageName || project.name === packageName || project.slug === packageName;
295
+ function projectMatchesSuggestedName(project, suggestedName) {
296
+ return project.id === suggestedName || project.name === suggestedName || project.slug === suggestedName;
175
297
  }
176
298
  async function resolveDurablePlatformMapping() {
177
299
  return null;
178
300
  }
179
- async function rememberIfRequested(options, project, projectSource, resolutionDetails) {
180
- if (options.remember) await options.context.stateStore.setRememberedProject({
181
- id: project.id,
182
- name: project.name,
183
- workspaceId: options.workspace.id
301
+ async function resolveBoundProjectTarget(options, projects, settings) {
302
+ if (options.explicitProject) {
303
+ const projectResult = resolveExplicitProject(options.explicitProject, projects, options.workspace);
304
+ if (projectResult.isErr()) return Result.err(projectResult.error);
305
+ return Result.ok(resolvedTarget(options.workspace, projectResult.value, "explicit", {
306
+ targetName: options.explicitProject,
307
+ targetNameSource: "explicit"
308
+ }));
309
+ }
310
+ if (settings.allowEnvProjectId && options.envProjectId) {
311
+ const project = projects.find((candidate) => candidate.id === options.envProjectId);
312
+ if (!project) return Result.err(new ProjectNotFoundError(options.envProjectId, options.workspace));
313
+ return Result.ok(resolvedTarget(options.workspace, project, "env", {
314
+ targetName: options.envProjectId,
315
+ targetNameSource: "env"
316
+ }));
317
+ }
318
+ const localPin = settings.localPin;
319
+ if (!localPin) return Result.ok(null);
320
+ if (localPin.kind === "present") {
321
+ if (localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({
322
+ pinnedWorkspaceId: localPin.pin.workspaceId,
323
+ pinnedProjectId: localPin.pin.projectId,
324
+ activeWorkspace: options.workspace
325
+ }));
326
+ const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
327
+ if (!project) return Result.err(new LocalStateStaleError());
328
+ return Result.ok(resolvedTarget(options.workspace, project, "local-pin", {
329
+ targetName: project.name,
330
+ targetNameSource: "local-pin"
331
+ }));
332
+ }
333
+ const platformMapping = await resolveDurablePlatformMapping();
334
+ if (platformMapping && platformMapping.workspace.id === options.workspace.id) return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
335
+ targetName: platformMapping.name,
336
+ targetNameSource: "platform-mapping"
337
+ }));
338
+ return Result.ok(null);
339
+ }
340
+ async function readImplicitLocalPin(options, settings) {
341
+ if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return Result.ok(null);
342
+ const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
343
+ if (localPinResult.isErr()) return Result.err(localPinReadErrorToProjectError(localPinResult.error));
344
+ const localPin = localPinResult.value;
345
+ if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({
346
+ pinnedWorkspaceId: localPin.pin.workspaceId,
347
+ pinnedProjectId: localPin.pin.projectId,
348
+ activeWorkspace: options.workspace
349
+ }));
350
+ return Result.ok(localPin);
351
+ }
352
+ function localPinReadErrorToProjectError(error) {
353
+ return matchError(error, {
354
+ LocalResolutionPinInvalidJsonError: () => new LocalStateStaleError(),
355
+ LocalResolutionPinInvalidShapeError: () => new LocalStateStaleError(),
356
+ LocalResolutionPinReadAbortedError: (error) => error,
357
+ UnhandledException: (error) => error
184
358
  });
359
+ }
360
+ function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
185
361
  return {
186
- workspace: options.workspace,
362
+ workspace,
187
363
  project: toProjectSummary(project),
188
364
  resolution: {
189
365
  projectSource,
@@ -191,11 +367,17 @@ async function rememberIfRequested(options, project, projectSource, resolutionDe
191
367
  }
192
368
  };
193
369
  }
370
+ function buildProjectRecoveryCommands(commandName) {
371
+ const commands = ["prisma-cli project link <id-or-name>"];
372
+ if (commandName) commands.push(`prisma-cli ${commandName} --project <id-or-name>`);
373
+ return commands;
374
+ }
194
375
  function toProjectSummary(project) {
195
376
  return {
196
377
  id: project.id,
197
- name: project.name
378
+ name: project.name,
379
+ ...project.url ? { url: project.url } : {}
198
380
  };
199
381
  }
200
382
  //#endregion
201
- export { inferTargetName, projectNotFoundError, resolveProjectTarget, sortProjects };
383
+ export { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, projectResolutionErrorToCliError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
@@ -0,0 +1,132 @@
1
+ import { CliError, usageError } from "../../shell/errors.js";
2
+ import "../../shell/command-arguments.js";
3
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
4
+ import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
5
+ import { Result, matchError } from "better-result";
6
+ //#region src/lib/project/setup.ts
7
+ function isValidProjectSetupName(projectName) {
8
+ return projectName.trim().length > 0;
9
+ }
10
+ function validateProjectSetupNameText(value, fallback) {
11
+ if ((value?.trim() || fallback).trim().length > 0) return;
12
+ return "Enter a Project name.";
13
+ }
14
+ function resolveProjectForSetup(projectRef, projects, workspace) {
15
+ const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
16
+ if (matches.length === 1) return matches[0];
17
+ if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
18
+ throw projectNotFoundError(projectRef, workspace);
19
+ }
20
+ async function bindProjectToDirectory(context, workspace, project, action) {
21
+ return Result.gen(async function* () {
22
+ yield* Result.await(writeLocalResolutionPin(context.runtime.cwd, {
23
+ workspaceId: workspace.id,
24
+ projectId: project.id
25
+ }, context.runtime.signal));
26
+ yield* Result.await(ensureLocalResolutionPinGitignore(context.runtime.cwd, context.runtime.signal));
27
+ return Result.ok({
28
+ workspace,
29
+ project,
30
+ directory: formatSetupDirectory(context.runtime.cwd),
31
+ localPin: {
32
+ path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
33
+ written: true
34
+ },
35
+ action
36
+ });
37
+ });
38
+ }
39
+ function projectDirectoryBindingErrorToCliError(error) {
40
+ return matchError(error, {
41
+ LocalResolutionPinSerializationError: (error) => {
42
+ throw error;
43
+ },
44
+ LocalResolutionPinWriteAbortedError: (error) => {
45
+ throw error;
46
+ },
47
+ LocalResolutionPinWriteFailedError: (error) => localStateWriteFailedError(error, {
48
+ why: `The CLI could not write ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`,
49
+ meta: {
50
+ pinPath: error.pinPath,
51
+ operation: error.operation
52
+ }
53
+ }),
54
+ LocalResolutionPinGitignoreUpdateAbortedError: (error) => {
55
+ throw error;
56
+ },
57
+ LocalResolutionPinGitignoreUpdateFailedError: (error) => localStateWriteFailedError(error, {
58
+ why: "The CLI could not update .gitignore to keep local Project binding state out of git.",
59
+ meta: {
60
+ gitignorePath: error.gitignorePath,
61
+ operation: error.operation
62
+ }
63
+ })
64
+ });
65
+ }
66
+ function localStateWriteFailedError(error, options) {
67
+ return new CliError({
68
+ code: "LOCAL_STATE_WRITE_FAILED",
69
+ domain: "project",
70
+ summary: "Could not save local Project binding",
71
+ why: options.why,
72
+ fix: "Check that this directory is writable and that .prisma/local.json and .gitignore are not blocked by directories or permissions, then retry.",
73
+ debug: formatDebugDetails(error.cause),
74
+ meta: options.meta,
75
+ exitCode: 1,
76
+ nextSteps: ["prisma-cli project link <id-or-name>", "prisma-cli app deploy --project <id-or-name>"]
77
+ });
78
+ }
79
+ function toProjectSummary(project) {
80
+ return {
81
+ id: project.id,
82
+ name: project.name,
83
+ ...project.url ? { url: project.url } : {}
84
+ };
85
+ }
86
+ function projectSetupNameRequiredError(command) {
87
+ return usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", [`prisma-cli ${command} my-app`], "project");
88
+ }
89
+ function projectCreateFailedError(error, projectName, workspace, options) {
90
+ const status = extractHttpStatus(error);
91
+ if (status === 401 || status === 403) return new CliError({
92
+ code: "PROJECT_CREATE_FAILED",
93
+ domain: "project",
94
+ summary: `Could not create Project "${projectName}"`,
95
+ why: `The platform rejected the Project create in workspace "${workspace.name}" (HTTP ${status}).`,
96
+ fix: options.permissionFix,
97
+ debug: formatDebugDetails(error),
98
+ exitCode: 1,
99
+ nextSteps: options.nextSteps
100
+ });
101
+ return new CliError({
102
+ code: "PROJECT_CREATE_FAILED",
103
+ domain: "project",
104
+ summary: `Could not create Project "${projectName}"`,
105
+ why: error instanceof Error ? error.message : String(error),
106
+ fix: options.fallbackFix,
107
+ debug: formatDebugDetails(error),
108
+ exitCode: 1,
109
+ nextSteps: options.nextSteps
110
+ });
111
+ }
112
+ function formatSetupDirectory(cwd) {
113
+ const basename = cwd.split(/[\\/]/).filter(Boolean).pop();
114
+ return basename ? `./${basename}` : ".";
115
+ }
116
+ function extractHttpStatus(error) {
117
+ if (!error || typeof error !== "object") return null;
118
+ const candidate = error;
119
+ if (typeof candidate.statusCode === "number") return candidate.statusCode;
120
+ if (typeof candidate.status === "number") return candidate.status;
121
+ if (typeof candidate.message === "string") {
122
+ const match = /\(HTTP (\d{3})\)/.exec(candidate.message);
123
+ if (match) return Number.parseInt(match[1], 10);
124
+ }
125
+ return null;
126
+ }
127
+ function formatDebugDetails(error) {
128
+ if (error instanceof Error) return error.stack ?? error.message;
129
+ return typeof error === "string" ? error : null;
130
+ }
131
+ //#endregion
132
+ export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };