@prisma/cli 3.0.0-alpha.13 → 3.0.0-alpha.14
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/dist/commands/app/index.js +116 -2
- package/dist/controllers/app.js +426 -2
- package/dist/lib/app/domain-guidance.js +14 -0
- package/dist/lib/app/preview-provider.js +99 -1
- package/dist/presenters/app.js +172 -1
- package/dist/shell/command-meta.js +69 -0
- package/package.json +2 -2
|
@@ -2,8 +2,8 @@ import { attachCommandDescriptor } from "../../shell/command-meta.js";
|
|
|
2
2
|
import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
|
|
3
3
|
import { configureRuntimeCommand } from "../../shell/runtime.js";
|
|
4
4
|
import { PREVIEW_BUILD_TYPES } from "../../lib/app/preview-build.js";
|
|
5
|
-
import { runAppBuild, runAppDeploy, runAppListDeploys, runAppListEnv, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy, runAppUpdateEnv } from "../../controllers/app.js";
|
|
6
|
-
import { renderAppBuild, renderAppDeploy, renderAppListDeploys, renderAppListEnv, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, renderAppUpdateEnv, serializeAppBuild, serializeAppDeploy, serializeAppListDeploys, serializeAppListEnv, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy, serializeAppUpdateEnv } from "../../presenters/app.js";
|
|
5
|
+
import { runAppBuild, runAppDeploy, runAppDomainAdd, runAppDomainRemove, runAppDomainRetry, runAppDomainShow, runAppDomainWait, runAppListDeploys, runAppListEnv, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy, runAppUpdateEnv } from "../../controllers/app.js";
|
|
6
|
+
import { renderAppBuild, renderAppDeploy, renderAppDomainAdd, renderAppDomainRemove, renderAppDomainRetry, renderAppDomainShow, renderAppListDeploys, renderAppListEnv, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, renderAppUpdateEnv, serializeAppBuild, serializeAppDeploy, serializeAppDomainAdd, serializeAppDomainRemove, serializeAppDomainRetry, serializeAppDomainShow, serializeAppListDeploys, serializeAppListEnv, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy, serializeAppUpdateEnv } from "../../presenters/app.js";
|
|
7
7
|
import { runCommand, runStreamingCommand } from "../../shell/command-runner.js";
|
|
8
8
|
import { Command, Option } from "commander";
|
|
9
9
|
//#region src/commands/app/index.ts
|
|
@@ -17,6 +17,7 @@ function createAppCommand(runtime) {
|
|
|
17
17
|
app.addCommand(createListEnvCommand(runtime));
|
|
18
18
|
app.addCommand(createShowCommand(runtime));
|
|
19
19
|
app.addCommand(createOpenCommand(runtime));
|
|
20
|
+
app.addCommand(createDomainCommand(runtime));
|
|
20
21
|
app.addCommand(createLogsCommand(runtime));
|
|
21
22
|
app.addCommand(createListDeploysCommand(runtime));
|
|
22
23
|
app.addCommand(createShowDeployCommand(runtime));
|
|
@@ -147,6 +148,119 @@ function createOpenCommand(runtime) {
|
|
|
147
148
|
});
|
|
148
149
|
return command;
|
|
149
150
|
}
|
|
151
|
+
function createDomainCommand(runtime) {
|
|
152
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("domain"), runtime), "app.domain");
|
|
153
|
+
addCompactGlobalFlags(command);
|
|
154
|
+
command.addCommand(createDomainAddCommand(runtime));
|
|
155
|
+
command.addCommand(createDomainShowCommand(runtime));
|
|
156
|
+
command.addCommand(createDomainRemoveCommand(runtime));
|
|
157
|
+
command.addCommand(createDomainRetryCommand(runtime));
|
|
158
|
+
command.addCommand(createDomainWaitCommand(runtime));
|
|
159
|
+
return command;
|
|
160
|
+
}
|
|
161
|
+
function addDomainTargetOptions(command) {
|
|
162
|
+
return command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name")).addOption(new Option("--branch <name>", "Branch name"));
|
|
163
|
+
}
|
|
164
|
+
function createDomainAddCommand(runtime) {
|
|
165
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("add"), runtime), "app.domain.add");
|
|
166
|
+
command.argument("<hostname>", "Custom domain hostname");
|
|
167
|
+
addDomainTargetOptions(command);
|
|
168
|
+
addGlobalFlags(command);
|
|
169
|
+
command.action(async (hostname, options) => {
|
|
170
|
+
const appName = options.app;
|
|
171
|
+
const projectRef = options.project;
|
|
172
|
+
const branchName = options.branch;
|
|
173
|
+
await runCommand(runtime, "app.domain.add", options, (context) => runAppDomainAdd(context, hostname, {
|
|
174
|
+
appName,
|
|
175
|
+
projectRef,
|
|
176
|
+
branchName
|
|
177
|
+
}), {
|
|
178
|
+
renderHuman: (context, descriptor, result) => renderAppDomainAdd(context, descriptor, result),
|
|
179
|
+
renderJson: (result) => serializeAppDomainAdd(result)
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
return command;
|
|
183
|
+
}
|
|
184
|
+
function createDomainShowCommand(runtime) {
|
|
185
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "app.domain.show");
|
|
186
|
+
command.argument("<hostname>", "Custom domain hostname");
|
|
187
|
+
addDomainTargetOptions(command);
|
|
188
|
+
addGlobalFlags(command);
|
|
189
|
+
command.action(async (hostname, options) => {
|
|
190
|
+
const appName = options.app;
|
|
191
|
+
const projectRef = options.project;
|
|
192
|
+
const branchName = options.branch;
|
|
193
|
+
await runCommand(runtime, "app.domain.show", options, (context) => runAppDomainShow(context, hostname, {
|
|
194
|
+
appName,
|
|
195
|
+
projectRef,
|
|
196
|
+
branchName
|
|
197
|
+
}), {
|
|
198
|
+
renderHuman: (context, descriptor, result) => renderAppDomainShow(context, descriptor, result),
|
|
199
|
+
renderJson: (result) => serializeAppDomainShow(result)
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
return command;
|
|
203
|
+
}
|
|
204
|
+
function createDomainRemoveCommand(runtime) {
|
|
205
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("remove"), runtime), "app.domain.remove");
|
|
206
|
+
command.argument("<hostname>", "Custom domain hostname");
|
|
207
|
+
addDomainTargetOptions(command);
|
|
208
|
+
addGlobalFlags(command);
|
|
209
|
+
command.action(async (hostname, options) => {
|
|
210
|
+
const appName = options.app;
|
|
211
|
+
const projectRef = options.project;
|
|
212
|
+
const branchName = options.branch;
|
|
213
|
+
await runCommand(runtime, "app.domain.remove", options, (context) => runAppDomainRemove(context, hostname, {
|
|
214
|
+
appName,
|
|
215
|
+
projectRef,
|
|
216
|
+
branchName
|
|
217
|
+
}), {
|
|
218
|
+
renderHuman: (context, descriptor, result) => renderAppDomainRemove(context, descriptor, result),
|
|
219
|
+
renderJson: (result) => serializeAppDomainRemove(result)
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return command;
|
|
223
|
+
}
|
|
224
|
+
function createDomainRetryCommand(runtime) {
|
|
225
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("retry"), runtime), "app.domain.retry");
|
|
226
|
+
command.argument("<hostname>", "Custom domain hostname");
|
|
227
|
+
addDomainTargetOptions(command);
|
|
228
|
+
addGlobalFlags(command);
|
|
229
|
+
command.action(async (hostname, options) => {
|
|
230
|
+
const appName = options.app;
|
|
231
|
+
const projectRef = options.project;
|
|
232
|
+
const branchName = options.branch;
|
|
233
|
+
await runCommand(runtime, "app.domain.retry", options, (context) => runAppDomainRetry(context, hostname, {
|
|
234
|
+
appName,
|
|
235
|
+
projectRef,
|
|
236
|
+
branchName
|
|
237
|
+
}), {
|
|
238
|
+
renderHuman: (context, descriptor, result) => renderAppDomainRetry(context, descriptor, result),
|
|
239
|
+
renderJson: (result) => serializeAppDomainRetry(result)
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return command;
|
|
243
|
+
}
|
|
244
|
+
function createDomainWaitCommand(runtime) {
|
|
245
|
+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("wait"), runtime), "app.domain.wait");
|
|
246
|
+
command.argument("<hostname>", "Custom domain hostname");
|
|
247
|
+
addDomainTargetOptions(command);
|
|
248
|
+
command.addOption(new Option("--timeout <duration>", "Maximum time to wait").default("15m"));
|
|
249
|
+
addGlobalFlags(command);
|
|
250
|
+
command.action(async (hostname, options) => {
|
|
251
|
+
const appName = options.app;
|
|
252
|
+
const projectRef = options.project;
|
|
253
|
+
const branchName = options.branch;
|
|
254
|
+
const timeout = options.timeout;
|
|
255
|
+
await runStreamingCommand(runtime, "app.domain.wait", options, (context) => runAppDomainWait(context, hostname, {
|
|
256
|
+
appName,
|
|
257
|
+
projectRef,
|
|
258
|
+
branchName,
|
|
259
|
+
timeout
|
|
260
|
+
}));
|
|
261
|
+
});
|
|
262
|
+
return command;
|
|
263
|
+
}
|
|
150
264
|
function createLogsCommand(runtime) {
|
|
151
265
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("logs"), runtime), "app.logs");
|
|
152
266
|
command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name")).addOption(new Option("--deployment <id>", "Deployment id"));
|
package/dist/controllers/app.js
CHANGED
|
@@ -16,7 +16,8 @@ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore,
|
|
|
16
16
|
import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
|
|
17
17
|
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
|
|
18
18
|
import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress } from "../lib/app/preview-progress.js";
|
|
19
|
-
import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
19
|
+
import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
|
|
20
|
+
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
20
21
|
import { createSelectPromptPort } from "./select-prompt-port.js";
|
|
21
22
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
22
23
|
import { listRealWorkspaceProjects } from "./project.js";
|
|
@@ -479,6 +480,132 @@ async function runAppOpen(context, appName, projectRef) {
|
|
|
479
480
|
nextSteps: ["prisma-cli app show", `prisma-cli app show-deploy ${liveDeployment.id}`]
|
|
480
481
|
};
|
|
481
482
|
}
|
|
483
|
+
async function runAppDomainAdd(context, hostname, options) {
|
|
484
|
+
const normalizedHostname = normalizeDomainHostname(hostname);
|
|
485
|
+
const target = await resolveAppDomainTarget(context, options);
|
|
486
|
+
const added = await target.provider.addDomain({
|
|
487
|
+
appId: target.app.id,
|
|
488
|
+
hostname: normalizedHostname
|
|
489
|
+
}).catch((error) => {
|
|
490
|
+
throw domainCommandError("add", error, normalizedHostname);
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
command: "app.domain.add",
|
|
494
|
+
result: {
|
|
495
|
+
...target.resultTarget,
|
|
496
|
+
domain: toAppDomainSummary(added.domain),
|
|
497
|
+
existing: added.existing
|
|
498
|
+
},
|
|
499
|
+
warnings: [],
|
|
500
|
+
nextSteps: [`prisma-cli app domain wait ${normalizedHostname}`, `prisma-cli app domain show ${normalizedHostname}`]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function runAppDomainShow(context, hostname, options) {
|
|
504
|
+
const normalizedHostname = normalizeDomainHostname(hostname);
|
|
505
|
+
const target = await resolveAppDomainTarget(context, options);
|
|
506
|
+
const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "show");
|
|
507
|
+
const detail = await target.provider.showDomain(domain.id).catch((error) => {
|
|
508
|
+
throw domainCommandError("show", error, normalizedHostname);
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
command: "app.domain.show",
|
|
512
|
+
result: {
|
|
513
|
+
...target.resultTarget,
|
|
514
|
+
domain: toAppDomainSummary(detail)
|
|
515
|
+
},
|
|
516
|
+
warnings: [],
|
|
517
|
+
nextSteps: buildDomainShowNextSteps(detail)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function runAppDomainRemove(context, hostname, options) {
|
|
521
|
+
const normalizedHostname = normalizeDomainHostname(hostname);
|
|
522
|
+
const target = await resolveAppDomainTarget(context, options);
|
|
523
|
+
const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "remove");
|
|
524
|
+
await confirmDomainRemoval(context, target.resultTarget, normalizedHostname);
|
|
525
|
+
await target.provider.removeDomain(domain.id).catch((error) => {
|
|
526
|
+
throw domainCommandError("remove", error, normalizedHostname);
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
command: "app.domain.remove",
|
|
530
|
+
result: {
|
|
531
|
+
...target.resultTarget,
|
|
532
|
+
hostname: normalizedHostname,
|
|
533
|
+
removed: true
|
|
534
|
+
},
|
|
535
|
+
warnings: [],
|
|
536
|
+
nextSteps: []
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function runAppDomainRetry(context, hostname, options) {
|
|
540
|
+
const normalizedHostname = normalizeDomainHostname(hostname);
|
|
541
|
+
const target = await resolveAppDomainTarget(context, options);
|
|
542
|
+
const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "retry");
|
|
543
|
+
const retried = await target.provider.retryDomain(domain.id).catch((error) => {
|
|
544
|
+
throw domainCommandError("retry", error, normalizedHostname);
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
command: "app.domain.retry",
|
|
548
|
+
result: {
|
|
549
|
+
...target.resultTarget,
|
|
550
|
+
domain: toAppDomainSummary(retried)
|
|
551
|
+
},
|
|
552
|
+
warnings: [],
|
|
553
|
+
nextSteps: [`prisma-cli app domain wait ${normalizedHostname}`]
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async function runAppDomainWait(context, hostname, options) {
|
|
557
|
+
const normalizedHostname = normalizeDomainHostname(hostname);
|
|
558
|
+
const timeoutMs = parseDomainWaitTimeout(options?.timeout);
|
|
559
|
+
const target = await resolveAppDomainTarget(context, options);
|
|
560
|
+
const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "wait");
|
|
561
|
+
if (!context.flags.json && !context.flags.quiet) context.output.stderr.write([
|
|
562
|
+
`app domain wait -> Waiting for ${normalizedHostname} to become active.`,
|
|
563
|
+
"",
|
|
564
|
+
`Workspace: ${target.resultTarget.workspace.name} Project: ${target.resultTarget.project.name} Branch: ${target.resultTarget.branch.name} App: ${target.resultTarget.app.name}`,
|
|
565
|
+
""
|
|
566
|
+
].join("\n"));
|
|
567
|
+
const start = Date.now();
|
|
568
|
+
const deadline = start + timeoutMs;
|
|
569
|
+
const pollIntervalMs = readDomainWaitPollIntervalMs(context);
|
|
570
|
+
let lastStatus = null;
|
|
571
|
+
let current = domain;
|
|
572
|
+
while (true) {
|
|
573
|
+
emitDomainWaitStatus(context, {
|
|
574
|
+
hostname: normalizedHostname,
|
|
575
|
+
domainId: current.id,
|
|
576
|
+
previousStatus: lastStatus,
|
|
577
|
+
status: current.status,
|
|
578
|
+
elapsedMs: Date.now() - start
|
|
579
|
+
});
|
|
580
|
+
lastStatus = current.status;
|
|
581
|
+
if (current.status === "active") {
|
|
582
|
+
if (!context.flags.json && !context.flags.quiet) context.output.stderr.write(`\n${normalizedHostname} is live at https://${normalizedHostname}\n`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (current.status === "failed") throw new CliError({
|
|
586
|
+
code: "DOMAIN_VERIFICATION_FAILED",
|
|
587
|
+
domain: "app",
|
|
588
|
+
summary: `Custom domain "${normalizedHostname}" failed verification`,
|
|
589
|
+
why: formatDomainFailureWhy(current),
|
|
590
|
+
fix: formatDomainFailureFix(current) ?? `Run prisma-cli app domain retry ${normalizedHostname}.`,
|
|
591
|
+
exitCode: 1,
|
|
592
|
+
nextSteps: [`prisma-cli app domain show ${normalizedHostname}`, `prisma-cli app domain retry ${normalizedHostname}`]
|
|
593
|
+
});
|
|
594
|
+
if (timeoutMs === 0 || Date.now() >= deadline) throw new CliError({
|
|
595
|
+
code: "DOMAIN_VERIFICATION_TIMEOUT",
|
|
596
|
+
domain: "app",
|
|
597
|
+
summary: `Timed out waiting for "${normalizedHostname}" to become active`,
|
|
598
|
+
why: `The domain is still "${current.status}".`,
|
|
599
|
+
fix: `Run prisma-cli app domain show ${normalizedHostname} to inspect the current status, or retry wait with a longer --timeout.`,
|
|
600
|
+
exitCode: 1,
|
|
601
|
+
nextSteps: [`prisma-cli app domain show ${normalizedHostname}`]
|
|
602
|
+
});
|
|
603
|
+
await sleep(Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)));
|
|
604
|
+
current = await target.provider.showDomain(current.id).catch((error) => {
|
|
605
|
+
throw domainCommandError("wait", error, normalizedHostname);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
482
609
|
async function runAppLogs(context, appName, deploymentId, projectRef) {
|
|
483
610
|
ensurePreviewAppMode(context);
|
|
484
611
|
const { provider, target: resolvedTarget, projectId } = await requireProviderAndProjectContext(context, projectRef);
|
|
@@ -708,6 +835,303 @@ async function runAppRemove(context, appName, projectRef) {
|
|
|
708
835
|
nextSteps: ["prisma-cli app deploy", "prisma-cli app list-deploys"]
|
|
709
836
|
};
|
|
710
837
|
}
|
|
838
|
+
async function resolveAppDomainTarget(context, options) {
|
|
839
|
+
ensurePreviewAppMode(context);
|
|
840
|
+
const branch = resolveDomainBranch(options?.branchName);
|
|
841
|
+
if (toBranchKind(branch.name) !== "production") throw new CliError({
|
|
842
|
+
code: "BRANCH_NOT_DEPLOYABLE",
|
|
843
|
+
domain: "branch",
|
|
844
|
+
summary: "Custom domains require the production branch",
|
|
845
|
+
why: `Custom domains on preview branch "${branch.name}" are not supported in Public Beta.`,
|
|
846
|
+
fix: "Use --branch production, or attach the domain after promoting/deploying to the production branch.",
|
|
847
|
+
exitCode: 2,
|
|
848
|
+
nextSteps: ["prisma-cli app domain add <hostname> --branch production"]
|
|
849
|
+
});
|
|
850
|
+
const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
|
|
851
|
+
const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
|
|
852
|
+
const skipLocalPin = Boolean(envProjectId || envAppId);
|
|
853
|
+
const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd);
|
|
854
|
+
if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
|
|
855
|
+
const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
|
|
856
|
+
allowCreate: false,
|
|
857
|
+
branch,
|
|
858
|
+
envProjectId,
|
|
859
|
+
localPin
|
|
860
|
+
});
|
|
861
|
+
const selectedApp = await resolveDomainAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), {
|
|
862
|
+
explicitAppName: options?.appName,
|
|
863
|
+
explicitAppId: envAppId
|
|
864
|
+
});
|
|
865
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
866
|
+
id: selectedApp.id,
|
|
867
|
+
name: selectedApp.name
|
|
868
|
+
});
|
|
869
|
+
return {
|
|
870
|
+
provider,
|
|
871
|
+
app: selectedApp,
|
|
872
|
+
resultTarget: {
|
|
873
|
+
workspace: target.workspace,
|
|
874
|
+
project: target.project,
|
|
875
|
+
branch: target.branch,
|
|
876
|
+
app: {
|
|
877
|
+
id: selectedApp.id,
|
|
878
|
+
name: selectedApp.name
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function resolveDomainBranch(explicitBranchName) {
|
|
884
|
+
return {
|
|
885
|
+
name: explicitBranchName?.trim() || "production",
|
|
886
|
+
annotation: explicitBranchName ? "set by --branch" : "production default"
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
async function resolveDomainAppSelection(context, projectId, apps, options) {
|
|
890
|
+
if (options.explicitAppId) {
|
|
891
|
+
const matched = apps.find((app) => app.id === options.explicitAppId);
|
|
892
|
+
if (!matched) throw usageError("Selected app does not exist in the resolved production branch", `The app "${options.explicitAppId}" from ${PRISMA_APP_ID_ENV_VAR} could not be found in resolved project "${projectId}".`, `Unset ${PRISMA_APP_ID_ENV_VAR}, pass --app <name>, or deploy the app on the production branch.`, ["prisma-cli app deploy --branch production"], "app");
|
|
893
|
+
return matched;
|
|
894
|
+
}
|
|
895
|
+
const selectedApp = await resolveExistingAppSelection(context, projectId, apps, options.explicitAppName);
|
|
896
|
+
if (selectedApp) return selectedApp;
|
|
897
|
+
throw usageError("Custom domain requires an existing app on the production branch", "The resolved production branch does not have an app that can receive a custom domain.", "Deploy or promote an app to production first, then rerun the domain command.", ["prisma-cli app deploy --branch production", "prisma-cli app show"], "app");
|
|
898
|
+
}
|
|
899
|
+
async function resolveDomainByHostname(provider, appId, hostname, command) {
|
|
900
|
+
const matched = (await provider.listDomains(appId).catch((error) => {
|
|
901
|
+
throw domainCommandError(command, error, hostname);
|
|
902
|
+
})).find((domain) => sameDomainHostname(domain.hostname, hostname));
|
|
903
|
+
if (matched) return matched;
|
|
904
|
+
throw domainNotFoundError(hostname);
|
|
905
|
+
}
|
|
906
|
+
function normalizeDomainHostname(hostname) {
|
|
907
|
+
const normalized = hostname.trim().replace(/\.$/, "").toLowerCase();
|
|
908
|
+
if (!isValidDomainHostname(normalized)) throw new CliError({
|
|
909
|
+
code: "DOMAIN_HOSTNAME_INVALID",
|
|
910
|
+
domain: "app",
|
|
911
|
+
summary: `Invalid custom domain "${hostname}"`,
|
|
912
|
+
why: "Custom domains must be valid hostnames without protocol, path, wildcard, or port.",
|
|
913
|
+
fix: "Pass a hostname like shop.acme.com.",
|
|
914
|
+
exitCode: 2,
|
|
915
|
+
nextSteps: ["prisma-cli app domain add shop.acme.com"]
|
|
916
|
+
});
|
|
917
|
+
return normalized;
|
|
918
|
+
}
|
|
919
|
+
function isValidDomainHostname(hostname) {
|
|
920
|
+
if (hostname.length < 1 || hostname.length > 253) return false;
|
|
921
|
+
if (hostname.includes("://") || hostname.includes("/") || hostname.includes(":") || hostname.startsWith("*.")) return false;
|
|
922
|
+
const labels = hostname.split(".");
|
|
923
|
+
if (labels.length < 2) return false;
|
|
924
|
+
return labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
|
|
925
|
+
}
|
|
926
|
+
function sameDomainHostname(left, right) {
|
|
927
|
+
return left.trim().replace(/\.$/, "").toLowerCase() === right.trim().replace(/\.$/, "").toLowerCase();
|
|
928
|
+
}
|
|
929
|
+
function toAppDomainSummary(domain) {
|
|
930
|
+
return {
|
|
931
|
+
id: domain.id,
|
|
932
|
+
type: domain.type,
|
|
933
|
+
url: domain.url,
|
|
934
|
+
hostname: domain.hostname,
|
|
935
|
+
computeServiceId: domain.computeServiceId,
|
|
936
|
+
status: domain.status,
|
|
937
|
+
foundryStatus: domain.foundryStatus,
|
|
938
|
+
failureReason: domain.failureReason,
|
|
939
|
+
failureCategory: domain.failureCategory,
|
|
940
|
+
certExpiresAt: domain.certExpiresAt,
|
|
941
|
+
createdAt: domain.createdAt,
|
|
942
|
+
updatedAt: domain.updatedAt,
|
|
943
|
+
dnsRecords: toAppDomainDnsRecords(domain)
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function toAppDomainDnsRecords(domain) {
|
|
947
|
+
return domain.dnsRecords.map((record) => ({
|
|
948
|
+
type: record.type,
|
|
949
|
+
name: record.name,
|
|
950
|
+
value: record.value,
|
|
951
|
+
ttl: record.ttl
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
function buildDomainShowNextSteps(domain) {
|
|
955
|
+
if (domain.status === "active") return [];
|
|
956
|
+
if (domain.status === "failed") return [`prisma-cli app domain retry ${domain.hostname}`];
|
|
957
|
+
return [`prisma-cli app domain wait ${domain.hostname}`];
|
|
958
|
+
}
|
|
959
|
+
async function confirmDomainRemoval(context, target, hostname) {
|
|
960
|
+
if (context.flags.yes) return;
|
|
961
|
+
if (!canPrompt(context)) throw new CliError({
|
|
962
|
+
code: "CONFIRMATION_REQUIRED",
|
|
963
|
+
domain: "app",
|
|
964
|
+
summary: "Custom domain removal requires confirmation in the current mode",
|
|
965
|
+
why: "This command detaches a domain and cannot prompt for confirmation in the current mode.",
|
|
966
|
+
fix: `Pass --yes to confirm removal of "${hostname}", or rerun prisma-cli app domain remove in an interactive TTY.`,
|
|
967
|
+
exitCode: 1,
|
|
968
|
+
nextSteps: [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`]
|
|
969
|
+
});
|
|
970
|
+
if (!await confirmPrompt({
|
|
971
|
+
input: context.runtime.stdin,
|
|
972
|
+
output: context.output.stderr,
|
|
973
|
+
message: `Detach ${hostname} from App "${target.app.name}"?`,
|
|
974
|
+
initialValue: false
|
|
975
|
+
})) throw usageError("Custom domain removal canceled", "The command was canceled before the domain was detached.", "Rerun the command and confirm removal, or pass --yes.", [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`], "app");
|
|
976
|
+
}
|
|
977
|
+
function domainCommandError(command, error, hostname) {
|
|
978
|
+
if (error instanceof PreviewDomainApiError) {
|
|
979
|
+
if (command === "add" && (error.status === 400 || error.status === 422) && isDomainDnsError(error)) return domainDnsNotConfiguredError(hostname, error);
|
|
980
|
+
if (command === "add" && error.status === 400) return new CliError({
|
|
981
|
+
code: "DOMAIN_HOSTNAME_INVALID",
|
|
982
|
+
domain: "app",
|
|
983
|
+
summary: `Invalid custom domain "${hostname}"`,
|
|
984
|
+
why: error.message,
|
|
985
|
+
fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
|
|
986
|
+
debug: formatDebugDetails(error),
|
|
987
|
+
exitCode: 2,
|
|
988
|
+
nextSteps: ["prisma-cli app domain add shop.acme.com"]
|
|
989
|
+
});
|
|
990
|
+
if (command === "add" && (error.status === 429 || isDomainQuotaError(error))) return new CliError({
|
|
991
|
+
code: "DOMAIN_QUOTA_EXCEEDED",
|
|
992
|
+
domain: "app",
|
|
993
|
+
summary: "Custom domain quota exceeded",
|
|
994
|
+
why: error.message,
|
|
995
|
+
fix: "Remove an existing custom domain before adding another one.",
|
|
996
|
+
debug: formatDebugDetails(error),
|
|
997
|
+
exitCode: 1,
|
|
998
|
+
nextSteps: ["prisma-cli app domain remove <hostname>"]
|
|
999
|
+
});
|
|
1000
|
+
if (command === "add" && error.status === 409) return domainAlreadyRegisteredError(hostname, error);
|
|
1001
|
+
if (command === "add" && error.status === 422) return new CliError({
|
|
1002
|
+
code: "NO_DEPLOYMENTS",
|
|
1003
|
+
domain: "app",
|
|
1004
|
+
summary: "Custom domain requires a live production deployment",
|
|
1005
|
+
why: "The selected production app does not have a promoted version that can receive a custom domain.",
|
|
1006
|
+
fix: "Deploy the app to the production branch, then rerun the domain command.",
|
|
1007
|
+
debug: formatDebugDetails(error),
|
|
1008
|
+
exitCode: 1,
|
|
1009
|
+
nextSteps: ["prisma-cli app deploy --branch production", `prisma-cli app domain add ${hostname}`]
|
|
1010
|
+
});
|
|
1011
|
+
if ((command === "show" || command === "remove" || command === "retry" || command === "wait") && error.status === 404) return domainNotFoundError(hostname);
|
|
1012
|
+
if (command === "retry" && error.status === 409) return new CliError({
|
|
1013
|
+
code: "DOMAIN_RETRY_NOT_ELIGIBLE",
|
|
1014
|
+
domain: "app",
|
|
1015
|
+
summary: `Custom domain "${hostname}" is not eligible for retry`,
|
|
1016
|
+
why: error.message,
|
|
1017
|
+
fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
|
|
1018
|
+
debug: formatDebugDetails(error),
|
|
1019
|
+
exitCode: 1,
|
|
1020
|
+
nextSteps: [`prisma-cli app domain show ${hostname}`]
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return new CliError({
|
|
1024
|
+
code: "DEPLOY_FAILED",
|
|
1025
|
+
domain: "app",
|
|
1026
|
+
summary: `Custom domain ${command} failed`,
|
|
1027
|
+
why: error instanceof Error ? error.message : String(error),
|
|
1028
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
1029
|
+
debug: formatDebugDetails(error),
|
|
1030
|
+
exitCode: 1,
|
|
1031
|
+
nextSteps: [`prisma-cli app domain show ${hostname}`]
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
function isDomainQuotaError(error) {
|
|
1035
|
+
if (error.status !== 409) return false;
|
|
1036
|
+
const text = `${error.message} ${error.hint ?? ""}`.toLowerCase();
|
|
1037
|
+
return text.includes("quota") || text.includes("maximum") || text.includes("limit");
|
|
1038
|
+
}
|
|
1039
|
+
function domainAlreadyRegisteredError(hostname, error) {
|
|
1040
|
+
return new CliError({
|
|
1041
|
+
code: "DOMAIN_ALREADY_REGISTERED",
|
|
1042
|
+
domain: "app",
|
|
1043
|
+
summary: `Custom domain "${hostname}" is already registered`,
|
|
1044
|
+
why: error.hint ?? error.message,
|
|
1045
|
+
fix: "Select the app that owns this hostname and remove it there, or contact support if you cannot access it.",
|
|
1046
|
+
debug: formatDebugDetails(error),
|
|
1047
|
+
exitCode: 1,
|
|
1048
|
+
nextSteps: [`Select the owning app and remove ${hostname} there.`, "Contact Prisma support if you cannot access the owning app."]
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function isDomainDnsError(error) {
|
|
1052
|
+
const text = `${error.message} ${error.hint ?? ""}`.toLowerCase();
|
|
1053
|
+
return text.includes("dns is not configured") || text.includes("dns verification failed") || text.includes("no cname") || text.includes("cname record") || text.includes("no a/aaaa") || /\bcname(?:s)?\s+to\b/.test(text);
|
|
1054
|
+
}
|
|
1055
|
+
function domainDnsNotConfiguredError(hostname, error) {
|
|
1056
|
+
const target = extractDomainDnsTarget(error);
|
|
1057
|
+
const record = target ? `CNAME ${hostname} -> ${target}` : null;
|
|
1058
|
+
return new CliError({
|
|
1059
|
+
code: "DOMAIN_DNS_NOT_CONFIGURED",
|
|
1060
|
+
domain: "app",
|
|
1061
|
+
summary: `DNS is not configured for "${hostname}"`,
|
|
1062
|
+
why: error.hint ?? error.message,
|
|
1063
|
+
fix: record ? `Add ${record} at your DNS provider, then rerun the domain command.` : "The platform did not return the required DNS target. Re-run with --trace for the underlying API response details.",
|
|
1064
|
+
debug: formatDebugDetails(error),
|
|
1065
|
+
exitCode: 1,
|
|
1066
|
+
nextSteps: record ? [`add ${record}`, `prisma-cli app domain add ${hostname}`] : [`prisma-cli app domain add ${hostname} --trace`]
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function extractDomainDnsTarget(error) {
|
|
1070
|
+
const text = `${error.hint ?? ""} ${error.message}`;
|
|
1071
|
+
return /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i.exec(text)?.[1]?.toLowerCase() ?? null;
|
|
1072
|
+
}
|
|
1073
|
+
function domainNotFoundError(hostname) {
|
|
1074
|
+
return new CliError({
|
|
1075
|
+
code: "DOMAIN_NOT_FOUND",
|
|
1076
|
+
domain: "app",
|
|
1077
|
+
summary: `Custom domain "${hostname}" not found`,
|
|
1078
|
+
why: "The hostname is not attached to the selected app.",
|
|
1079
|
+
fix: "Check the hostname and selected app, or add the domain first.",
|
|
1080
|
+
exitCode: 1,
|
|
1081
|
+
nextSteps: [`prisma-cli app domain add ${hostname}`]
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
function formatDomainFailureWhy(domain) {
|
|
1085
|
+
if (domain.failureReason) return domain.failureCategory ? `${domain.failureCategory}: ${domain.failureReason}` : domain.failureReason;
|
|
1086
|
+
return "The platform reported a terminal failed state for this custom domain.";
|
|
1087
|
+
}
|
|
1088
|
+
function parseDomainWaitTimeout(value) {
|
|
1089
|
+
if (!value) return 900 * 1e3;
|
|
1090
|
+
const trimmed = value.trim().toLowerCase();
|
|
1091
|
+
if (trimmed === "0") return 0;
|
|
1092
|
+
const match = /^(\d+)(ms|s|m|h)$/.exec(trimmed);
|
|
1093
|
+
if (!match) throw usageError(`Invalid timeout "${value}"`, "Timeout must be a duration such as 0, 30s, 15m, or 1h.", "Pass --timeout 15m, or --timeout 0 to poll once.", ["prisma-cli app domain wait shop.acme.com --timeout 15m"], "app");
|
|
1094
|
+
const amount = Number.parseInt(match[1], 10);
|
|
1095
|
+
const unit = match[2];
|
|
1096
|
+
return amount * (unit === "h" ? 3600 * 1e3 : unit === "m" ? 60 * 1e3 : unit === "s" ? 1e3 : 1);
|
|
1097
|
+
}
|
|
1098
|
+
function readDomainWaitPollIntervalMs(context) {
|
|
1099
|
+
const raw = context.runtime.env.PRISMA_CLI_DOMAIN_WAIT_POLL_MS;
|
|
1100
|
+
if (!raw) return 5e3;
|
|
1101
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1102
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 5e3;
|
|
1103
|
+
}
|
|
1104
|
+
function emitDomainWaitStatus(context, event) {
|
|
1105
|
+
if (context.flags.json) {
|
|
1106
|
+
writeJsonEvent(context.output, {
|
|
1107
|
+
type: "status",
|
|
1108
|
+
command: "app.domain.wait",
|
|
1109
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1110
|
+
data: {
|
|
1111
|
+
hostname: event.hostname,
|
|
1112
|
+
domainId: event.domainId,
|
|
1113
|
+
previousStatus: event.previousStatus,
|
|
1114
|
+
status: event.status,
|
|
1115
|
+
elapsedMs: event.elapsedMs
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (context.flags.quiet) return;
|
|
1121
|
+
if (event.previousStatus === event.status) return;
|
|
1122
|
+
const transition = event.previousStatus ? `${event.previousStatus} -> ${event.status}` : event.status;
|
|
1123
|
+
context.output.stderr.write(` ${transition} (${formatElapsed(event.elapsedMs)})\n`);
|
|
1124
|
+
}
|
|
1125
|
+
function formatElapsed(milliseconds) {
|
|
1126
|
+
const seconds = Math.max(Math.floor(milliseconds / 1e3), 0);
|
|
1127
|
+
const minutes = Math.floor(seconds / 60);
|
|
1128
|
+
const remainingSeconds = seconds % 60;
|
|
1129
|
+
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
|
1130
|
+
}
|
|
1131
|
+
async function sleep(milliseconds) {
|
|
1132
|
+
if (milliseconds <= 0) return;
|
|
1133
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1134
|
+
}
|
|
711
1135
|
async function resolveDeployAppSelection(context, projectId, apps, options) {
|
|
712
1136
|
if (options.explicitAppName) {
|
|
713
1137
|
const matches = findAppsByName(apps, options.explicitAppName);
|
|
@@ -1688,4 +2112,4 @@ function emitLegacyEnvDeprecationWarning(context, legacyCommand, replacement) {
|
|
|
1688
2112
|
context.runtime.stderr.write(`${message}\n`);
|
|
1689
2113
|
}
|
|
1690
2114
|
//#endregion
|
|
1691
|
-
export { runAppBuild, runAppDeploy, runAppListDeploys, runAppListEnv, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy, runAppUpdateEnv };
|
|
2115
|
+
export { runAppBuild, runAppDeploy, runAppDomainAdd, runAppDomainRemove, runAppDomainRetry, runAppDomainShow, runAppDomainWait, runAppListDeploys, runAppListEnv, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy, runAppUpdateEnv };
|
|
@@ -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,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 };
|
package/dist/presenters/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderDeployOutputRows } from "../lib/app/deploy-output.js";
|
|
2
|
+
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
2
3
|
import { renderList, renderShow, serializeList } from "../output/patterns.js";
|
|
3
4
|
//#region src/presenters/app.ts
|
|
4
5
|
function renderAppBuild(context, descriptor, result) {
|
|
@@ -253,6 +254,105 @@ function renderAppOpen(context, descriptor, result) {
|
|
|
253
254
|
function serializeAppOpen(result) {
|
|
254
255
|
return result;
|
|
255
256
|
}
|
|
257
|
+
function renderAppDomainAdd(context, descriptor, result) {
|
|
258
|
+
return renderShow({
|
|
259
|
+
title: result.existing ? "Showing the existing custom domain for the selected app." : "Adding a custom domain to the selected app.",
|
|
260
|
+
descriptor,
|
|
261
|
+
fields: [
|
|
262
|
+
...domainTargetFields(result),
|
|
263
|
+
{
|
|
264
|
+
key: "hostname",
|
|
265
|
+
value: result.domain.hostname
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: "status",
|
|
269
|
+
value: result.domain.status,
|
|
270
|
+
tone: toneForDomainStatus(result.domain.status)
|
|
271
|
+
},
|
|
272
|
+
...domainDnsFields(result.domain)
|
|
273
|
+
]
|
|
274
|
+
}, context.ui);
|
|
275
|
+
}
|
|
276
|
+
function serializeAppDomainAdd(result) {
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
function renderAppDomainShow(context, descriptor, result) {
|
|
280
|
+
return renderShow({
|
|
281
|
+
title: "Showing custom domain status.",
|
|
282
|
+
descriptor,
|
|
283
|
+
fields: [
|
|
284
|
+
...domainTargetFields(result),
|
|
285
|
+
{
|
|
286
|
+
key: "hostname",
|
|
287
|
+
value: result.domain.hostname
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
key: "status",
|
|
291
|
+
value: result.domain.status,
|
|
292
|
+
tone: toneForDomainStatus(result.domain.status)
|
|
293
|
+
},
|
|
294
|
+
...domainFailureFields(result.domain),
|
|
295
|
+
{
|
|
296
|
+
key: "cert expires",
|
|
297
|
+
value: formatOptionalUtcDate(result.domain.certExpiresAt),
|
|
298
|
+
tone: result.domain.certExpiresAt ? "default" : "dim"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: "created",
|
|
302
|
+
value: formatUtcDate(result.domain.createdAt),
|
|
303
|
+
tone: "dim"
|
|
304
|
+
},
|
|
305
|
+
...domainDnsFields(result.domain)
|
|
306
|
+
]
|
|
307
|
+
}, context.ui);
|
|
308
|
+
}
|
|
309
|
+
function serializeAppDomainShow(result) {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function renderAppDomainRemove(context, descriptor, result) {
|
|
313
|
+
return renderShow({
|
|
314
|
+
title: "Removing a custom domain from the selected app.",
|
|
315
|
+
descriptor,
|
|
316
|
+
fields: [
|
|
317
|
+
...domainTargetFields(result),
|
|
318
|
+
{
|
|
319
|
+
key: "hostname",
|
|
320
|
+
value: result.hostname
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
key: "removed",
|
|
324
|
+
value: result.removed ? "yes" : "no",
|
|
325
|
+
tone: result.removed ? "success" : "dim"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}, context.ui);
|
|
329
|
+
}
|
|
330
|
+
function serializeAppDomainRemove(result) {
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
function renderAppDomainRetry(context, descriptor, result) {
|
|
334
|
+
return renderShow({
|
|
335
|
+
title: "Retrying custom domain verification.",
|
|
336
|
+
descriptor,
|
|
337
|
+
fields: [
|
|
338
|
+
...domainTargetFields(result),
|
|
339
|
+
{
|
|
340
|
+
key: "hostname",
|
|
341
|
+
value: result.domain.hostname
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
key: "status",
|
|
345
|
+
value: result.domain.status,
|
|
346
|
+
tone: toneForDomainStatus(result.domain.status)
|
|
347
|
+
},
|
|
348
|
+
...domainFailureFields(result.domain),
|
|
349
|
+
...domainDnsFields(result.domain)
|
|
350
|
+
]
|
|
351
|
+
}, context.ui);
|
|
352
|
+
}
|
|
353
|
+
function serializeAppDomainRetry(result) {
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
256
356
|
function renderAppPromote(context, descriptor, result) {
|
|
257
357
|
return renderShow({
|
|
258
358
|
title: "Switching the live deployment for the selected app.",
|
|
@@ -380,6 +480,77 @@ function toneForStatus(status) {
|
|
|
380
480
|
if (status === "failed" || status === "error") return "error";
|
|
381
481
|
return "default";
|
|
382
482
|
}
|
|
483
|
+
function toneForDomainStatus(status) {
|
|
484
|
+
if (status === "active") return "success";
|
|
485
|
+
if (status === "failed") return "error";
|
|
486
|
+
if (status === "pending_dns" || status === "verifying" || status === "provisioning_tls" || status === "verified_routing_blocked") return "warning";
|
|
487
|
+
return "default";
|
|
488
|
+
}
|
|
489
|
+
function domainTargetFields(result) {
|
|
490
|
+
return [
|
|
491
|
+
{
|
|
492
|
+
key: "workspace",
|
|
493
|
+
value: result.workspace.name
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
key: "project",
|
|
497
|
+
value: result.project.name
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
key: "branch",
|
|
501
|
+
value: result.branch.name
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
key: "app",
|
|
505
|
+
value: result.app.name
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
function domainDnsFields(domain) {
|
|
510
|
+
const records = domain.dnsRecords;
|
|
511
|
+
if (records.length === 0) return [{
|
|
512
|
+
key: "dns record",
|
|
513
|
+
value: "not provided by platform",
|
|
514
|
+
tone: "dim"
|
|
515
|
+
}];
|
|
516
|
+
return [{
|
|
517
|
+
key: "dns record",
|
|
518
|
+
value: records.map((record) => {
|
|
519
|
+
const ttl = record.ttl ? ` ttl ${record.ttl}` : "";
|
|
520
|
+
return `${record.type} ${record.name} -> ${record.value}${ttl}`;
|
|
521
|
+
}).join(", ")
|
|
522
|
+
}];
|
|
523
|
+
}
|
|
524
|
+
function formatDomainFailure(domain) {
|
|
525
|
+
if (!domain.failureReason) return domain.failureCategory ?? "none";
|
|
526
|
+
return domain.failureCategory ? `${domain.failureCategory} - ${domain.failureReason}` : domain.failureReason;
|
|
527
|
+
}
|
|
528
|
+
function domainFailureFields(domain) {
|
|
529
|
+
const tone = hasDomainFailure(domain) ? "error" : "dim";
|
|
530
|
+
return [{
|
|
531
|
+
key: "failure",
|
|
532
|
+
value: formatDomainFailure(domain),
|
|
533
|
+
tone
|
|
534
|
+
}, ...domainFixFields(domain)];
|
|
535
|
+
}
|
|
536
|
+
function hasDomainFailure(domain) {
|
|
537
|
+
return Boolean(domain.failureCategory || domain.failureReason);
|
|
538
|
+
}
|
|
539
|
+
function domainFixFields(domain) {
|
|
540
|
+
const fix = formatDomainFailureFix(domain);
|
|
541
|
+
return fix ? [{
|
|
542
|
+
key: "fix",
|
|
543
|
+
value: fix
|
|
544
|
+
}] : [];
|
|
545
|
+
}
|
|
546
|
+
function formatOptionalUtcDate(value) {
|
|
547
|
+
return value ? formatUtcDate(value) : "-";
|
|
548
|
+
}
|
|
549
|
+
function formatUtcDate(value) {
|
|
550
|
+
const date = new Date(value);
|
|
551
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
552
|
+
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")} ${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")} UTC`;
|
|
553
|
+
}
|
|
383
554
|
function formatRecentDeployments(deployments) {
|
|
384
555
|
if (deployments.length === 0) return "none";
|
|
385
556
|
return deployments.map((deployment) => `${deployment.id}${deployment.live ? " (live)" : ""}`).join(", ");
|
|
@@ -389,4 +560,4 @@ function formatVariableNames(variables) {
|
|
|
389
560
|
return variables.join(", ");
|
|
390
561
|
}
|
|
391
562
|
//#endregion
|
|
392
|
-
export { renderAppBuild, renderAppDeploy, renderAppListDeploys, renderAppListEnv, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, renderAppUpdateEnv, serializeAppBuild, serializeAppDeploy, serializeAppListDeploys, serializeAppListEnv, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy, serializeAppUpdateEnv };
|
|
563
|
+
export { renderAppBuild, renderAppDeploy, renderAppDomainAdd, renderAppDomainRemove, renderAppDomainRetry, renderAppDomainShow, renderAppListDeploys, renderAppListEnv, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, renderAppUpdateEnv, serializeAppBuild, serializeAppDeploy, serializeAppDomainAdd, serializeAppDomainRemove, serializeAppDomainRetry, serializeAppDomainShow, serializeAppListDeploys, serializeAppListEnv, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy, serializeAppUpdateEnv };
|
|
@@ -227,6 +227,75 @@ const DESCRIPTORS = [
|
|
|
227
227
|
description: "Open the app's live URL",
|
|
228
228
|
examples: ["prisma-cli app open", "prisma-cli app open --app hello-world"]
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
id: "app.domain",
|
|
232
|
+
path: [
|
|
233
|
+
"prisma",
|
|
234
|
+
"app",
|
|
235
|
+
"domain"
|
|
236
|
+
],
|
|
237
|
+
description: "Manage custom domains for an app",
|
|
238
|
+
examples: [
|
|
239
|
+
"prisma-cli app domain add shop.acme.com",
|
|
240
|
+
"prisma-cli app domain wait shop.acme.com --timeout 15m",
|
|
241
|
+
"prisma-cli app domain retry shop.acme.com"
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "app.domain.add",
|
|
246
|
+
path: [
|
|
247
|
+
"prisma",
|
|
248
|
+
"app",
|
|
249
|
+
"domain",
|
|
250
|
+
"add"
|
|
251
|
+
],
|
|
252
|
+
description: "Register a custom domain on the app's production branch",
|
|
253
|
+
examples: ["prisma-cli app domain add shop.acme.com"]
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "app.domain.show",
|
|
257
|
+
path: [
|
|
258
|
+
"prisma",
|
|
259
|
+
"app",
|
|
260
|
+
"domain",
|
|
261
|
+
"show"
|
|
262
|
+
],
|
|
263
|
+
description: "Show custom domain status and certificate details",
|
|
264
|
+
examples: ["prisma-cli app domain show shop.acme.com"]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "app.domain.remove",
|
|
268
|
+
path: [
|
|
269
|
+
"prisma",
|
|
270
|
+
"app",
|
|
271
|
+
"domain",
|
|
272
|
+
"remove"
|
|
273
|
+
],
|
|
274
|
+
description: "Detach a custom domain from the app",
|
|
275
|
+
examples: ["prisma-cli app domain remove shop.acme.com --yes"]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "app.domain.retry",
|
|
279
|
+
path: [
|
|
280
|
+
"prisma",
|
|
281
|
+
"app",
|
|
282
|
+
"domain",
|
|
283
|
+
"retry"
|
|
284
|
+
],
|
|
285
|
+
description: "Retry custom domain DNS verification and TLS provisioning",
|
|
286
|
+
examples: ["prisma-cli app domain retry shop.acme.com"]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "app.domain.wait",
|
|
290
|
+
path: [
|
|
291
|
+
"prisma",
|
|
292
|
+
"app",
|
|
293
|
+
"domain",
|
|
294
|
+
"wait"
|
|
295
|
+
],
|
|
296
|
+
description: "Wait until a custom domain is active or failed",
|
|
297
|
+
examples: ["prisma-cli app domain wait shop.acme.com", "prisma-cli app domain wait shop.acme.com --timeout 0 --json"]
|
|
298
|
+
},
|
|
230
299
|
{
|
|
231
300
|
id: "app.logs",
|
|
232
301
|
path: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma/cli",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.14",
|
|
4
4
|
"description": "Preview of the unified Prisma CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@prisma/compute-sdk": "^0.19.0",
|
|
40
40
|
"c12": "4.0.0-beta.4",
|
|
41
41
|
"@prisma/credentials-store": "^7.7.0",
|
|
42
|
-
"@prisma/management-api-sdk": "^1.
|
|
42
|
+
"@prisma/management-api-sdk": "^1.33.0",
|
|
43
43
|
"colorette": "^2.0.20",
|
|
44
44
|
"commander": "^12.1.0",
|
|
45
45
|
"magicast": "^0.3.5",
|