@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.
- package/README.md +18 -3
- package/dist/adapters/git.js +8 -3
- package/dist/adapters/local-state.js +12 -4
- package/dist/adapters/mock-api.js +81 -2
- package/dist/adapters/token-storage.js +63 -22
- package/dist/cli.js +17 -2
- package/dist/cli2.js +7 -3
- package/dist/commands/app/index.js +26 -13
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/database/index.js +159 -0
- package/dist/commands/env.js +8 -4
- package/dist/commands/project/index.js +28 -2
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +284 -132
- package/dist/controllers/app.js +493 -344
- package/dist/controllers/auth.js +8 -8
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/database.js +318 -0
- package/dist/controllers/project.js +302 -75
- package/dist/lib/app/branch-database-deploy.js +373 -0
- package/dist/lib/app/branch-database.js +316 -0
- package/dist/lib/app/bun-project.js +12 -5
- package/dist/lib/app/deploy-output.js +10 -1
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/local-dev.js +34 -18
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-build-settings.js +385 -0
- package/dist/lib/app/preview-build.js +272 -81
- package/dist/lib/app/preview-provider.js +163 -54
- package/dist/lib/app/production-deploy-gate.js +161 -0
- package/dist/lib/auth/auth-ops.js +69 -19
- package/dist/lib/auth/guard.js +3 -2
- package/dist/lib/auth/login.js +109 -14
- package/dist/lib/database/provider.js +167 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/git/local-branch.js +41 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/interactive-setup.js +56 -0
- package/dist/lib/project/local-pin.js +182 -33
- package/dist/lib/project/resolution.js +287 -105
- package/dist/lib/project/setup.js +132 -0
- package/dist/presenters/app-env.js +149 -14
- package/dist/presenters/app.js +170 -20
- package/dist/presenters/auth.js +19 -6
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/database.js +274 -0
- package/dist/presenters/project.js +100 -47
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/command-arguments.js +6 -0
- package/dist/shell/command-meta.js +139 -16
- package/dist/shell/command-runner.js +38 -8
- package/dist/shell/diagnostics-output.js +63 -0
- package/dist/shell/errors.js +14 -1
- package/dist/shell/output.js +13 -2
- package/dist/shell/runtime.js +3 -3
- package/dist/shell/ui.js +23 -1
- package/dist/shell/update-check.js +247 -0
- package/dist/use-cases/auth.js +15 -4
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/dist/use-cases/project.js +2 -1
- package/package.json +12 -10
|
@@ -1,76 +1,100 @@
|
|
|
1
1
|
import { CliError } from "../../shell/errors.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
129
|
+
function localStateStaleCliError() {
|
|
103
130
|
return new CliError({
|
|
104
|
-
code: "
|
|
131
|
+
code: "LOCAL_STATE_STALE",
|
|
105
132
|
domain: "project",
|
|
106
|
-
summary: "
|
|
107
|
-
why:
|
|
108
|
-
fix:
|
|
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
|
|
138
|
+
nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
|
|
111
139
|
});
|
|
112
140
|
}
|
|
113
|
-
function
|
|
141
|
+
function localProjectWorkspaceMismatchCliError(options) {
|
|
114
142
|
return new CliError({
|
|
115
|
-
code: "
|
|
143
|
+
code: "LOCAL_PROJECT_WORKSPACE_MISMATCH",
|
|
116
144
|
domain: "project",
|
|
117
|
-
summary: "
|
|
118
|
-
why:
|
|
119
|
-
fix: "
|
|
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: [
|
|
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
|
-
|
|
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"),
|
|
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)
|
|
158
|
-
|
|
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
|
|
174
|
-
return project.id ===
|
|
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
|
|
180
|
-
if (options.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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 };
|