@prisma/cli 3.0.0-alpha.9 → 3.0.0-beta.0

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.
@@ -15,8 +15,9 @@ import { inferTargetName, projectNotFoundError, resolveProjectTarget } from "../
15
15
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, readLocalResolutionPin, writeLocalResolutionPin } from "../lib/project/local-pin.js";
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
- import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress } from "../lib/app/preview-progress.js";
19
- import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
18
+ import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.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";
@@ -202,136 +203,6 @@ async function runAppDeploy(context, appName, options) {
202
203
  nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`]
203
204
  };
204
205
  }
205
- async function runAppUpdateEnv(context, appName, envAssignments, projectRef) {
206
- ensurePreviewAppMode(context);
207
- emitLegacyEnvDeprecationWarning(context, "app update-env", "project env add");
208
- const envVars = parseEnvAssignments(envAssignments, {
209
- commandName: "update-env",
210
- requireAtLeastOne: true
211
- });
212
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
213
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
214
- if (!selectedApp) throw noDeploymentsError("No deployments available to update environment variables", "The resolved project does not have any deployed app yet.");
215
- const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
216
- throw deployFailedError("Failed to inspect app deployments", error, ["prisma-cli app list-deploys"]);
217
- });
218
- if (deploymentsResult.deployments.length === 0) throw noDeploymentsError("No deployments available to update environment variables", `The selected app "${deploymentsResult.app.name}" does not have any deployments yet.`);
219
- const updateResult = await provider.updateAppEnv({
220
- appId: deploymentsResult.app.id,
221
- envVars,
222
- progress: createPreviewUpdateEnvProgress(context.output.stderr, !context.flags.json && !context.flags.quiet),
223
- promoteProgress: createPreviewPromoteProgress(context.output.stderr, !context.flags.json && !context.flags.quiet)
224
- }).catch((error) => {
225
- throw deployFailedError("Failed to update app environment variables", error, ["prisma-cli app list-env"]);
226
- });
227
- await context.stateStore.setSelectedApp(projectId, {
228
- id: updateResult.app.id,
229
- name: updateResult.app.name
230
- });
231
- await context.stateStore.setKnownLiveDeployment(projectId, updateResult.app.id, updateResult.deployment.id);
232
- return {
233
- command: "app.update-env",
234
- result: {
235
- projectId: updateResult.projectId,
236
- app: {
237
- id: updateResult.app.id,
238
- name: updateResult.app.name
239
- },
240
- deployment: updateResult.deployment,
241
- variables: updateResult.variables
242
- },
243
- warnings: [],
244
- nextSteps: ["prisma-cli app list-env", `prisma-cli app show-deploy ${updateResult.deployment.id}`]
245
- };
246
- }
247
- async function runAppListEnv(context, appName, projectRef) {
248
- ensurePreviewAppMode(context);
249
- emitLegacyEnvDeprecationWarning(context, "app list-env", "project env list");
250
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
251
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
252
- if (!selectedApp) return {
253
- command: "app.list-env",
254
- result: {
255
- projectId,
256
- app: null,
257
- deployment: null,
258
- variables: []
259
- },
260
- warnings: [],
261
- nextSteps: ["prisma-cli app deploy"]
262
- };
263
- const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
264
- throw deployFailedError("Failed to inspect app deployments", error, ["prisma-cli app list-deploys"]);
265
- });
266
- const knownLiveDeploymentId = await context.stateStore.readKnownLiveDeployment(projectId, deploymentsResult.app.id);
267
- const missingKnownLiveDeploymentId = knownLiveDeploymentId && !deploymentsResult.deployments.some((candidate) => candidate.id === knownLiveDeploymentId) ? knownLiveDeploymentId : null;
268
- const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
269
- const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId).slice().sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
270
- const deployment = currentLiveDeploymentId ? deployments.find((candidate) => candidate.id === currentLiveDeploymentId) ?? null : null;
271
- await context.stateStore.setSelectedApp(projectId, {
272
- id: deploymentsResult.app.id,
273
- name: deploymentsResult.app.name
274
- });
275
- if (missingKnownLiveDeploymentId) {
276
- const envResult = await provider.listAppEnvNames({
277
- appId: deploymentsResult.app.id,
278
- deploymentId: missingKnownLiveDeploymentId
279
- }).catch((error) => {
280
- throw deployFailedError("Failed to inspect app environment variables", error, ["prisma-cli app list-deploys"]);
281
- });
282
- return {
283
- command: "app.list-env",
284
- result: {
285
- projectId,
286
- app: {
287
- id: envResult.app.id,
288
- name: envResult.app.name
289
- },
290
- deployment: envResult.deployment,
291
- variables: envResult.variables
292
- },
293
- warnings: [],
294
- nextSteps: [`prisma-cli app show-deploy ${envResult.deployment.id}`]
295
- };
296
- }
297
- if (!deployment) return {
298
- command: "app.list-env",
299
- result: {
300
- projectId,
301
- app: {
302
- id: deploymentsResult.app.id,
303
- name: deploymentsResult.app.name
304
- },
305
- deployment: null,
306
- variables: []
307
- },
308
- warnings: [],
309
- nextSteps: ["prisma-cli app deploy"]
310
- };
311
- const envResult = await provider.listAppEnvNames({
312
- appId: deploymentsResult.app.id,
313
- deploymentId: deployment.id
314
- }).catch((error) => {
315
- throw deployFailedError("Failed to inspect app environment variables", error, ["prisma-cli app list-deploys"]);
316
- });
317
- return {
318
- command: "app.list-env",
319
- result: {
320
- projectId,
321
- app: {
322
- id: envResult.app.id,
323
- name: envResult.app.name
324
- },
325
- deployment: {
326
- ...deployment,
327
- live: deployment.live ?? envResult.deployment.live
328
- },
329
- variables: envResult.variables
330
- },
331
- warnings: [],
332
- nextSteps: deployment.id ? [`prisma-cli app show-deploy ${deployment.id}`] : ["prisma-cli app deploy"]
333
- };
334
- }
335
206
  async function runAppListDeploys(context, appName, projectRef) {
336
207
  ensurePreviewAppMode(context);
337
208
  const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
@@ -479,6 +350,132 @@ async function runAppOpen(context, appName, projectRef) {
479
350
  nextSteps: ["prisma-cli app show", `prisma-cli app show-deploy ${liveDeployment.id}`]
480
351
  };
481
352
  }
353
+ async function runAppDomainAdd(context, hostname, options) {
354
+ const normalizedHostname = normalizeDomainHostname(hostname);
355
+ const target = await resolveAppDomainTarget(context, options);
356
+ const added = await target.provider.addDomain({
357
+ appId: target.app.id,
358
+ hostname: normalizedHostname
359
+ }).catch((error) => {
360
+ throw domainCommandError("add", error, normalizedHostname);
361
+ });
362
+ return {
363
+ command: "app.domain.add",
364
+ result: {
365
+ ...target.resultTarget,
366
+ domain: toAppDomainSummary(added.domain),
367
+ existing: added.existing
368
+ },
369
+ warnings: [],
370
+ nextSteps: [`prisma-cli app domain wait ${normalizedHostname}`, `prisma-cli app domain show ${normalizedHostname}`]
371
+ };
372
+ }
373
+ async function runAppDomainShow(context, hostname, options) {
374
+ const normalizedHostname = normalizeDomainHostname(hostname);
375
+ const target = await resolveAppDomainTarget(context, options);
376
+ const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "show");
377
+ const detail = await target.provider.showDomain(domain.id).catch((error) => {
378
+ throw domainCommandError("show", error, normalizedHostname);
379
+ });
380
+ return {
381
+ command: "app.domain.show",
382
+ result: {
383
+ ...target.resultTarget,
384
+ domain: toAppDomainSummary(detail)
385
+ },
386
+ warnings: [],
387
+ nextSteps: buildDomainShowNextSteps(detail)
388
+ };
389
+ }
390
+ async function runAppDomainRemove(context, hostname, options) {
391
+ const normalizedHostname = normalizeDomainHostname(hostname);
392
+ const target = await resolveAppDomainTarget(context, options);
393
+ const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "remove");
394
+ await confirmDomainRemoval(context, target.resultTarget, normalizedHostname);
395
+ await target.provider.removeDomain(domain.id).catch((error) => {
396
+ throw domainCommandError("remove", error, normalizedHostname);
397
+ });
398
+ return {
399
+ command: "app.domain.remove",
400
+ result: {
401
+ ...target.resultTarget,
402
+ hostname: normalizedHostname,
403
+ removed: true
404
+ },
405
+ warnings: [],
406
+ nextSteps: []
407
+ };
408
+ }
409
+ async function runAppDomainRetry(context, hostname, options) {
410
+ const normalizedHostname = normalizeDomainHostname(hostname);
411
+ const target = await resolveAppDomainTarget(context, options);
412
+ const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "retry");
413
+ const retried = await target.provider.retryDomain(domain.id).catch((error) => {
414
+ throw domainCommandError("retry", error, normalizedHostname);
415
+ });
416
+ return {
417
+ command: "app.domain.retry",
418
+ result: {
419
+ ...target.resultTarget,
420
+ domain: toAppDomainSummary(retried)
421
+ },
422
+ warnings: [],
423
+ nextSteps: [`prisma-cli app domain wait ${normalizedHostname}`]
424
+ };
425
+ }
426
+ async function runAppDomainWait(context, hostname, options) {
427
+ const normalizedHostname = normalizeDomainHostname(hostname);
428
+ const timeoutMs = parseDomainWaitTimeout(options?.timeout);
429
+ const target = await resolveAppDomainTarget(context, options);
430
+ const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "wait");
431
+ if (!context.flags.json && !context.flags.quiet) context.output.stderr.write([
432
+ `app domain wait -> Waiting for ${normalizedHostname} to become active.`,
433
+ "",
434
+ `Workspace: ${target.resultTarget.workspace.name} Project: ${target.resultTarget.project.name} Branch: ${target.resultTarget.branch.name} App: ${target.resultTarget.app.name}`,
435
+ ""
436
+ ].join("\n"));
437
+ const start = Date.now();
438
+ const deadline = start + timeoutMs;
439
+ const pollIntervalMs = readDomainWaitPollIntervalMs(context);
440
+ let lastStatus = null;
441
+ let current = domain;
442
+ while (true) {
443
+ emitDomainWaitStatus(context, {
444
+ hostname: normalizedHostname,
445
+ domainId: current.id,
446
+ previousStatus: lastStatus,
447
+ status: current.status,
448
+ elapsedMs: Date.now() - start
449
+ });
450
+ lastStatus = current.status;
451
+ if (current.status === "active") {
452
+ if (!context.flags.json && !context.flags.quiet) context.output.stderr.write(`\n${normalizedHostname} is live at https://${normalizedHostname}\n`);
453
+ return;
454
+ }
455
+ if (current.status === "failed") throw new CliError({
456
+ code: "DOMAIN_VERIFICATION_FAILED",
457
+ domain: "app",
458
+ summary: `Custom domain "${normalizedHostname}" failed verification`,
459
+ why: formatDomainFailureWhy(current),
460
+ fix: formatDomainFailureFix(current) ?? `Run prisma-cli app domain retry ${normalizedHostname}.`,
461
+ exitCode: 1,
462
+ nextSteps: [`prisma-cli app domain show ${normalizedHostname}`, `prisma-cli app domain retry ${normalizedHostname}`]
463
+ });
464
+ if (timeoutMs === 0 || Date.now() >= deadline) throw new CliError({
465
+ code: "DOMAIN_VERIFICATION_TIMEOUT",
466
+ domain: "app",
467
+ summary: `Timed out waiting for "${normalizedHostname}" to become active`,
468
+ why: `The domain is still "${current.status}".`,
469
+ fix: `Run prisma-cli app domain show ${normalizedHostname} to inspect the current status, or retry wait with a longer --timeout.`,
470
+ exitCode: 1,
471
+ nextSteps: [`prisma-cli app domain show ${normalizedHostname}`]
472
+ });
473
+ await sleep(Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)));
474
+ current = await target.provider.showDomain(current.id).catch((error) => {
475
+ throw domainCommandError("wait", error, normalizedHostname);
476
+ });
477
+ }
478
+ }
482
479
  async function runAppLogs(context, appName, deploymentId, projectRef) {
483
480
  ensurePreviewAppMode(context);
484
481
  const { provider, target: resolvedTarget, projectId } = await requireProviderAndProjectContext(context, projectRef);
@@ -708,6 +705,303 @@ async function runAppRemove(context, appName, projectRef) {
708
705
  nextSteps: ["prisma-cli app deploy", "prisma-cli app list-deploys"]
709
706
  };
710
707
  }
708
+ async function resolveAppDomainTarget(context, options) {
709
+ ensurePreviewAppMode(context);
710
+ const branch = resolveDomainBranch(options?.branchName);
711
+ if (toBranchKind(branch.name) !== "production") throw new CliError({
712
+ code: "BRANCH_NOT_DEPLOYABLE",
713
+ domain: "branch",
714
+ summary: "Custom domains require the production branch",
715
+ why: `Custom domains on preview branch "${branch.name}" are not supported in Public Beta.`,
716
+ fix: "Use --branch production, or attach the domain after promoting/deploying to the production branch.",
717
+ exitCode: 2,
718
+ nextSteps: ["prisma-cli app domain add <hostname> --branch production"]
719
+ });
720
+ const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
721
+ const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
722
+ const skipLocalPin = Boolean(envProjectId || envAppId);
723
+ const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd);
724
+ if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
725
+ const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
726
+ allowCreate: false,
727
+ branch,
728
+ envProjectId,
729
+ localPin
730
+ });
731
+ const selectedApp = await resolveDomainAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), {
732
+ explicitAppName: options?.appName,
733
+ explicitAppId: envAppId
734
+ });
735
+ await context.stateStore.setSelectedApp(projectId, {
736
+ id: selectedApp.id,
737
+ name: selectedApp.name
738
+ });
739
+ return {
740
+ provider,
741
+ app: selectedApp,
742
+ resultTarget: {
743
+ workspace: target.workspace,
744
+ project: target.project,
745
+ branch: target.branch,
746
+ app: {
747
+ id: selectedApp.id,
748
+ name: selectedApp.name
749
+ }
750
+ }
751
+ };
752
+ }
753
+ function resolveDomainBranch(explicitBranchName) {
754
+ return {
755
+ name: explicitBranchName?.trim() || "production",
756
+ annotation: explicitBranchName ? "set by --branch" : "production default"
757
+ };
758
+ }
759
+ async function resolveDomainAppSelection(context, projectId, apps, options) {
760
+ if (options.explicitAppId) {
761
+ const matched = apps.find((app) => app.id === options.explicitAppId);
762
+ 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");
763
+ return matched;
764
+ }
765
+ const selectedApp = await resolveExistingAppSelection(context, projectId, apps, options.explicitAppName);
766
+ if (selectedApp) return selectedApp;
767
+ 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");
768
+ }
769
+ async function resolveDomainByHostname(provider, appId, hostname, command) {
770
+ const matched = (await provider.listDomains(appId).catch((error) => {
771
+ throw domainCommandError(command, error, hostname);
772
+ })).find((domain) => sameDomainHostname(domain.hostname, hostname));
773
+ if (matched) return matched;
774
+ throw domainNotFoundError(hostname);
775
+ }
776
+ function normalizeDomainHostname(hostname) {
777
+ const normalized = hostname.trim().replace(/\.$/, "").toLowerCase();
778
+ if (!isValidDomainHostname(normalized)) throw new CliError({
779
+ code: "DOMAIN_HOSTNAME_INVALID",
780
+ domain: "app",
781
+ summary: `Invalid custom domain "${hostname}"`,
782
+ why: "Custom domains must be valid hostnames without protocol, path, wildcard, or port.",
783
+ fix: "Pass a hostname like shop.acme.com.",
784
+ exitCode: 2,
785
+ nextSteps: ["prisma-cli app domain add shop.acme.com"]
786
+ });
787
+ return normalized;
788
+ }
789
+ function isValidDomainHostname(hostname) {
790
+ if (hostname.length < 1 || hostname.length > 253) return false;
791
+ if (hostname.includes("://") || hostname.includes("/") || hostname.includes(":") || hostname.startsWith("*.")) return false;
792
+ const labels = hostname.split(".");
793
+ if (labels.length < 2) return false;
794
+ return labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
795
+ }
796
+ function sameDomainHostname(left, right) {
797
+ return left.trim().replace(/\.$/, "").toLowerCase() === right.trim().replace(/\.$/, "").toLowerCase();
798
+ }
799
+ function toAppDomainSummary(domain) {
800
+ return {
801
+ id: domain.id,
802
+ type: domain.type,
803
+ url: domain.url,
804
+ hostname: domain.hostname,
805
+ computeServiceId: domain.computeServiceId,
806
+ status: domain.status,
807
+ foundryStatus: domain.foundryStatus,
808
+ failureReason: domain.failureReason,
809
+ failureCategory: domain.failureCategory,
810
+ certExpiresAt: domain.certExpiresAt,
811
+ createdAt: domain.createdAt,
812
+ updatedAt: domain.updatedAt,
813
+ dnsRecords: toAppDomainDnsRecords(domain)
814
+ };
815
+ }
816
+ function toAppDomainDnsRecords(domain) {
817
+ return domain.dnsRecords.map((record) => ({
818
+ type: record.type,
819
+ name: record.name,
820
+ value: record.value,
821
+ ttl: record.ttl
822
+ }));
823
+ }
824
+ function buildDomainShowNextSteps(domain) {
825
+ if (domain.status === "active") return [];
826
+ if (domain.status === "failed") return [`prisma-cli app domain retry ${domain.hostname}`];
827
+ return [`prisma-cli app domain wait ${domain.hostname}`];
828
+ }
829
+ async function confirmDomainRemoval(context, target, hostname) {
830
+ if (context.flags.yes) return;
831
+ if (!canPrompt(context)) throw new CliError({
832
+ code: "CONFIRMATION_REQUIRED",
833
+ domain: "app",
834
+ summary: "Custom domain removal requires confirmation in the current mode",
835
+ why: "This command detaches a domain and cannot prompt for confirmation in the current mode.",
836
+ fix: `Pass --yes to confirm removal of "${hostname}", or rerun prisma-cli app domain remove in an interactive TTY.`,
837
+ exitCode: 1,
838
+ nextSteps: [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`]
839
+ });
840
+ if (!await confirmPrompt({
841
+ input: context.runtime.stdin,
842
+ output: context.output.stderr,
843
+ message: `Detach ${hostname} from App "${target.app.name}"?`,
844
+ initialValue: false
845
+ })) 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");
846
+ }
847
+ function domainCommandError(command, error, hostname) {
848
+ if (error instanceof PreviewDomainApiError) {
849
+ if (command === "add" && (error.status === 400 || error.status === 422) && isDomainDnsError(error)) return domainDnsNotConfiguredError(hostname, error);
850
+ if (command === "add" && error.status === 400) return new CliError({
851
+ code: "DOMAIN_HOSTNAME_INVALID",
852
+ domain: "app",
853
+ summary: `Invalid custom domain "${hostname}"`,
854
+ why: error.message,
855
+ fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
856
+ debug: formatDebugDetails(error),
857
+ exitCode: 2,
858
+ nextSteps: ["prisma-cli app domain add shop.acme.com"]
859
+ });
860
+ if (command === "add" && (error.status === 429 || isDomainQuotaError(error))) return new CliError({
861
+ code: "DOMAIN_QUOTA_EXCEEDED",
862
+ domain: "app",
863
+ summary: "Custom domain quota exceeded",
864
+ why: error.message,
865
+ fix: "Remove an existing custom domain before adding another one.",
866
+ debug: formatDebugDetails(error),
867
+ exitCode: 1,
868
+ nextSteps: ["prisma-cli app domain remove <hostname>"]
869
+ });
870
+ if (command === "add" && error.status === 409) return domainAlreadyRegisteredError(hostname, error);
871
+ if (command === "add" && error.status === 422) return new CliError({
872
+ code: "NO_DEPLOYMENTS",
873
+ domain: "app",
874
+ summary: "Custom domain requires a live production deployment",
875
+ why: "The selected production app does not have a promoted version that can receive a custom domain.",
876
+ fix: "Deploy the app to the production branch, then rerun the domain command.",
877
+ debug: formatDebugDetails(error),
878
+ exitCode: 1,
879
+ nextSteps: ["prisma-cli app deploy --branch production", `prisma-cli app domain add ${hostname}`]
880
+ });
881
+ if ((command === "show" || command === "remove" || command === "retry" || command === "wait") && error.status === 404) return domainNotFoundError(hostname);
882
+ if (command === "retry" && error.status === 409) return new CliError({
883
+ code: "DOMAIN_RETRY_NOT_ELIGIBLE",
884
+ domain: "app",
885
+ summary: `Custom domain "${hostname}" is not eligible for retry`,
886
+ why: error.message,
887
+ fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
888
+ debug: formatDebugDetails(error),
889
+ exitCode: 1,
890
+ nextSteps: [`prisma-cli app domain show ${hostname}`]
891
+ });
892
+ }
893
+ return new CliError({
894
+ code: "DEPLOY_FAILED",
895
+ domain: "app",
896
+ summary: `Custom domain ${command} failed`,
897
+ why: error instanceof Error ? error.message : String(error),
898
+ fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
899
+ debug: formatDebugDetails(error),
900
+ exitCode: 1,
901
+ nextSteps: [`prisma-cli app domain show ${hostname}`]
902
+ });
903
+ }
904
+ function isDomainQuotaError(error) {
905
+ if (error.status !== 409) return false;
906
+ const text = `${error.message} ${error.hint ?? ""}`.toLowerCase();
907
+ return text.includes("quota") || text.includes("maximum") || text.includes("limit");
908
+ }
909
+ function domainAlreadyRegisteredError(hostname, error) {
910
+ return new CliError({
911
+ code: "DOMAIN_ALREADY_REGISTERED",
912
+ domain: "app",
913
+ summary: `Custom domain "${hostname}" is already registered`,
914
+ why: error.hint ?? error.message,
915
+ fix: "Select the app that owns this hostname and remove it there, or contact support if you cannot access it.",
916
+ debug: formatDebugDetails(error),
917
+ exitCode: 1,
918
+ nextSteps: [`Select the owning app and remove ${hostname} there.`, "Contact Prisma support if you cannot access the owning app."]
919
+ });
920
+ }
921
+ function isDomainDnsError(error) {
922
+ const text = `${error.message} ${error.hint ?? ""}`.toLowerCase();
923
+ 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);
924
+ }
925
+ function domainDnsNotConfiguredError(hostname, error) {
926
+ const target = extractDomainDnsTarget(error);
927
+ const record = target ? `CNAME ${hostname} -> ${target}` : null;
928
+ return new CliError({
929
+ code: "DOMAIN_DNS_NOT_CONFIGURED",
930
+ domain: "app",
931
+ summary: `DNS is not configured for "${hostname}"`,
932
+ why: error.hint ?? error.message,
933
+ 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.",
934
+ debug: formatDebugDetails(error),
935
+ exitCode: 1,
936
+ nextSteps: record ? [`add ${record}`, `prisma-cli app domain add ${hostname}`] : [`prisma-cli app domain add ${hostname} --trace`]
937
+ });
938
+ }
939
+ function extractDomainDnsTarget(error) {
940
+ const text = `${error.hint ?? ""} ${error.message}`;
941
+ return /\b((?:[a-z0-9-]+\.)+prisma\.build)\b/i.exec(text)?.[1]?.toLowerCase() ?? null;
942
+ }
943
+ function domainNotFoundError(hostname) {
944
+ return new CliError({
945
+ code: "DOMAIN_NOT_FOUND",
946
+ domain: "app",
947
+ summary: `Custom domain "${hostname}" not found`,
948
+ why: "The hostname is not attached to the selected app.",
949
+ fix: "Check the hostname and selected app, or add the domain first.",
950
+ exitCode: 1,
951
+ nextSteps: [`prisma-cli app domain add ${hostname}`]
952
+ });
953
+ }
954
+ function formatDomainFailureWhy(domain) {
955
+ if (domain.failureReason) return domain.failureCategory ? `${domain.failureCategory}: ${domain.failureReason}` : domain.failureReason;
956
+ return "The platform reported a terminal failed state for this custom domain.";
957
+ }
958
+ function parseDomainWaitTimeout(value) {
959
+ if (!value) return 900 * 1e3;
960
+ const trimmed = value.trim().toLowerCase();
961
+ if (trimmed === "0") return 0;
962
+ const match = /^(\d+)(ms|s|m|h)$/.exec(trimmed);
963
+ 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");
964
+ const amount = Number.parseInt(match[1], 10);
965
+ const unit = match[2];
966
+ return amount * (unit === "h" ? 3600 * 1e3 : unit === "m" ? 60 * 1e3 : unit === "s" ? 1e3 : 1);
967
+ }
968
+ function readDomainWaitPollIntervalMs(context) {
969
+ const raw = context.runtime.env.PRISMA_CLI_DOMAIN_WAIT_POLL_MS;
970
+ if (!raw) return 5e3;
971
+ const parsed = Number.parseInt(raw, 10);
972
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 5e3;
973
+ }
974
+ function emitDomainWaitStatus(context, event) {
975
+ if (context.flags.json) {
976
+ writeJsonEvent(context.output, {
977
+ type: "status",
978
+ command: "app.domain.wait",
979
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
980
+ data: {
981
+ hostname: event.hostname,
982
+ domainId: event.domainId,
983
+ previousStatus: event.previousStatus,
984
+ status: event.status,
985
+ elapsedMs: event.elapsedMs
986
+ }
987
+ });
988
+ return;
989
+ }
990
+ if (context.flags.quiet) return;
991
+ if (event.previousStatus === event.status) return;
992
+ const transition = event.previousStatus ? `${event.previousStatus} -> ${event.status}` : event.status;
993
+ context.output.stderr.write(` ${transition} (${formatElapsed(event.elapsedMs)})\n`);
994
+ }
995
+ function formatElapsed(milliseconds) {
996
+ const seconds = Math.max(Math.floor(milliseconds / 1e3), 0);
997
+ const minutes = Math.floor(seconds / 60);
998
+ const remainingSeconds = seconds % 60;
999
+ return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
1000
+ }
1001
+ async function sleep(milliseconds) {
1002
+ if (milliseconds <= 0) return;
1003
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
1004
+ }
711
1005
  async function resolveDeployAppSelection(context, projectId, apps, options) {
712
1006
  if (options.explicitAppName) {
713
1007
  const matches = findAppsByName(apps, options.explicitAppName);
@@ -1671,21 +1965,5 @@ function sortApps(apps) {
1671
1965
  function toOptionalEnvVars(envVars) {
1672
1966
  return Object.keys(envVars).length > 0 ? envVars : void 0;
1673
1967
  }
1674
- /**
1675
- * Emits a deprecation banner to stderr when the legacy single-shot
1676
- * env-var commands are invoked. The banner is suppressed in --json
1677
- * mode so machine consumers keep their JSON channel clean; --json
1678
- * users discover the deprecation via release notes and the new
1679
- * `prisma-cli project env` namespace's output anyway.
1680
- *
1681
- * Removal of these legacy commands is deliberately scoped out of the
1682
- * Public Beta — see the Compute Beta plan, sub-track 3B.1, where the
1683
- * Terminal team picks an explicit removal milestone.
1684
- */
1685
- function emitLegacyEnvDeprecationWarning(context, legacyCommand, replacement) {
1686
- if (context.flags.json) return;
1687
- const message = `[deprecation] \`prisma-cli ${legacyCommand}\` is deprecated. Use \`prisma-cli ${replacement}\` instead.`;
1688
- context.runtime.stderr.write(`${message}\n`);
1689
- }
1690
1968
  //#endregion
1691
- export { runAppBuild, runAppDeploy, runAppListDeploys, runAppListEnv, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy, runAppUpdateEnv };
1969
+ export { runAppBuild, runAppDeploy, runAppDomainAdd, runAppDomainRemove, runAppDomainRetry, runAppDomainShow, runAppDomainWait, runAppListDeploys, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy };
@@ -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 };