@prisma/cli 3.0.0-alpha.12 → 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.
@@ -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"));
@@ -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 };
@@ -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.12",
3
+ "version": "3.0.0-alpha.14",
4
4
  "description": "Preview of the unified Prisma CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,10 +36,10 @@
36
36
  "license": "Apache-2.0",
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^1.2.0",
39
- "@prisma/compute-sdk": "^0.18.0",
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.32.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",