@prisma/cli 3.0.0-beta.1 → 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 (61) hide show
  1. package/README.md +5 -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 +22 -10
  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/controllers/app-env-api.js +54 -0
  13. package/dist/controllers/app-env-file.js +181 -0
  14. package/dist/controllers/app-env.js +283 -129
  15. package/dist/controllers/app.js +247 -106
  16. package/dist/controllers/auth.js +8 -8
  17. package/dist/controllers/branch.js +78 -48
  18. package/dist/controllers/database.js +318 -0
  19. package/dist/controllers/project.js +153 -82
  20. package/dist/lib/app/branch-database-deploy.js +373 -0
  21. package/dist/lib/app/branch-database.js +316 -0
  22. package/dist/lib/app/bun-project.js +12 -5
  23. package/dist/lib/app/deploy-output.js +10 -1
  24. package/dist/lib/app/env-config.js +1 -1
  25. package/dist/lib/app/env-file.js +82 -0
  26. package/dist/lib/app/env-vars.js +28 -2
  27. package/dist/lib/app/local-dev.js +34 -18
  28. package/dist/lib/app/preview-branch-database.js +102 -0
  29. package/dist/lib/app/preview-build-settings.js +385 -0
  30. package/dist/lib/app/preview-build.js +272 -81
  31. package/dist/lib/app/preview-provider.js +163 -54
  32. package/dist/lib/app/production-deploy-gate.js +161 -0
  33. package/dist/lib/auth/auth-ops.js +30 -21
  34. package/dist/lib/auth/guard.js +3 -2
  35. package/dist/lib/auth/login.js +109 -14
  36. package/dist/lib/database/provider.js +167 -0
  37. package/dist/lib/diagnostics.js +15 -0
  38. package/dist/lib/git/local-branch.js +41 -0
  39. package/dist/lib/git/local-status.js +57 -0
  40. package/dist/lib/project/interactive-setup.js +1 -1
  41. package/dist/lib/project/local-pin.js +182 -33
  42. package/dist/lib/project/resolution.js +203 -49
  43. package/dist/lib/project/setup.js +59 -15
  44. package/dist/presenters/app-env.js +149 -14
  45. package/dist/presenters/app.js +170 -20
  46. package/dist/presenters/branch.js +37 -102
  47. package/dist/presenters/database.js +274 -0
  48. package/dist/presenters/project.js +63 -39
  49. package/dist/presenters/verbose-context.js +64 -0
  50. package/dist/shell/command-meta.js +103 -13
  51. package/dist/shell/command-runner.js +34 -6
  52. package/dist/shell/diagnostics-output.js +63 -0
  53. package/dist/shell/errors.js +12 -1
  54. package/dist/shell/output.js +10 -1
  55. package/dist/shell/runtime.js +3 -3
  56. package/dist/shell/ui.js +23 -1
  57. package/dist/shell/update-check.js +247 -0
  58. package/dist/use-cases/branch.js +20 -68
  59. package/dist/use-cases/create-cli-gateways.js +2 -17
  60. package/dist/use-cases/project.js +2 -1
  61. package/package.json +12 -10
@@ -1,44 +1,193 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { Result, TaggedError, UnhandledException } from "better-result";
3
4
  //#region src/lib/project/local-pin.ts
4
5
  const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
5
- async function readLocalResolutionPin(cwd) {
6
- try {
7
- const raw = await readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), "utf8");
8
- const parsed = JSON.parse(raw);
9
- if (!isLocalResolutionPin(parsed)) return { kind: "invalid" };
10
- return {
6
+ var LocalResolutionPinInvalidJsonError = class extends TaggedError("LocalResolutionPinInvalidJsonError")() {
7
+ constructor(cause) {
8
+ super({
9
+ message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} contains invalid JSON.`,
10
+ cause,
11
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
12
+ });
13
+ }
14
+ };
15
+ var LocalResolutionPinInvalidShapeError = class extends TaggedError("LocalResolutionPinInvalidShapeError")() {
16
+ constructor() {
17
+ super({
18
+ message: `${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} must contain workspaceId and projectId string fields only.`,
19
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
20
+ });
21
+ }
22
+ };
23
+ var LocalResolutionPinReadAbortedError = class extends TaggedError("LocalResolutionPinReadAbortedError")() {
24
+ constructor(cause) {
25
+ super({
26
+ message: `Reading ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} was aborted.`,
27
+ cause,
28
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
29
+ });
30
+ }
31
+ };
32
+ var LocalResolutionPinSerializationError = class extends TaggedError("LocalResolutionPinSerializationError")() {
33
+ constructor(cause) {
34
+ super({
35
+ message: `Could not serialize ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`,
36
+ cause,
37
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
38
+ });
39
+ }
40
+ };
41
+ var LocalResolutionPinWriteAbortedError = class extends TaggedError("LocalResolutionPinWriteAbortedError")() {
42
+ constructor(cause) {
43
+ super({
44
+ message: `Writing ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} was aborted.`,
45
+ cause,
46
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
47
+ });
48
+ }
49
+ };
50
+ var LocalResolutionPinWriteFailedError = class extends TaggedError("LocalResolutionPinWriteFailedError")() {
51
+ constructor(operation, cause) {
52
+ super({
53
+ message: `Could not write ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`,
54
+ cause,
55
+ operation,
56
+ pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH
57
+ });
58
+ }
59
+ };
60
+ var LocalResolutionPinGitignoreUpdateAbortedError = class extends TaggedError("LocalResolutionPinGitignoreUpdateAbortedError")() {
61
+ constructor(cause) {
62
+ super({
63
+ message: "Updating .gitignore for the local Project binding was aborted.",
64
+ cause,
65
+ gitignorePath: ".gitignore"
66
+ });
67
+ }
68
+ };
69
+ var LocalResolutionPinGitignoreUpdateFailedError = class extends TaggedError("LocalResolutionPinGitignoreUpdateFailedError")() {
70
+ constructor(operation, cause) {
71
+ super({
72
+ message: "Could not update .gitignore for the local Project binding.",
73
+ cause,
74
+ operation,
75
+ gitignorePath: ".gitignore"
76
+ });
77
+ }
78
+ };
79
+ async function readLocalResolutionPin(cwd, signal) {
80
+ return Result.gen(async function* () {
81
+ yield* ensureLocalResolutionPinReadNotAborted(signal);
82
+ const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal));
83
+ if (file.kind === "missing") return Result.ok({ kind: "missing" });
84
+ const parsed = yield* parseLocalResolutionPin(file.raw);
85
+ if (!isLocalResolutionPin(parsed)) return Result.err(new LocalResolutionPinInvalidShapeError());
86
+ return Result.ok({
11
87
  kind: "present",
12
88
  pin: parsed
13
- };
14
- } catch (error) {
15
- if (error.code === "ENOENT") return { kind: "missing" };
16
- if (error instanceof SyntaxError) return { kind: "invalid" };
17
- throw error;
18
- }
19
- }
20
- async function writeLocalResolutionPin(cwd, pin) {
21
- const prismaDir = path.join(cwd, ".prisma");
22
- await mkdir(prismaDir, { recursive: true });
23
- const pinPath = path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH);
24
- const tmpPath = path.join(prismaDir, `local.${process.pid}.${Date.now()}.tmp`);
25
- await writeFile(tmpPath, `${JSON.stringify(pin, null, 2)}\n`, "utf8");
26
- await rename(tmpPath, pinPath);
27
- }
28
- async function ensureLocalResolutionPinGitignore(cwd) {
89
+ });
90
+ });
91
+ }
92
+ function ensureLocalResolutionPinReadNotAborted(signal) {
93
+ return Result.try({
94
+ try: () => signal?.throwIfAborted(),
95
+ catch: (cause) => new LocalResolutionPinReadAbortedError(cause)
96
+ });
97
+ }
98
+ async function readLocalResolutionPinFile(cwd, signal) {
99
+ const readResult = await Result.tryPromise({
100
+ try: () => readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
101
+ encoding: "utf8",
102
+ signal
103
+ }),
104
+ catch: (cause) => signal?.aborted ? new LocalResolutionPinReadAbortedError(cause) : new UnhandledException({ cause })
105
+ });
106
+ if (readResult.isErr()) {
107
+ if (readResult.error instanceof UnhandledException && readResult.error.cause.code === "ENOENT") return Result.ok({ kind: "missing" });
108
+ return Result.err(readResult.error);
109
+ }
110
+ return Result.ok({
111
+ kind: "present",
112
+ raw: readResult.value
113
+ });
114
+ }
115
+ function parseLocalResolutionPin(raw) {
116
+ return Result.try({
117
+ try: () => JSON.parse(raw),
118
+ catch: (cause) => cause instanceof SyntaxError ? new LocalResolutionPinInvalidJsonError(cause) : new UnhandledException({ cause })
119
+ });
120
+ }
121
+ async function writeLocalResolutionPin(cwd, pin, signal) {
122
+ return Result.gen(async function* () {
123
+ const prismaDir = path.join(cwd, ".prisma");
124
+ yield* ensureLocalResolutionPinWriteNotAborted(signal);
125
+ yield* Result.await(writeLocalResolutionPinBoundary(() => mkdir(prismaDir, { recursive: true }), "create-directory", signal));
126
+ const pinPath = path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH);
127
+ const tmpPath = path.join(prismaDir, `local.${process.pid}.${Date.now()}.tmp`);
128
+ const serialized = yield* serializeLocalResolutionPin(pin);
129
+ yield* Result.await(writeLocalResolutionPinBoundary(() => writeFile(tmpPath, serialized, {
130
+ encoding: "utf8",
131
+ signal
132
+ }), "write-temp-file", signal));
133
+ yield* ensureLocalResolutionPinWriteNotAborted(signal);
134
+ yield* Result.await(writeLocalResolutionPinBoundary(() => rename(tmpPath, pinPath), "rename-temp-file", signal));
135
+ return Result.ok(void 0);
136
+ });
137
+ }
138
+ async function ensureLocalResolutionPinGitignore(cwd, signal) {
29
139
  const gitignorePath = path.join(cwd, ".gitignore");
30
140
  let existing = null;
31
- try {
32
- existing = await readFile(gitignorePath, "utf8");
33
- } catch (error) {
34
- if (error.code !== "ENOENT") throw error;
35
- }
36
- if (existing === null) {
37
- await writeFile(gitignorePath, ".prisma/\n", "utf8");
38
- return;
39
- }
40
- if (existing.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".prisma/" || line === ".prisma/local.json")) return;
41
- await writeFile(gitignorePath, existing.endsWith("\n") ? `${existing}.prisma/\n` : `${existing}\n.prisma/\n`, "utf8");
141
+ const notAborted = ensureLocalResolutionPinGitignoreUpdateNotAborted(signal);
142
+ if (notAborted.isErr()) return Result.err(notAborted.error);
143
+ const existingResult = await Result.tryPromise({
144
+ try: () => readFile(gitignorePath, {
145
+ encoding: "utf8",
146
+ signal
147
+ }),
148
+ catch: (cause) => signal?.aborted ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) : new LocalResolutionPinGitignoreUpdateFailedError("read", cause)
149
+ });
150
+ if (existingResult.isErr()) if (existingResult.error instanceof LocalResolutionPinGitignoreUpdateFailedError && existingResult.error.cause.code === "ENOENT") existing = null;
151
+ else return Result.err(existingResult.error);
152
+ else existing = existingResult.value;
153
+ if (existing === null) return writeLocalResolutionPinGitignore(gitignorePath, ".prisma/\n", signal);
154
+ if (existing.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".prisma/" || line === ".prisma/local.json")) return Result.ok(void 0);
155
+ return writeLocalResolutionPinGitignore(gitignorePath, existing.endsWith("\n") ? `${existing}.prisma/\n` : `${existing}\n.prisma/\n`, signal);
156
+ }
157
+ function ensureLocalResolutionPinWriteNotAborted(signal) {
158
+ return Result.try({
159
+ try: () => signal?.throwIfAborted(),
160
+ catch: (cause) => new LocalResolutionPinWriteAbortedError(cause)
161
+ });
162
+ }
163
+ function serializeLocalResolutionPin(pin) {
164
+ return Result.try({
165
+ try: () => `${JSON.stringify(pin, null, 2)}\n`,
166
+ catch: (cause) => new LocalResolutionPinSerializationError(cause)
167
+ });
168
+ }
169
+ function writeLocalResolutionPinBoundary(run, operation, signal) {
170
+ return Result.tryPromise({
171
+ try: async () => {
172
+ await run();
173
+ },
174
+ catch: (cause) => signal?.aborted ? new LocalResolutionPinWriteAbortedError(cause) : new LocalResolutionPinWriteFailedError(operation, cause)
175
+ });
176
+ }
177
+ function ensureLocalResolutionPinGitignoreUpdateNotAborted(signal) {
178
+ return Result.try({
179
+ try: () => signal?.throwIfAborted(),
180
+ catch: (cause) => new LocalResolutionPinGitignoreUpdateAbortedError(cause)
181
+ });
182
+ }
183
+ function writeLocalResolutionPinGitignore(gitignorePath, contents, signal) {
184
+ return Result.tryPromise({
185
+ try: () => writeFile(gitignorePath, contents, {
186
+ encoding: "utf8",
187
+ signal
188
+ }),
189
+ catch: (cause) => signal?.aborted ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) : new LocalResolutionPinGitignoreUpdateFailedError("write", cause)
190
+ });
42
191
  }
43
192
  function isLocalResolutionPin(value) {
44
193
  if (!value || typeof value !== "object") return false;
@@ -3,34 +3,98 @@ import { formatCommandArgument } from "../../shell/command-arguments.js";
3
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { Result, TaggedError, matchError } from "better-result";
6
7
  //#region src/lib/project/resolution.ts
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
+ });
15
+ }
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
23
+ });
24
+ }
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
+ });
32
+ }
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
+ };
7
54
  async function resolveProjectTarget(options) {
8
- const projects = await options.listProjects();
9
- const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: true });
10
- if (target) return target;
11
- throw await projectSetupRequiredError({
12
- cwd: options.context.runtime.cwd,
13
- projects,
14
- commandName: options.commandName
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
+ }));
15
69
  });
16
70
  }
17
71
  async function inspectProjectBinding(options) {
18
- const projects = await options.listProjects();
19
- const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: false });
20
- if (target) return target;
21
- return {
22
- workspace: options.workspace,
23
- project: null,
24
- localBinding: { status: "not-linked" },
25
- resolution: { projectSource: "unbound" },
26
- ...await buildProjectSetupSuggestion({
27
- cwd: options.context.runtime.cwd,
28
- projects,
29
- commandName: options.commandName ?? "project show"
30
- })
31
- };
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
+ });
32
93
  }
33
94
  function projectNotFoundError(projectRef, workspace) {
95
+ return projectResolutionErrorToCliError(new ProjectNotFoundError(projectRef, workspace));
96
+ }
97
+ function projectNotFoundCliError(projectRef, workspace) {
34
98
  return new CliError({
35
99
  code: "PROJECT_NOT_FOUND",
36
100
  domain: "project",
@@ -42,6 +106,9 @@ function projectNotFoundError(projectRef, workspace) {
42
106
  });
43
107
  }
44
108
  function projectAmbiguousError(projectRef, matches) {
109
+ return projectResolutionErrorToCliError(new ProjectAmbiguousError(projectRef, matches));
110
+ }
111
+ function projectAmbiguousCliError(projectRef, matches) {
45
112
  const firstMatch = matches[0];
46
113
  const nextSteps = ["prisma-cli project list"];
47
114
  if (firstMatch) nextSteps.push(`prisma-cli app deploy --project ${firstMatch.id}`);
@@ -59,7 +126,7 @@ function projectAmbiguousError(projectRef, matches) {
59
126
  nextSteps
60
127
  });
61
128
  }
62
- function localStateStaleError() {
129
+ function localStateStaleCliError() {
63
130
  return new CliError({
64
131
  code: "LOCAL_STATE_STALE",
65
132
  domain: "project",
@@ -71,8 +138,55 @@ function localStateStaleError() {
71
138
  nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
72
139
  });
73
140
  }
141
+ function localProjectWorkspaceMismatchCliError(options) {
142
+ return new CliError({
143
+ code: "LOCAL_PROJECT_WORKSPACE_MISMATCH",
144
+ domain: "project",
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
+ },
155
+ exitCode: 1,
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
+ }
74
188
  async function buildProjectSetupSuggestion(options) {
75
- const suggestedName = await inferTargetName(options.cwd);
189
+ const suggestedName = await inferTargetName(options.cwd, options.signal);
76
190
  const candidates = sortProjects(options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name))).map(toProjectSummary);
77
191
  return {
78
192
  suggestedProjectName: suggestedName.name,
@@ -83,17 +197,24 @@ async function buildProjectSetupSuggestion(options) {
83
197
  }
84
198
  async function projectSetupRequiredError(options) {
85
199
  const suggestion = await buildProjectSetupSuggestion(options);
200
+ return new ProjectSetupRequiredError({
201
+ commandName: options.commandName,
202
+ suggestion
203
+ });
204
+ }
205
+ function projectSetupRequiredCliError(error) {
206
+ const suggestion = error.suggestion;
86
207
  return new CliError({
87
208
  code: "PROJECT_SETUP_REQUIRED",
88
209
  domain: "project",
89
210
  summary: "Choose a Project before running this command",
90
- why: `This directory is not linked to a Prisma Project, and ${options.commandName ? `prisma-cli ${options.commandName}` : "this command"} will not choose one from package or directory names.`,
211
+ why: error.message,
91
212
  fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
92
213
  meta: { ...suggestion },
93
214
  exitCode: 1,
94
215
  nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
95
216
  nextActions: buildProjectSetupNextActions({
96
- commandName: options.commandName,
217
+ commandName: error.commandName,
97
218
  suggestedProjectName: suggestion.suggestedProjectName
98
219
  })
99
220
  });
@@ -131,9 +252,13 @@ function buildProjectSetupNextActions(options = {}) {
131
252
  });
132
253
  return actions;
133
254
  }
134
- async function readPackageName(cwd) {
255
+ async function readPackageName(cwd, signal) {
256
+ signal?.throwIfAborted();
135
257
  try {
136
- 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
+ });
137
262
  const parsed = JSON.parse(raw);
138
263
  if (!parsed || typeof parsed !== "object") return null;
139
264
  const packageName = "name" in parsed ? parsed.name : null;
@@ -144,8 +269,8 @@ async function readPackageName(cwd) {
144
269
  throw error;
145
270
  }
146
271
  }
147
- async function inferTargetName(cwd) {
148
- const packageName = await readPackageName(cwd);
272
+ async function inferTargetName(cwd, signal) {
273
+ const packageName = await readPackageName(cwd, signal);
149
274
  if (packageName && isValidInferredTargetName(packageName)) return {
150
275
  name: packageName,
151
276
  source: "package-name"
@@ -163,9 +288,9 @@ function sortProjects(projects) {
163
288
  }
164
289
  function resolveExplicitProject(projectRef, projects, workspace) {
165
290
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
166
- if (matches.length === 1) return matches[0];
167
- if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
168
- throw projectNotFoundError(projectRef, workspace);
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));
169
294
  }
170
295
  function projectMatchesSuggestedName(project, suggestedName) {
171
296
  return project.id === suggestedName || project.name === suggestedName || project.slug === suggestedName;
@@ -174,35 +299,63 @@ async function resolveDurablePlatformMapping() {
174
299
  return null;
175
300
  }
176
301
  async function resolveBoundProjectTarget(options, projects, settings) {
177
- if (options.explicitProject) return resolvedTarget(options.workspace, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
178
- targetName: options.explicitProject,
179
- targetNameSource: "explicit"
180
- });
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
+ }
181
310
  if (settings.allowEnvProjectId && options.envProjectId) {
182
311
  const project = projects.find((candidate) => candidate.id === options.envProjectId);
183
- if (!project) throw projectNotFoundError(options.envProjectId, options.workspace);
184
- return resolvedTarget(options.workspace, project, "env", {
312
+ if (!project) return Result.err(new ProjectNotFoundError(options.envProjectId, options.workspace));
313
+ return Result.ok(resolvedTarget(options.workspace, project, "env", {
185
314
  targetName: options.envProjectId,
186
315
  targetNameSource: "env"
187
- });
316
+ }));
188
317
  }
189
- const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
190
- if (localPin.kind === "invalid") throw localStateStaleError();
318
+ const localPin = settings.localPin;
319
+ if (!localPin) return Result.ok(null);
191
320
  if (localPin.kind === "present") {
192
- if (localPin.pin.workspaceId !== options.workspace.id) throw localStateStaleError();
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
+ }));
193
326
  const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
194
- if (!project) throw localStateStaleError();
195
- return resolvedTarget(options.workspace, project, "local-pin", {
327
+ if (!project) return Result.err(new LocalStateStaleError());
328
+ return Result.ok(resolvedTarget(options.workspace, project, "local-pin", {
196
329
  targetName: project.name,
197
330
  targetNameSource: "local-pin"
198
- });
331
+ }));
199
332
  }
200
333
  const platformMapping = await resolveDurablePlatformMapping();
201
- if (platformMapping && platformMapping.workspace.id === options.workspace.id) return resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
334
+ if (platformMapping && platformMapping.workspace.id === options.workspace.id) return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
202
335
  targetName: platformMapping.name,
203
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
204
358
  });
205
- return null;
206
359
  }
207
360
  function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
208
361
  return {
@@ -222,8 +375,9 @@ function buildProjectRecoveryCommands(commandName) {
222
375
  function toProjectSummary(project) {
223
376
  return {
224
377
  id: project.id,
225
- name: project.name
378
+ name: project.name,
379
+ ...project.url ? { url: project.url } : {}
226
380
  };
227
381
  }
228
382
  //#endregion
229
- export { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
383
+ export { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, projectResolutionErrorToCliError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
@@ -2,6 +2,7 @@ import { CliError, usageError } from "../../shell/errors.js";
2
2
  import "../../shell/command-arguments.js";
3
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
4
4
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
5
+ import { Result, matchError } from "better-result";
5
6
  //#region src/lib/project/setup.ts
6
7
  function isValidProjectSetupName(projectName) {
7
8
  return projectName.trim().length > 0;
@@ -17,26 +18,69 @@ function resolveProjectForSetup(projectRef, projects, workspace) {
17
18
  throw projectNotFoundError(projectRef, workspace);
18
19
  }
19
20
  async function bindProjectToDirectory(context, workspace, project, action) {
20
- await writeLocalResolutionPin(context.runtime.cwd, {
21
- workspaceId: workspace.id,
22
- projectId: project.id
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
+ });
23
37
  });
24
- await ensureLocalResolutionPinGitignore(context.runtime.cwd);
25
- return {
26
- workspace,
27
- project,
28
- directory: formatSetupDirectory(context.runtime.cwd),
29
- localPin: {
30
- path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
31
- written: true
38
+ }
39
+ function projectDirectoryBindingErrorToCliError(error) {
40
+ return matchError(error, {
41
+ LocalResolutionPinSerializationError: (error) => {
42
+ throw error;
32
43
  },
33
- action
34
- };
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
+ });
35
78
  }
36
79
  function toProjectSummary(project) {
37
80
  return {
38
81
  id: project.id,
39
- name: project.name
82
+ name: project.name,
83
+ ...project.url ? { url: project.url } : {}
40
84
  };
41
85
  }
42
86
  function projectSetupNameRequiredError(command) {
@@ -85,4 +129,4 @@ function formatDebugDetails(error) {
85
129
  return typeof error === "string" ? error : null;
86
130
  }
87
131
  //#endregion
88
- export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };
132
+ export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };