@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.
@@ -3,10 +3,11 @@ import { usageError } from "../../shell/errors.js";
3
3
  const VALID_ROLES = new Set(["production", "preview"]);
4
4
  function positionalHint(command) {
5
5
  if (command === "add" || command === "update") return "KEY=value ";
6
- if (command === "rm") return "KEY ";
6
+ if (command === "remove") return "KEY ";
7
7
  return "";
8
8
  }
9
9
  function resolveEnvScope(flags, options) {
10
+ if (flags.roleName && flags.branchName) throw usageError(`prisma-cli project env ${options.command} accepts either --role or --branch`, "--role targets a project-level config map; --branch targets a preview branch override.", "Pass exactly one scope flag.", [`prisma-cli project env ${options.command} ${positionalHint(options.command)}--role preview`, `prisma-cli project env ${options.command} ${positionalHint(options.command)}--branch feature/foo`], "app");
10
11
  if (flags.roleName) {
11
12
  if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
12
13
  return {
@@ -14,9 +15,17 @@ function resolveEnvScope(flags, options) {
14
15
  role: flags.roleName
15
16
  };
16
17
  }
18
+ if (flags.branchName) return {
19
+ kind: "branch",
20
+ branchName: flags.branchName
21
+ };
17
22
  if (options.requireExplicit) {
18
23
  const positional = positionalHint(options.command);
19
- throw usageError(`prisma-cli project env ${options.command} requires --role`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} ${positional}--role production`, `prisma-cli project env ${options.command} ${positional}--role preview`], "app");
24
+ throw usageError(`prisma-cli project env ${options.command} requires --role or --branch`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production, --role preview, or --branch <git-name>.", [
25
+ `prisma-cli project env ${options.command} ${positional}--role production`,
26
+ `prisma-cli project env ${options.command} ${positional}--role preview`,
27
+ `prisma-cli project env ${options.command} ${positional}--branch feature/foo`
28
+ ], "app");
20
29
  }
21
30
  return null;
22
31
  }
@@ -38,7 +47,7 @@ function parseKeyValuePositional(raw, command, env = process.env) {
38
47
  const key = raw.slice(0, separatorIndex);
39
48
  const value = raw.slice(separatorIndex + 1);
40
49
  validateKey(key, command);
41
- if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env rm to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
50
+ if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env remove to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
42
51
  return {
43
52
  key,
44
53
  value
@@ -46,12 +55,13 @@ function parseKeyValuePositional(raw, command, env = process.env) {
46
55
  }
47
56
  const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
48
57
  function validateKey(key, command) {
49
- if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
58
+ if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
50
59
  if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
51
- if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
60
+ if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
52
61
  }
53
62
  function formatScopeLabel(scope) {
54
- return scope.role;
63
+ if (scope.kind === "role") return scope.role;
64
+ return `branch:${scope.branchName}`;
55
65
  }
56
66
  //#endregion
57
67
  export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
@@ -46,7 +46,7 @@ async function executePreviewBuild(options) {
46
46
  });
47
47
  const artifact = await strategy.execute();
48
48
  try {
49
- if (buildType === "nextjs") await restageNextjsArtifact(artifact.directory, options.appPath);
49
+ if (buildType === "nextjs") await restageNextjsArtifact(artifact, options.appPath);
50
50
  await normalizeArtifactSymlinks(artifact.directory, options.appPath);
51
51
  return {
52
52
  artifact,
@@ -114,8 +114,10 @@ async function stageNextjsStandaloneArtifact(options) {
114
114
  appRoot,
115
115
  sourceRoot: await resolveSourceRoot(appRoot)
116
116
  });
117
+ await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
117
118
  }
118
- async function restageNextjsArtifact(artifactDir, appPath) {
119
+ async function restageNextjsArtifact(artifact, appPath) {
120
+ const artifactDir = artifact.directory;
119
121
  const standaloneDir = path.join(appPath, ".next", "standalone");
120
122
  await rm(artifactDir, {
121
123
  recursive: true,
@@ -126,17 +128,52 @@ async function restageNextjsArtifact(artifactDir, appPath) {
126
128
  artifactDir,
127
129
  appPath
128
130
  });
131
+ const serverSubpath = nextjsServerSubpath(artifact.entrypoint);
132
+ const serverDir = serverSubpath ? path.join(artifactDir, serverSubpath) : artifactDir;
129
133
  const publicDir = path.join(appPath, "public");
130
- if (await directoryExists(publicDir)) await cp(publicDir, path.join(artifactDir, "public"), {
134
+ if (await directoryExists(publicDir)) await cp(publicDir, path.join(serverDir, "public"), {
131
135
  recursive: true,
132
136
  verbatimSymlinks: true
133
137
  });
134
138
  const staticDir = path.join(appPath, ".next", "static");
135
- if (await directoryExists(staticDir)) await cp(staticDir, path.join(artifactDir, ".next", "static"), {
139
+ if (await directoryExists(staticDir)) await cp(staticDir, path.join(serverDir, ".next", "static"), {
136
140
  recursive: true,
137
141
  verbatimSymlinks: true
138
142
  });
139
143
  }
144
+ function nextjsServerSubpath(entrypoint) {
145
+ const dir = path.posix.dirname(entrypoint);
146
+ return dir === "." ? "" : dir;
147
+ }
148
+ async function hoistPnpmDependencies(nodeModulesDir) {
149
+ const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
150
+ if (!await directoryExists(pnpmNodeModulesDir)) return;
151
+ const entries = await readdir(pnpmNodeModulesDir, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
154
+ if (entry.name.startsWith("@") && entry.isDirectory()) {
155
+ const scopedEntries = await readdir(sourcePath, { withFileTypes: true });
156
+ for (const scopedEntry of scopedEntries) {
157
+ const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name);
158
+ if (await pathExists(scopedDestination)) continue;
159
+ await mkdir(path.dirname(scopedDestination), { recursive: true });
160
+ await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
161
+ standaloneRoot: pnpmNodeModulesDir,
162
+ appRoot: nodeModulesDir,
163
+ sourceRoot: nodeModulesDir
164
+ });
165
+ }
166
+ continue;
167
+ }
168
+ const destinationPath = path.join(nodeModulesDir, entry.name);
169
+ if (await pathExists(destinationPath)) continue;
170
+ await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
171
+ standaloneRoot: pnpmNodeModulesDir,
172
+ appRoot: nodeModulesDir,
173
+ sourceRoot: nodeModulesDir
174
+ });
175
+ }
176
+ }
140
177
  async function normalizeArtifactSymlinks(artifactDir, appPath) {
141
178
  const normalizedArtifactDir = path.resolve(artifactDir);
142
179
  const normalizedAppPath = path.resolve(appPath);
@@ -174,7 +211,9 @@ function isPathWithin(rootPath, candidatePath) {
174
211
  async function copyPathMaterializingSymlinks(sourcePath, destinationPath, options) {
175
212
  const sourceStat = await lstat(sourcePath);
176
213
  if (sourceStat.isSymbolicLink()) {
177
- await copyPathMaterializingSymlinks(await resolveSymlinkTarget(sourcePath, options), destinationPath, options);
214
+ const resolvedTarget = await resolveSymlinkTarget(sourcePath, options);
215
+ if (resolvedTarget === null) return;
216
+ await copyPathMaterializingSymlinks(resolvedTarget, destinationPath, options);
178
217
  return;
179
218
  }
180
219
  if (sourceStat.isDirectory()) {
@@ -200,8 +239,14 @@ async function resolveSymlinkTarget(symlinkPath, options) {
200
239
  const fallbackTarget = path.join(options.appRoot, path.relative(options.standaloneRoot, resolvedTarget));
201
240
  if (await pathExists(fallbackTarget)) return fallbackTarget;
202
241
  }
242
+ if (isPnpmHoistLink(symlinkPath)) return null;
203
243
  throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
204
244
  }
245
+ function isPnpmHoistLink(symlinkPath) {
246
+ const parts = path.dirname(symlinkPath).split(path.sep);
247
+ for (let i = 0; i < parts.length - 1; i++) if (parts[i] === ".pnpm" && parts[i + 1] === "node_modules") return true;
248
+ return false;
249
+ }
205
250
  async function pathExists(targetPath) {
206
251
  try {
207
252
  await stat(targetPath);
@@ -96,29 +96,5 @@ function createPreviewPromoteProgress(output, enabled) {
96
96
  }
97
97
  };
98
98
  }
99
- function createPreviewUpdateEnvProgress(output, enabled) {
100
- if (!enabled) return;
101
- const write = (line) => {
102
- output.write(`${line}\n`);
103
- };
104
- return {
105
- onVersionCreated(versionId) {
106
- write(`Creating updated deployment ${versionId}...`);
107
- },
108
- onStartRequested() {
109
- write("Starting deployment...");
110
- },
111
- onStatusChange(status) {
112
- write(`Status: ${status}`);
113
- },
114
- onRunning(url) {
115
- if (url) {
116
- write(`Deployment is running at ${url}.`);
117
- return;
118
- }
119
- write("Deployment is running.");
120
- }
121
- };
122
- }
123
99
  //#endregion
124
- export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress };
100
+ export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress };
@@ -3,6 +3,18 @@ import { PreviewBuildStrategy } from "./preview-build.js";
3
3
  import path from "node:path";
4
4
  import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
5
5
  //#region src/lib/app/preview-provider.ts
6
+ var PreviewDomainApiError = class extends Error {
7
+ status;
8
+ code;
9
+ hint;
10
+ constructor(options) {
11
+ super(`${options.summary}: ${options.message}${options.hint ? ` ${options.hint}` : ""}`);
12
+ this.name = "PreviewDomainApiError";
13
+ this.status = options.status;
14
+ this.code = options.code ?? null;
15
+ this.hint = options.hint ?? null;
16
+ }
17
+ };
6
18
  function createPreviewAppProvider(client, options) {
7
19
  const sdk = new ComputeClient(client);
8
20
  return {
@@ -35,6 +47,43 @@ function createPreviewAppProvider(client, options) {
35
47
  name: appResult.value.name
36
48
  };
37
49
  },
50
+ async listDomains(appId) {
51
+ return listComputeServiceDomains(client, appId);
52
+ },
53
+ async addDomain(options) {
54
+ const result = await client.POST("/v1/compute-services/{computeServiceId}/domains", {
55
+ params: { path: { computeServiceId: options.appId } },
56
+ body: { hostname: options.hostname }
57
+ });
58
+ if (result.error || !result.data) {
59
+ if (result.response.status === 409) {
60
+ const existing = (await listComputeServiceDomains(client, options.appId)).find((domain) => sameHostname(domain.hostname, options.hostname));
61
+ if (existing) return {
62
+ domain: existing,
63
+ existing: true
64
+ };
65
+ }
66
+ throw domainApiCallError("Failed to add custom domain", result.response, result.error);
67
+ }
68
+ return {
69
+ domain: normalizeDomainRecord(result.data.data),
70
+ existing: false
71
+ };
72
+ },
73
+ async showDomain(domainId) {
74
+ const result = await client.GET("/v1/domains/{domainId}", { params: { path: { domainId } } });
75
+ if (result.error || !result.data) throw domainApiCallError("Failed to show custom domain", result.response, result.error);
76
+ return normalizeDomainRecord(result.data.data);
77
+ },
78
+ async removeDomain(domainId) {
79
+ const result = await client.DELETE("/v1/domains/{domainId}", { params: { path: { domainId } } });
80
+ if (result.error) throw domainApiCallError("Failed to remove custom domain", result.response, result.error);
81
+ },
82
+ async retryDomain(domainId) {
83
+ const result = await client.POST("/v1/domains/{domainId}/retry", { params: { path: { domainId } } });
84
+ if (result.error || !result.data) throw domainApiCallError("Failed to retry custom domain", result.response, result.error);
85
+ return normalizeDomainRecord(result.data.data);
86
+ },
38
87
  async promoteDeployment(options) {
39
88
  const promoteResult = await sdk.promote({
40
89
  serviceId: options.appId,
@@ -263,6 +312,46 @@ async function listComputeServices(client, options) {
263
312
  liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
264
313
  }));
265
314
  }
315
+ async function listComputeServiceDomains(client, computeServiceId) {
316
+ const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", { params: { path: { computeServiceId } } });
317
+ if (result.error || !result.data) throw domainApiCallError("Failed to list custom domains", result.response, result.error);
318
+ return result.data.data.map((domain) => normalizeDomainRecord(domain));
319
+ }
320
+ function normalizeDomainRecord(domain) {
321
+ return {
322
+ id: domain.id,
323
+ type: domain.type,
324
+ url: domain.url,
325
+ hostname: domain.hostname,
326
+ computeServiceId: domain.computeServiceId,
327
+ status: domain.status,
328
+ foundryStatus: domain.foundryStatus,
329
+ failureReason: domain.failureReason,
330
+ failureCategory: domain.failureCategory,
331
+ certExpiresAt: domain.certExpiresAt,
332
+ createdAt: domain.createdAt,
333
+ updatedAt: domain.updatedAt,
334
+ dnsRecords: normalizeDomainDnsRecords(domain.dnsRecords)
335
+ };
336
+ }
337
+ function normalizeDomainDnsRecords(records) {
338
+ if (!Array.isArray(records)) return [];
339
+ return records.map((record) => {
340
+ if (typeof record.type !== "string" || typeof record.name !== "string" || typeof record.value !== "string") return null;
341
+ return {
342
+ type: record.type,
343
+ name: record.name,
344
+ value: record.value,
345
+ ttl: typeof record.ttl === "number" ? record.ttl : null
346
+ };
347
+ }).filter((record) => Boolean(record));
348
+ }
349
+ function sameHostname(left, right) {
350
+ return normalizeHostnameForComparison(left) === normalizeHostnameForComparison(right);
351
+ }
352
+ function normalizeHostnameForComparison(hostname) {
353
+ return hostname.trim().replace(/\.$/, "").toLowerCase();
354
+ }
266
355
  async function createBranchApp(client, options) {
267
356
  const branch = await resolveOrCreateBranch(client, {
268
357
  projectId: options.projectId,
@@ -301,6 +390,15 @@ function apiCallError(summary, response, error) {
301
390
  const hint = error.error?.hint ? ` ${error.error.hint}` : "";
302
391
  return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
303
392
  }
393
+ function domainApiCallError(summary, response, error) {
394
+ return new PreviewDomainApiError({
395
+ summary,
396
+ status: response.status,
397
+ code: error.error?.code ?? null,
398
+ message: error.error?.message ?? `Management API returned HTTP ${response.status}.`,
399
+ hint: error.error?.hint ?? null
400
+ });
401
+ }
304
402
  async function findAppForDeployment(sdk, deploymentId) {
305
403
  const projectsResult = await sdk.listProjects();
306
404
  if (projectsResult.isErr()) throw new Error(projectsResult.error.message);
@@ -330,4 +428,4 @@ function toAbsoluteUrl(url) {
330
428
  return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
331
429
  }
332
430
  //#endregion
333
- export { createPreviewAppProvider };
431
+ export { PreviewDomainApiError, createPreviewAppProvider };
@@ -1,7 +1,8 @@
1
1
  import { renderList, renderShow, serializeList } from "../output/patterns.js";
2
2
  //#region src/presenters/app-env.ts
3
3
  function scopeLabel(scope) {
4
- return scope.role;
4
+ if (scope.kind === "role") return scope.role ?? "unknown";
5
+ return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
5
6
  }
6
7
  function renderEnvAdd(context, descriptor, result) {
7
8
  return renderShow({
@@ -79,7 +80,7 @@ function renderEnvList(context, descriptor, result) {
79
80
  },
80
81
  items: result.variables.map((variable) => ({
81
82
  noun: "variable",
82
- label: variable.key,
83
+ label: `${variable.key} (${variable.source})`,
83
84
  id: variable.id,
84
85
  status: variable.isManagedBySystem ? "default" : null
85
86
  })),
@@ -94,7 +95,7 @@ function serializeEnvList(result) {
94
95
  context: { scope: scopeLabel(result.scope) },
95
96
  items: result.variables.map((variable) => ({
96
97
  noun: "variable",
97
- label: variable.key,
98
+ label: `${variable.key} (${variable.source})`,
98
99
  id: variable.id,
99
100
  status: variable.isManagedBySystem ? "default" : null
100
101
  }))
@@ -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) {
@@ -44,74 +45,6 @@ function formatDuration(durationMs) {
44
45
  if (durationMs < 1e3) return `${durationMs}ms`;
45
46
  return `${(durationMs / 1e3).toFixed(1)}s`;
46
47
  }
47
- function renderAppUpdateEnv(context, descriptor, result) {
48
- return renderShow({
49
- title: "Updating environment variables for the selected app.",
50
- descriptor,
51
- fields: [
52
- {
53
- key: "project",
54
- value: result.projectId
55
- },
56
- {
57
- key: "app",
58
- value: result.app.name
59
- },
60
- {
61
- key: "deployment",
62
- value: result.deployment.id
63
- },
64
- {
65
- key: "status",
66
- value: result.deployment.status,
67
- tone: toneForStatus(result.deployment.status)
68
- },
69
- ...result.deployment.url ? [{
70
- key: "url",
71
- value: result.deployment.url,
72
- tone: "link"
73
- }] : [],
74
- {
75
- key: "variables",
76
- value: formatVariableNames(result.variables),
77
- tone: result.variables.length > 0 ? "default" : "dim"
78
- }
79
- ]
80
- }, context.ui);
81
- }
82
- function serializeAppUpdateEnv(result) {
83
- return result;
84
- }
85
- function renderAppListEnv(context, descriptor, result) {
86
- return renderShow({
87
- title: "Listing environment variables for the selected app.",
88
- descriptor,
89
- fields: [
90
- {
91
- key: "project",
92
- value: result.projectId
93
- },
94
- {
95
- key: "app",
96
- value: result.app?.name ?? "not selected",
97
- tone: result.app ? "default" : "dim"
98
- },
99
- {
100
- key: "deployment",
101
- value: result.deployment?.id ?? "none",
102
- tone: result.deployment ? toneForStatus(result.deployment.status) : "dim"
103
- },
104
- {
105
- key: "variables",
106
- value: formatVariableNames(result.variables),
107
- tone: result.variables.length > 0 ? "default" : "dim"
108
- }
109
- ]
110
- }, context.ui);
111
- }
112
- function serializeAppListEnv(result) {
113
- return result;
114
- }
115
48
  function renderAppListDeploys(context, descriptor, result) {
116
49
  return renderList({
117
50
  title: "Listing deployments for the selected app.",
@@ -253,6 +186,105 @@ function renderAppOpen(context, descriptor, result) {
253
186
  function serializeAppOpen(result) {
254
187
  return result;
255
188
  }
189
+ function renderAppDomainAdd(context, descriptor, result) {
190
+ return renderShow({
191
+ title: result.existing ? "Showing the existing custom domain for the selected app." : "Adding a custom domain to the selected app.",
192
+ descriptor,
193
+ fields: [
194
+ ...domainTargetFields(result),
195
+ {
196
+ key: "hostname",
197
+ value: result.domain.hostname
198
+ },
199
+ {
200
+ key: "status",
201
+ value: result.domain.status,
202
+ tone: toneForDomainStatus(result.domain.status)
203
+ },
204
+ ...domainDnsFields(result.domain)
205
+ ]
206
+ }, context.ui);
207
+ }
208
+ function serializeAppDomainAdd(result) {
209
+ return result;
210
+ }
211
+ function renderAppDomainShow(context, descriptor, result) {
212
+ return renderShow({
213
+ title: "Showing custom domain status.",
214
+ descriptor,
215
+ fields: [
216
+ ...domainTargetFields(result),
217
+ {
218
+ key: "hostname",
219
+ value: result.domain.hostname
220
+ },
221
+ {
222
+ key: "status",
223
+ value: result.domain.status,
224
+ tone: toneForDomainStatus(result.domain.status)
225
+ },
226
+ ...domainFailureFields(result.domain),
227
+ {
228
+ key: "cert expires",
229
+ value: formatOptionalUtcDate(result.domain.certExpiresAt),
230
+ tone: result.domain.certExpiresAt ? "default" : "dim"
231
+ },
232
+ {
233
+ key: "created",
234
+ value: formatUtcDate(result.domain.createdAt),
235
+ tone: "dim"
236
+ },
237
+ ...domainDnsFields(result.domain)
238
+ ]
239
+ }, context.ui);
240
+ }
241
+ function serializeAppDomainShow(result) {
242
+ return result;
243
+ }
244
+ function renderAppDomainRemove(context, descriptor, result) {
245
+ return renderShow({
246
+ title: "Removing a custom domain from the selected app.",
247
+ descriptor,
248
+ fields: [
249
+ ...domainTargetFields(result),
250
+ {
251
+ key: "hostname",
252
+ value: result.hostname
253
+ },
254
+ {
255
+ key: "removed",
256
+ value: result.removed ? "yes" : "no",
257
+ tone: result.removed ? "success" : "dim"
258
+ }
259
+ ]
260
+ }, context.ui);
261
+ }
262
+ function serializeAppDomainRemove(result) {
263
+ return result;
264
+ }
265
+ function renderAppDomainRetry(context, descriptor, result) {
266
+ return renderShow({
267
+ title: "Retrying custom domain verification.",
268
+ descriptor,
269
+ fields: [
270
+ ...domainTargetFields(result),
271
+ {
272
+ key: "hostname",
273
+ value: result.domain.hostname
274
+ },
275
+ {
276
+ key: "status",
277
+ value: result.domain.status,
278
+ tone: toneForDomainStatus(result.domain.status)
279
+ },
280
+ ...domainFailureFields(result.domain),
281
+ ...domainDnsFields(result.domain)
282
+ ]
283
+ }, context.ui);
284
+ }
285
+ function serializeAppDomainRetry(result) {
286
+ return result;
287
+ }
256
288
  function renderAppPromote(context, descriptor, result) {
257
289
  return renderShow({
258
290
  title: "Switching the live deployment for the selected app.",
@@ -380,13 +412,80 @@ function toneForStatus(status) {
380
412
  if (status === "failed" || status === "error") return "error";
381
413
  return "default";
382
414
  }
415
+ function toneForDomainStatus(status) {
416
+ if (status === "active") return "success";
417
+ if (status === "failed") return "error";
418
+ if (status === "pending_dns" || status === "verifying" || status === "provisioning_tls" || status === "verified_routing_blocked") return "warning";
419
+ return "default";
420
+ }
421
+ function domainTargetFields(result) {
422
+ return [
423
+ {
424
+ key: "workspace",
425
+ value: result.workspace.name
426
+ },
427
+ {
428
+ key: "project",
429
+ value: result.project.name
430
+ },
431
+ {
432
+ key: "branch",
433
+ value: result.branch.name
434
+ },
435
+ {
436
+ key: "app",
437
+ value: result.app.name
438
+ }
439
+ ];
440
+ }
441
+ function domainDnsFields(domain) {
442
+ const records = domain.dnsRecords;
443
+ if (records.length === 0) return [{
444
+ key: "dns record",
445
+ value: "not provided by platform",
446
+ tone: "dim"
447
+ }];
448
+ return [{
449
+ key: "dns record",
450
+ value: records.map((record) => {
451
+ const ttl = record.ttl ? ` ttl ${record.ttl}` : "";
452
+ return `${record.type} ${record.name} -> ${record.value}${ttl}`;
453
+ }).join(", ")
454
+ }];
455
+ }
456
+ function formatDomainFailure(domain) {
457
+ if (!domain.failureReason) return domain.failureCategory ?? "none";
458
+ return domain.failureCategory ? `${domain.failureCategory} - ${domain.failureReason}` : domain.failureReason;
459
+ }
460
+ function domainFailureFields(domain) {
461
+ const tone = hasDomainFailure(domain) ? "error" : "dim";
462
+ return [{
463
+ key: "failure",
464
+ value: formatDomainFailure(domain),
465
+ tone
466
+ }, ...domainFixFields(domain)];
467
+ }
468
+ function hasDomainFailure(domain) {
469
+ return Boolean(domain.failureCategory || domain.failureReason);
470
+ }
471
+ function domainFixFields(domain) {
472
+ const fix = formatDomainFailureFix(domain);
473
+ return fix ? [{
474
+ key: "fix",
475
+ value: fix
476
+ }] : [];
477
+ }
478
+ function formatOptionalUtcDate(value) {
479
+ return value ? formatUtcDate(value) : "-";
480
+ }
481
+ function formatUtcDate(value) {
482
+ const date = new Date(value);
483
+ if (Number.isNaN(date.getTime())) return value;
484
+ 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`;
485
+ }
383
486
  function formatRecentDeployments(deployments) {
384
487
  if (deployments.length === 0) return "none";
385
488
  return deployments.map((deployment) => `${deployment.id}${deployment.live ? " (live)" : ""}`).join(", ");
386
489
  }
387
- function formatVariableNames(variables) {
388
- if (variables.length === 0) return "none";
389
- return variables.join(", ");
390
- }
391
490
  //#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 };
491
+ export { renderAppBuild, renderAppDeploy, renderAppDomainAdd, renderAppDomainRemove, renderAppDomainRetry, renderAppDomainShow, renderAppListDeploys, renderAppOpen, renderAppPromote, renderAppRemove, renderAppRollback, renderAppRun, renderAppShow, renderAppShowDeploy, serializeAppBuild, serializeAppDeploy, serializeAppDomainAdd, serializeAppDomainRemove, serializeAppDomainRetry, serializeAppDomainShow, serializeAppListDeploys, serializeAppOpen, serializeAppPromote, serializeAppRemove, serializeAppRollback, serializeAppRun, serializeAppShow, serializeAppShowDeploy };