@prisma/cli 3.0.0-alpha.9 → 3.0.0-beta.1
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 +132 -14
- package/dist/adapters/token-storage.js +57 -1
- package/dist/commands/app/index.js +119 -35
- package/dist/commands/env.js +17 -9
- package/dist/commands/project/index.js +28 -2
- package/dist/controllers/app-env.js +156 -32
- package/dist/controllers/app.js +682 -396
- package/dist/controllers/project.js +177 -21
- package/dist/lib/app/domain-guidance.js +14 -0
- package/dist/lib/app/env-config.js +16 -6
- package/dist/lib/app/preview-build.js +50 -5
- package/dist/lib/app/preview-progress.js +1 -25
- package/dist/lib/app/preview-provider.js +99 -1
- package/dist/lib/auth/auth-ops.js +49 -8
- package/dist/lib/project/interactive-setup.js +56 -0
- package/dist/lib/project/resolution.js +124 -96
- package/dist/lib/project/setup.js +88 -0
- package/dist/presenters/app-env.js +4 -3
- package/dist/presenters/app.js +172 -73
- package/dist/presenters/auth.js +19 -6
- package/dist/presenters/project.js +44 -15
- package/dist/shell/command-arguments.js +6 -0
- package/dist/shell/command-meta.js +120 -24
- package/dist/shell/command-runner.js +21 -11
- package/dist/shell/errors.js +4 -1
- package/dist/shell/output.js +3 -1
- package/dist/use-cases/auth.js +15 -4
- package/package.json +4 -4
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
1
|
+
import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors.js";
|
|
2
2
|
import { renderSummaryLine } from "../shell/ui.js";
|
|
3
3
|
import { canPrompt } from "../shell/runtime.js";
|
|
4
4
|
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
5
|
-
import {
|
|
5
|
+
import { formatCommandArgument } from "../shell/command-arguments.js";
|
|
6
|
+
import { readLocalResolutionPin } from "../lib/project/local-pin.js";
|
|
7
|
+
import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
|
|
8
|
+
import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
|
|
9
|
+
import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
|
|
10
|
+
import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
6
11
|
import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
|
|
7
12
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
8
13
|
import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
|
|
@@ -14,6 +19,12 @@ const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
|
|
|
14
19
|
function isRealMode(context) {
|
|
15
20
|
return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
|
|
16
21
|
}
|
|
22
|
+
async function readProjectListLocalBinding(cwd, workspace, projects) {
|
|
23
|
+
const pin = await readLocalResolutionPin(cwd);
|
|
24
|
+
if (pin.kind === "present") return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId) ? { status: "linked" } : { status: "invalid" };
|
|
25
|
+
if (pin.kind === "invalid") return { status: "invalid" };
|
|
26
|
+
return { status: "not-linked" };
|
|
27
|
+
}
|
|
17
28
|
async function runProjectList(context) {
|
|
18
29
|
const authState = await requireAuthenticatedAuthState(context);
|
|
19
30
|
const workspace = authState.workspace;
|
|
@@ -21,40 +32,171 @@ async function runProjectList(context) {
|
|
|
21
32
|
if (isRealMode(context)) {
|
|
22
33
|
const client = await requireComputeAuth(context.runtime.env);
|
|
23
34
|
if (!client) throw authRequiredError();
|
|
35
|
+
const projects = sortProjects(await listRealWorkspaceProjects(client, workspace));
|
|
36
|
+
const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, projects);
|
|
37
|
+
const nextActions = buildProjectListNextActions(localBinding);
|
|
24
38
|
return {
|
|
25
39
|
command: "project.list",
|
|
26
40
|
result: {
|
|
27
41
|
workspace,
|
|
28
|
-
projects:
|
|
42
|
+
projects: projects.map(toProjectSummary),
|
|
43
|
+
localBinding
|
|
29
44
|
},
|
|
30
45
|
warnings: [],
|
|
31
|
-
nextSteps: []
|
|
46
|
+
nextSteps: [],
|
|
47
|
+
nextActions
|
|
32
48
|
};
|
|
33
49
|
}
|
|
50
|
+
const result = await createProjectUseCases(createCliUseCaseGateways(context)).list(authState);
|
|
51
|
+
const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, result.projects);
|
|
52
|
+
const nextActions = buildProjectListNextActions(localBinding);
|
|
34
53
|
return {
|
|
35
54
|
command: "project.list",
|
|
36
|
-
result:
|
|
55
|
+
result: {
|
|
56
|
+
...result,
|
|
57
|
+
localBinding
|
|
58
|
+
},
|
|
37
59
|
warnings: [],
|
|
38
|
-
nextSteps: []
|
|
60
|
+
nextSteps: [],
|
|
61
|
+
nextActions
|
|
39
62
|
};
|
|
40
63
|
}
|
|
64
|
+
function buildProjectListNextActions(localBinding) {
|
|
65
|
+
return localBinding?.status === "linked" ? [] : buildProjectSetupNextActions({
|
|
66
|
+
createCommand: "prisma-cli project create <name>",
|
|
67
|
+
reason: localBinding?.status === "invalid" ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands." : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory."
|
|
68
|
+
});
|
|
69
|
+
}
|
|
41
70
|
async function runProjectShow(context, explicitProject) {
|
|
42
71
|
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
43
72
|
if (!workspace) throw workspaceRequiredError();
|
|
73
|
+
const result = isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject);
|
|
44
74
|
return {
|
|
45
75
|
command: "project.show",
|
|
46
|
-
result
|
|
76
|
+
result,
|
|
47
77
|
warnings: [],
|
|
48
|
-
nextSteps: []
|
|
78
|
+
nextSteps: [],
|
|
79
|
+
nextActions: result.project === null ? buildProjectSetupNextActions({
|
|
80
|
+
commandName: "project show",
|
|
81
|
+
suggestedProjectName: result.suggestedProjectName,
|
|
82
|
+
reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project."
|
|
83
|
+
}) : []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function runProjectCreate(context, projectName) {
|
|
87
|
+
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
88
|
+
if (!workspace) throw workspaceRequiredError();
|
|
89
|
+
if (!isValidProjectSetupName(projectName)) throw projectSetupNameRequiredError("project create");
|
|
90
|
+
if (!isRealMode(context)) throw featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project");
|
|
91
|
+
const client = await requireComputeAuth(context.runtime.env);
|
|
92
|
+
if (!client) throw authRequiredError();
|
|
93
|
+
const provider = createPreviewAppProvider(client);
|
|
94
|
+
const name = projectName.trim();
|
|
95
|
+
const created = await provider.createProject({ name }).catch((error) => {
|
|
96
|
+
throw projectCreateFailedError(error, name, workspace, {
|
|
97
|
+
nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"],
|
|
98
|
+
permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
|
|
99
|
+
fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
command: "project.create",
|
|
104
|
+
result: await bindProjectToDirectory(context, workspace, {
|
|
105
|
+
id: created.id,
|
|
106
|
+
name: created.name
|
|
107
|
+
}, "created"),
|
|
108
|
+
warnings: [],
|
|
109
|
+
nextSteps: ["prisma-cli app deploy"]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function runProjectLink(context, projectRef) {
|
|
113
|
+
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
114
|
+
if (!workspace) throw workspaceRequiredError();
|
|
115
|
+
let provider = null;
|
|
116
|
+
let projects;
|
|
117
|
+
if (isRealMode(context)) {
|
|
118
|
+
const client = await requireComputeAuth(context.runtime.env);
|
|
119
|
+
if (!client) throw authRequiredError();
|
|
120
|
+
provider = createPreviewAppProvider(client);
|
|
121
|
+
projects = await listRealWorkspaceProjects(client, workspace);
|
|
122
|
+
} else projects = listFixtureWorkspaceProjects(context, workspace);
|
|
123
|
+
let result;
|
|
124
|
+
if (projectRef?.trim()) result = await bindProjectToDirectory(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked");
|
|
125
|
+
else if (canPrompt(context) && !context.flags.yes) result = await resolveInteractiveProjectLinkSetup(context, workspace, projects, provider);
|
|
126
|
+
else throw await projectLinkTargetRequiredError(context, projects);
|
|
127
|
+
return {
|
|
128
|
+
command: "project.link",
|
|
129
|
+
result,
|
|
130
|
+
warnings: [],
|
|
131
|
+
nextSteps: ["prisma-cli app deploy"]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function resolveInteractiveProjectLinkSetup(context, workspace, projects, provider) {
|
|
135
|
+
const setup = await promptForProjectSetupChoice({
|
|
136
|
+
context,
|
|
137
|
+
projects,
|
|
138
|
+
createProject: (projectName) => {
|
|
139
|
+
if (!provider) throw featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project");
|
|
140
|
+
return createProjectForLinkSetup(provider, projectName, workspace);
|
|
141
|
+
},
|
|
142
|
+
cancel: {
|
|
143
|
+
why: "Project link needs a Project before it can continue.",
|
|
144
|
+
fix: "Choose an existing Project or create a new one, then rerun project link.",
|
|
145
|
+
nextSteps: ["prisma-cli project link <id-or-name>", "prisma-cli project create <name>"]
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return bindProjectToDirectory(context, workspace, setup.project, setup.action);
|
|
149
|
+
}
|
|
150
|
+
async function createProjectForLinkSetup(provider, projectName, workspace) {
|
|
151
|
+
const created = await provider.createProject({ name: projectName }).catch((error) => {
|
|
152
|
+
throw projectCreateFailedError(error, projectName, workspace, {
|
|
153
|
+
nextSteps: [
|
|
154
|
+
"prisma-cli project list",
|
|
155
|
+
"prisma-cli project link <id-or-name>",
|
|
156
|
+
`prisma-cli project create ${formatCommandArgument(projectName)}`
|
|
157
|
+
],
|
|
158
|
+
permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
|
|
159
|
+
fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
id: created.id,
|
|
164
|
+
name: created.name,
|
|
165
|
+
workspace
|
|
49
166
|
};
|
|
50
167
|
}
|
|
168
|
+
async function projectLinkTargetRequiredError(context, projects) {
|
|
169
|
+
const suggestedName = await inferTargetName(context.runtime.cwd);
|
|
170
|
+
const createCommand = `prisma-cli project create ${formatCommandArgument(suggestedName.name)}`;
|
|
171
|
+
const recoveryCommands = ["prisma-cli project link <id-or-name>", createCommand];
|
|
172
|
+
return new CliError({
|
|
173
|
+
code: "PROJECT_LINK_TARGET_REQUIRED",
|
|
174
|
+
domain: "project",
|
|
175
|
+
summary: "Choose a Project to link this directory",
|
|
176
|
+
why: "This directory is not linked to a Prisma Project. Existing Projects are candidates until the user chooses one, and package or directory names are suggestions only.",
|
|
177
|
+
fix: "Run prisma-cli project link in a TTY to choose from the setup list, pass a Project id or name, or create a new Project.",
|
|
178
|
+
meta: {
|
|
179
|
+
suggestedProjectName: suggestedName.name,
|
|
180
|
+
suggestedProjectNameSource: suggestedName.source,
|
|
181
|
+
candidates: sortProjects(projects).map(toProjectSummary),
|
|
182
|
+
recoveryCommands
|
|
183
|
+
},
|
|
184
|
+
exitCode: 2,
|
|
185
|
+
nextSteps: ["prisma-cli project list", ...recoveryCommands],
|
|
186
|
+
nextActions: buildProjectSetupNextActions({
|
|
187
|
+
suggestedProjectName: suggestedName.name,
|
|
188
|
+
createCommand,
|
|
189
|
+
reason: "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections."
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
}
|
|
51
193
|
async function runGitConnect(context, gitUrl, options = {}) {
|
|
52
194
|
const workspace = (await requireAuthenticatedAuthState(context)).workspace;
|
|
53
195
|
if (!workspace) throw workspaceRequiredError();
|
|
54
196
|
if (isRealMode(context)) {
|
|
55
197
|
const client = await requireComputeAuth(context.runtime.env);
|
|
56
198
|
if (!client) throw authRequiredError();
|
|
57
|
-
const target = await
|
|
199
|
+
const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git connect");
|
|
58
200
|
const repository = await resolveRepositoryForConnect(context, gitUrl);
|
|
59
201
|
const api = client;
|
|
60
202
|
const existing = await readFirstSourceRepository(api, target.project.id);
|
|
@@ -89,7 +231,7 @@ async function runGitConnect(context, gitUrl, options = {}) {
|
|
|
89
231
|
nextSteps: []
|
|
90
232
|
};
|
|
91
233
|
}
|
|
92
|
-
const target = await
|
|
234
|
+
const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git connect");
|
|
93
235
|
const repository = await resolveRepositoryForConnect(context, gitUrl);
|
|
94
236
|
const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
|
|
95
237
|
if (existingConnection) {
|
|
@@ -122,7 +264,7 @@ async function runGitDisconnect(context, options = {}) {
|
|
|
122
264
|
if (isRealMode(context)) {
|
|
123
265
|
const client = await requireComputeAuth(context.runtime.env);
|
|
124
266
|
if (!client) throw authRequiredError();
|
|
125
|
-
const target = await
|
|
267
|
+
const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git disconnect");
|
|
126
268
|
const api = client;
|
|
127
269
|
const existing = await readFirstSourceRepository(api, target.project.id);
|
|
128
270
|
if (!existing) throw repoNotConnectedError();
|
|
@@ -138,7 +280,7 @@ async function runGitDisconnect(context, options = {}) {
|
|
|
138
280
|
nextSteps: []
|
|
139
281
|
};
|
|
140
282
|
}
|
|
141
|
-
const target = await
|
|
283
|
+
const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git disconnect");
|
|
142
284
|
const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
|
|
143
285
|
if (!existingConnection) throw repoNotConnectedError();
|
|
144
286
|
await context.stateStore.clearRepositoryConnection(target.project.id);
|
|
@@ -153,6 +295,17 @@ async function runGitDisconnect(context, options = {}) {
|
|
|
153
295
|
};
|
|
154
296
|
}
|
|
155
297
|
async function resolveProjectShowInRealMode(context, workspace, explicitProject) {
|
|
298
|
+
const client = await requireComputeAuth(context.runtime.env);
|
|
299
|
+
if (!client) throw authRequiredError();
|
|
300
|
+
return inspectProjectBinding({
|
|
301
|
+
context,
|
|
302
|
+
workspace,
|
|
303
|
+
explicitProject,
|
|
304
|
+
listProjects: () => listRealWorkspaceProjects(client, workspace),
|
|
305
|
+
commandName: "project show"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function resolveRequiredProjectInRealMode(context, workspace, explicitProject, commandName) {
|
|
156
309
|
const client = await requireComputeAuth(context.runtime.env);
|
|
157
310
|
if (!client) throw authRequiredError();
|
|
158
311
|
return resolveProjectTarget({
|
|
@@ -160,16 +313,25 @@ async function resolveProjectShowInRealMode(context, workspace, explicitProject)
|
|
|
160
313
|
workspace,
|
|
161
314
|
explicitProject,
|
|
162
315
|
listProjects: () => listRealWorkspaceProjects(client, workspace),
|
|
163
|
-
|
|
316
|
+
commandName
|
|
164
317
|
});
|
|
165
318
|
}
|
|
166
319
|
async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
|
|
320
|
+
return inspectProjectBinding({
|
|
321
|
+
context,
|
|
322
|
+
workspace,
|
|
323
|
+
explicitProject,
|
|
324
|
+
listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
|
|
325
|
+
commandName: "project show"
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function resolveRequiredProjectInFixtureMode(context, workspace, explicitProject, commandName) {
|
|
167
329
|
return resolveProjectTarget({
|
|
168
330
|
context,
|
|
169
331
|
workspace,
|
|
170
332
|
explicitProject,
|
|
171
333
|
listProjects: async () => listFixtureWorkspaceProjects(context, workspace),
|
|
172
|
-
|
|
334
|
+
commandName
|
|
173
335
|
});
|
|
174
336
|
}
|
|
175
337
|
async function listRealWorkspaceProjects(client, workspace) {
|
|
@@ -494,11 +656,5 @@ function repoConnectionFixForStatus(status) {
|
|
|
494
656
|
if (status === 422) return "Make sure the GitHub App installation has access to this repository.";
|
|
495
657
|
return "Re-run with --trace for the underlying API response details.";
|
|
496
658
|
}
|
|
497
|
-
function toProjectSummary(project) {
|
|
498
|
-
return {
|
|
499
|
-
id: project.id,
|
|
500
|
-
name: project.name
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
659
|
//#endregion
|
|
504
|
-
export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectList, runProjectShow };
|
|
660
|
+
export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectCreate, runProjectLink, runProjectList, runProjectShow };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/lib/app/domain-guidance.ts
|
|
2
|
+
function formatDomainFailureFix(domain) {
|
|
3
|
+
if (domain.status !== "failed") return null;
|
|
4
|
+
const dnsRecord = domain.dnsRecords[0];
|
|
5
|
+
if (domain.failureCategory === "dns") {
|
|
6
|
+
if (dnsRecord) return `Add ${dnsRecord.type} ${dnsRecord.name} -> ${dnsRecord.value}, then run prisma-cli app domain retry ${domain.hostname}.`;
|
|
7
|
+
return `DNS verification failed, but the platform did not return a DNS record. Run prisma-cli app domain show ${domain.hostname} later, then retry when the DNS target is available.`;
|
|
8
|
+
}
|
|
9
|
+
if (domain.failureCategory === "acme") return `Retry TLS issuance with prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
|
|
10
|
+
if (domain.failureCategory === "storage") return `Retry provisioning with prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
|
|
11
|
+
return `Run prisma-cli app domain retry ${domain.hostname}. Contact support if it fails again.`;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { formatDomainFailureFix };
|
|
@@ -3,10 +3,11 @@ import { usageError } from "../../shell/errors.js";
|
|
|
3
3
|
const VALID_ROLES = new Set(["production", "preview"]);
|
|
4
4
|
function positionalHint(command) {
|
|
5
5
|
if (command === "add" || command === "update") return "KEY=value ";
|
|
6
|
-
if (command === "
|
|
6
|
+
if (command === "remove") return "KEY ";
|
|
7
7
|
return "";
|
|
8
8
|
}
|
|
9
9
|
function resolveEnvScope(flags, options) {
|
|
10
|
+
if (flags.roleName && flags.branchName) throw usageError(`prisma-cli project env ${options.command} accepts either --role or --branch`, "--role targets a project-level config map; --branch targets a preview branch override.", "Pass exactly one scope flag.", [`prisma-cli project env ${options.command} ${positionalHint(options.command)}--role preview`, `prisma-cli project env ${options.command} ${positionalHint(options.command)}--branch feature/foo`], "app");
|
|
10
11
|
if (flags.roleName) {
|
|
11
12
|
if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
|
|
12
13
|
return {
|
|
@@ -14,9 +15,17 @@ function resolveEnvScope(flags, options) {
|
|
|
14
15
|
role: flags.roleName
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
if (flags.branchName) return {
|
|
19
|
+
kind: "branch",
|
|
20
|
+
branchName: flags.branchName
|
|
21
|
+
};
|
|
17
22
|
if (options.requireExplicit) {
|
|
18
23
|
const positional = positionalHint(options.command);
|
|
19
|
-
throw usageError(`prisma-cli project env ${options.command} requires --role`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production
|
|
24
|
+
throw usageError(`prisma-cli project env ${options.command} requires --role or --branch`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
25
|
+
`prisma-cli project env ${options.command} ${positional}--role production`,
|
|
26
|
+
`prisma-cli project env ${options.command} ${positional}--role preview`,
|
|
27
|
+
`prisma-cli project env ${options.command} ${positional}--branch feature/foo`
|
|
28
|
+
], "app");
|
|
20
29
|
}
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
@@ -38,7 +47,7 @@ function parseKeyValuePositional(raw, command, env = process.env) {
|
|
|
38
47
|
const key = raw.slice(0, separatorIndex);
|
|
39
48
|
const value = raw.slice(separatorIndex + 1);
|
|
40
49
|
validateKey(key, command);
|
|
41
|
-
if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env
|
|
50
|
+
if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env remove to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
|
|
42
51
|
return {
|
|
43
52
|
key,
|
|
44
53
|
value
|
|
@@ -46,12 +55,13 @@ function parseKeyValuePositional(raw, command, env = process.env) {
|
|
|
46
55
|
}
|
|
47
56
|
const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
|
|
48
57
|
function validateKey(key, command) {
|
|
49
|
-
if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "
|
|
58
|
+
if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
|
|
50
59
|
if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
|
|
51
|
-
if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "
|
|
60
|
+
if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
|
|
52
61
|
}
|
|
53
62
|
function formatScopeLabel(scope) {
|
|
54
|
-
return scope.role;
|
|
63
|
+
if (scope.kind === "role") return scope.role;
|
|
64
|
+
return `branch:${scope.branchName}`;
|
|
55
65
|
}
|
|
56
66
|
//#endregion
|
|
57
67
|
export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
|
|
@@ -46,7 +46,7 @@ async function executePreviewBuild(options) {
|
|
|
46
46
|
});
|
|
47
47
|
const artifact = await strategy.execute();
|
|
48
48
|
try {
|
|
49
|
-
if (buildType === "nextjs") await restageNextjsArtifact(artifact
|
|
49
|
+
if (buildType === "nextjs") await restageNextjsArtifact(artifact, options.appPath);
|
|
50
50
|
await normalizeArtifactSymlinks(artifact.directory, options.appPath);
|
|
51
51
|
return {
|
|
52
52
|
artifact,
|
|
@@ -114,8 +114,10 @@ async function stageNextjsStandaloneArtifact(options) {
|
|
|
114
114
|
appRoot,
|
|
115
115
|
sourceRoot: await resolveSourceRoot(appRoot)
|
|
116
116
|
});
|
|
117
|
+
await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
|
|
117
118
|
}
|
|
118
|
-
async function restageNextjsArtifact(
|
|
119
|
+
async function restageNextjsArtifact(artifact, appPath) {
|
|
120
|
+
const artifactDir = artifact.directory;
|
|
119
121
|
const standaloneDir = path.join(appPath, ".next", "standalone");
|
|
120
122
|
await rm(artifactDir, {
|
|
121
123
|
recursive: true,
|
|
@@ -126,17 +128,52 @@ async function restageNextjsArtifact(artifactDir, appPath) {
|
|
|
126
128
|
artifactDir,
|
|
127
129
|
appPath
|
|
128
130
|
});
|
|
131
|
+
const serverSubpath = nextjsServerSubpath(artifact.entrypoint);
|
|
132
|
+
const serverDir = serverSubpath ? path.join(artifactDir, serverSubpath) : artifactDir;
|
|
129
133
|
const publicDir = path.join(appPath, "public");
|
|
130
|
-
if (await directoryExists(publicDir)) await cp(publicDir, path.join(
|
|
134
|
+
if (await directoryExists(publicDir)) await cp(publicDir, path.join(serverDir, "public"), {
|
|
131
135
|
recursive: true,
|
|
132
136
|
verbatimSymlinks: true
|
|
133
137
|
});
|
|
134
138
|
const staticDir = path.join(appPath, ".next", "static");
|
|
135
|
-
if (await directoryExists(staticDir)) await cp(staticDir, path.join(
|
|
139
|
+
if (await directoryExists(staticDir)) await cp(staticDir, path.join(serverDir, ".next", "static"), {
|
|
136
140
|
recursive: true,
|
|
137
141
|
verbatimSymlinks: true
|
|
138
142
|
});
|
|
139
143
|
}
|
|
144
|
+
function nextjsServerSubpath(entrypoint) {
|
|
145
|
+
const dir = path.posix.dirname(entrypoint);
|
|
146
|
+
return dir === "." ? "" : dir;
|
|
147
|
+
}
|
|
148
|
+
async function hoistPnpmDependencies(nodeModulesDir) {
|
|
149
|
+
const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
|
|
150
|
+
if (!await directoryExists(pnpmNodeModulesDir)) return;
|
|
151
|
+
const entries = await readdir(pnpmNodeModulesDir, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
|
|
154
|
+
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
155
|
+
const scopedEntries = await readdir(sourcePath, { withFileTypes: true });
|
|
156
|
+
for (const scopedEntry of scopedEntries) {
|
|
157
|
+
const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name);
|
|
158
|
+
if (await pathExists(scopedDestination)) continue;
|
|
159
|
+
await mkdir(path.dirname(scopedDestination), { recursive: true });
|
|
160
|
+
await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
|
|
161
|
+
standaloneRoot: pnpmNodeModulesDir,
|
|
162
|
+
appRoot: nodeModulesDir,
|
|
163
|
+
sourceRoot: nodeModulesDir
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const destinationPath = path.join(nodeModulesDir, entry.name);
|
|
169
|
+
if (await pathExists(destinationPath)) continue;
|
|
170
|
+
await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
|
|
171
|
+
standaloneRoot: pnpmNodeModulesDir,
|
|
172
|
+
appRoot: nodeModulesDir,
|
|
173
|
+
sourceRoot: nodeModulesDir
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
140
177
|
async function normalizeArtifactSymlinks(artifactDir, appPath) {
|
|
141
178
|
const normalizedArtifactDir = path.resolve(artifactDir);
|
|
142
179
|
const normalizedAppPath = path.resolve(appPath);
|
|
@@ -174,7 +211,9 @@ function isPathWithin(rootPath, candidatePath) {
|
|
|
174
211
|
async function copyPathMaterializingSymlinks(sourcePath, destinationPath, options) {
|
|
175
212
|
const sourceStat = await lstat(sourcePath);
|
|
176
213
|
if (sourceStat.isSymbolicLink()) {
|
|
177
|
-
|
|
214
|
+
const resolvedTarget = await resolveSymlinkTarget(sourcePath, options);
|
|
215
|
+
if (resolvedTarget === null) return;
|
|
216
|
+
await copyPathMaterializingSymlinks(resolvedTarget, destinationPath, options);
|
|
178
217
|
return;
|
|
179
218
|
}
|
|
180
219
|
if (sourceStat.isDirectory()) {
|
|
@@ -200,8 +239,14 @@ async function resolveSymlinkTarget(symlinkPath, options) {
|
|
|
200
239
|
const fallbackTarget = path.join(options.appRoot, path.relative(options.standaloneRoot, resolvedTarget));
|
|
201
240
|
if (await pathExists(fallbackTarget)) return fallbackTarget;
|
|
202
241
|
}
|
|
242
|
+
if (isPnpmHoistLink(symlinkPath)) return null;
|
|
203
243
|
throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
|
|
204
244
|
}
|
|
245
|
+
function isPnpmHoistLink(symlinkPath) {
|
|
246
|
+
const parts = path.dirname(symlinkPath).split(path.sep);
|
|
247
|
+
for (let i = 0; i < parts.length - 1; i++) if (parts[i] === ".pnpm" && parts[i + 1] === "node_modules") return true;
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
205
250
|
async function pathExists(targetPath) {
|
|
206
251
|
try {
|
|
207
252
|
await stat(targetPath);
|
|
@@ -96,29 +96,5 @@ function createPreviewPromoteProgress(output, enabled) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
function createPreviewUpdateEnvProgress(output, enabled) {
|
|
100
|
-
if (!enabled) return;
|
|
101
|
-
const write = (line) => {
|
|
102
|
-
output.write(`${line}\n`);
|
|
103
|
-
};
|
|
104
|
-
return {
|
|
105
|
-
onVersionCreated(versionId) {
|
|
106
|
-
write(`Creating updated deployment ${versionId}...`);
|
|
107
|
-
},
|
|
108
|
-
onStartRequested() {
|
|
109
|
-
write("Starting deployment...");
|
|
110
|
-
},
|
|
111
|
-
onStatusChange(status) {
|
|
112
|
-
write(`Status: ${status}`);
|
|
113
|
-
},
|
|
114
|
-
onRunning(url) {
|
|
115
|
-
if (url) {
|
|
116
|
-
write(`Deployment is running at ${url}.`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
write("Deployment is running.");
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
99
|
//#endregion
|
|
124
|
-
export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress
|
|
100
|
+
export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress };
|
|
@@ -3,6 +3,18 @@ import { PreviewBuildStrategy } from "./preview-build.js";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
|
|
5
5
|
//#region src/lib/app/preview-provider.ts
|
|
6
|
+
var PreviewDomainApiError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
code;
|
|
9
|
+
hint;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(`${options.summary}: ${options.message}${options.hint ? ` ${options.hint}` : ""}`);
|
|
12
|
+
this.name = "PreviewDomainApiError";
|
|
13
|
+
this.status = options.status;
|
|
14
|
+
this.code = options.code ?? null;
|
|
15
|
+
this.hint = options.hint ?? null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
6
18
|
function createPreviewAppProvider(client, options) {
|
|
7
19
|
const sdk = new ComputeClient(client);
|
|
8
20
|
return {
|
|
@@ -35,6 +47,43 @@ function createPreviewAppProvider(client, options) {
|
|
|
35
47
|
name: appResult.value.name
|
|
36
48
|
};
|
|
37
49
|
},
|
|
50
|
+
async listDomains(appId) {
|
|
51
|
+
return listComputeServiceDomains(client, appId);
|
|
52
|
+
},
|
|
53
|
+
async addDomain(options) {
|
|
54
|
+
const result = await client.POST("/v1/compute-services/{computeServiceId}/domains", {
|
|
55
|
+
params: { path: { computeServiceId: options.appId } },
|
|
56
|
+
body: { hostname: options.hostname }
|
|
57
|
+
});
|
|
58
|
+
if (result.error || !result.data) {
|
|
59
|
+
if (result.response.status === 409) {
|
|
60
|
+
const existing = (await listComputeServiceDomains(client, options.appId)).find((domain) => sameHostname(domain.hostname, options.hostname));
|
|
61
|
+
if (existing) return {
|
|
62
|
+
domain: existing,
|
|
63
|
+
existing: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
throw domainApiCallError("Failed to add custom domain", result.response, result.error);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
domain: normalizeDomainRecord(result.data.data),
|
|
70
|
+
existing: false
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
async showDomain(domainId) {
|
|
74
|
+
const result = await client.GET("/v1/domains/{domainId}", { params: { path: { domainId } } });
|
|
75
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to show custom domain", result.response, result.error);
|
|
76
|
+
return normalizeDomainRecord(result.data.data);
|
|
77
|
+
},
|
|
78
|
+
async removeDomain(domainId) {
|
|
79
|
+
const result = await client.DELETE("/v1/domains/{domainId}", { params: { path: { domainId } } });
|
|
80
|
+
if (result.error) throw domainApiCallError("Failed to remove custom domain", result.response, result.error);
|
|
81
|
+
},
|
|
82
|
+
async retryDomain(domainId) {
|
|
83
|
+
const result = await client.POST("/v1/domains/{domainId}/retry", { params: { path: { domainId } } });
|
|
84
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to retry custom domain", result.response, result.error);
|
|
85
|
+
return normalizeDomainRecord(result.data.data);
|
|
86
|
+
},
|
|
38
87
|
async promoteDeployment(options) {
|
|
39
88
|
const promoteResult = await sdk.promote({
|
|
40
89
|
serviceId: options.appId,
|
|
@@ -263,6 +312,46 @@ async function listComputeServices(client, options) {
|
|
|
263
312
|
liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
|
|
264
313
|
}));
|
|
265
314
|
}
|
|
315
|
+
async function listComputeServiceDomains(client, computeServiceId) {
|
|
316
|
+
const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", { params: { path: { computeServiceId } } });
|
|
317
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to list custom domains", result.response, result.error);
|
|
318
|
+
return result.data.data.map((domain) => normalizeDomainRecord(domain));
|
|
319
|
+
}
|
|
320
|
+
function normalizeDomainRecord(domain) {
|
|
321
|
+
return {
|
|
322
|
+
id: domain.id,
|
|
323
|
+
type: domain.type,
|
|
324
|
+
url: domain.url,
|
|
325
|
+
hostname: domain.hostname,
|
|
326
|
+
computeServiceId: domain.computeServiceId,
|
|
327
|
+
status: domain.status,
|
|
328
|
+
foundryStatus: domain.foundryStatus,
|
|
329
|
+
failureReason: domain.failureReason,
|
|
330
|
+
failureCategory: domain.failureCategory,
|
|
331
|
+
certExpiresAt: domain.certExpiresAt,
|
|
332
|
+
createdAt: domain.createdAt,
|
|
333
|
+
updatedAt: domain.updatedAt,
|
|
334
|
+
dnsRecords: normalizeDomainDnsRecords(domain.dnsRecords)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function normalizeDomainDnsRecords(records) {
|
|
338
|
+
if (!Array.isArray(records)) return [];
|
|
339
|
+
return records.map((record) => {
|
|
340
|
+
if (typeof record.type !== "string" || typeof record.name !== "string" || typeof record.value !== "string") return null;
|
|
341
|
+
return {
|
|
342
|
+
type: record.type,
|
|
343
|
+
name: record.name,
|
|
344
|
+
value: record.value,
|
|
345
|
+
ttl: typeof record.ttl === "number" ? record.ttl : null
|
|
346
|
+
};
|
|
347
|
+
}).filter((record) => Boolean(record));
|
|
348
|
+
}
|
|
349
|
+
function sameHostname(left, right) {
|
|
350
|
+
return normalizeHostnameForComparison(left) === normalizeHostnameForComparison(right);
|
|
351
|
+
}
|
|
352
|
+
function normalizeHostnameForComparison(hostname) {
|
|
353
|
+
return hostname.trim().replace(/\.$/, "").toLowerCase();
|
|
354
|
+
}
|
|
266
355
|
async function createBranchApp(client, options) {
|
|
267
356
|
const branch = await resolveOrCreateBranch(client, {
|
|
268
357
|
projectId: options.projectId,
|
|
@@ -301,6 +390,15 @@ function apiCallError(summary, response, error) {
|
|
|
301
390
|
const hint = error.error?.hint ? ` ${error.error.hint}` : "";
|
|
302
391
|
return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
|
|
303
392
|
}
|
|
393
|
+
function domainApiCallError(summary, response, error) {
|
|
394
|
+
return new PreviewDomainApiError({
|
|
395
|
+
summary,
|
|
396
|
+
status: response.status,
|
|
397
|
+
code: error.error?.code ?? null,
|
|
398
|
+
message: error.error?.message ?? `Management API returned HTTP ${response.status}.`,
|
|
399
|
+
hint: error.error?.hint ?? null
|
|
400
|
+
});
|
|
401
|
+
}
|
|
304
402
|
async function findAppForDeployment(sdk, deploymentId) {
|
|
305
403
|
const projectsResult = await sdk.listProjects();
|
|
306
404
|
if (projectsResult.isErr()) throw new Error(projectsResult.error.message);
|
|
@@ -330,4 +428,4 @@ function toAbsoluteUrl(url) {
|
|
|
330
428
|
return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
|
|
331
429
|
}
|
|
332
430
|
//#endregion
|
|
333
|
-
export { createPreviewAppProvider };
|
|
431
|
+
export { PreviewDomainApiError, createPreviewAppProvider };
|