@prisma/cli 3.0.0-alpha.8 → 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.
- package/README.md +119 -14
- package/dist/adapters/token-storage.js +57 -1
- package/dist/commands/app/index.js +113 -30
- package/dist/commands/env.js +17 -9
- package/dist/controllers/app-env.js +150 -24
- package/dist/controllers/app.js +427 -149
- package/dist/lib/app/domain-guidance.js +14 -0
- package/dist/lib/app/env-config.js +16 -6
- package/dist/lib/app/preview-build.js +143 -1
- package/dist/lib/app/preview-progress.js +1 -25
- package/dist/lib/app/preview-provider.js +99 -1
- package/dist/presenters/app-env.js +4 -3
- package/dist/presenters/app.js +172 -73
- package/dist/shell/command-meta.js +84 -21
- package/dist/shell/command-runner.js +18 -10
- package/dist/shell/errors.js +2 -1
- package/package.json +4 -4
|
@@ -3,10 +3,11 @@ import { usageError } from "../../shell/errors.js";
|
|
|
3
3
|
const VALID_ROLES = new Set(["production", "preview"]);
|
|
4
4
|
function positionalHint(command) {
|
|
5
5
|
if (command === "add" || command === "update") return "KEY=value ";
|
|
6
|
-
if (command === "
|
|
6
|
+
if (command === "remove") return "KEY ";
|
|
7
7
|
return "";
|
|
8
8
|
}
|
|
9
9
|
function resolveEnvScope(flags, options) {
|
|
10
|
+
if (flags.roleName && flags.branchName) throw usageError(`prisma-cli project env ${options.command} accepts either --role or --branch`, "--role targets a project-level config map; --branch targets a preview branch override.", "Pass exactly one scope flag.", [`prisma-cli project env ${options.command} ${positionalHint(options.command)}--role preview`, `prisma-cli project env ${options.command} ${positionalHint(options.command)}--branch feature/foo`], "app");
|
|
10
11
|
if (flags.roleName) {
|
|
11
12
|
if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
|
|
12
13
|
return {
|
|
@@ -14,9 +15,17 @@ function resolveEnvScope(flags, options) {
|
|
|
14
15
|
role: flags.roleName
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
if (flags.branchName) return {
|
|
19
|
+
kind: "branch",
|
|
20
|
+
branchName: flags.branchName
|
|
21
|
+
};
|
|
17
22
|
if (options.requireExplicit) {
|
|
18
23
|
const positional = positionalHint(options.command);
|
|
19
|
-
throw usageError(`prisma-cli project env ${options.command} requires --role`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production
|
|
24
|
+
throw usageError(`prisma-cli project env ${options.command} requires --role or --branch`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production, --role preview, or --branch <git-name>.", [
|
|
25
|
+
`prisma-cli project env ${options.command} ${positional}--role production`,
|
|
26
|
+
`prisma-cli project env ${options.command} ${positional}--role preview`,
|
|
27
|
+
`prisma-cli project env ${options.command} ${positional}--branch feature/foo`
|
|
28
|
+
], "app");
|
|
20
29
|
}
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
@@ -38,7 +47,7 @@ function parseKeyValuePositional(raw, command, env = process.env) {
|
|
|
38
47
|
const key = raw.slice(0, separatorIndex);
|
|
39
48
|
const value = raw.slice(separatorIndex + 1);
|
|
40
49
|
validateKey(key, command);
|
|
41
|
-
if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env
|
|
50
|
+
if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env remove to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
|
|
42
51
|
return {
|
|
43
52
|
key,
|
|
44
53
|
value
|
|
@@ -46,12 +55,13 @@ function parseKeyValuePositional(raw, command, env = process.env) {
|
|
|
46
55
|
}
|
|
47
56
|
const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
|
|
48
57
|
function validateKey(key, command) {
|
|
49
|
-
if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "
|
|
58
|
+
if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
|
|
50
59
|
if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
|
|
51
|
-
if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "
|
|
60
|
+
if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], "app");
|
|
52
61
|
}
|
|
53
62
|
function formatScopeLabel(scope) {
|
|
54
|
-
return scope.role;
|
|
63
|
+
if (scope.kind === "role") return scope.role;
|
|
64
|
+
return `branch:${scope.branchName}`;
|
|
55
65
|
}
|
|
56
66
|
//#endregion
|
|
57
67
|
export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveBunEntrypoint } from "./bun-project.js";
|
|
2
|
-
import { cp, readdir, readlink, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AstroBuild, BunBuild, NextjsBuild, NuxtBuild, TanstackStartBuild } from "@prisma/compute-sdk";
|
|
5
5
|
//#region src/lib/app/preview-build.ts
|
|
@@ -46,6 +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, options.appPath);
|
|
49
50
|
await normalizeArtifactSymlinks(artifact.directory, options.appPath);
|
|
50
51
|
return {
|
|
51
52
|
artifact,
|
|
@@ -104,6 +105,75 @@ async function createPreviewBuildStrategy(options) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
async function stageNextjsStandaloneArtifact(options) {
|
|
109
|
+
const standaloneRoot = path.resolve(options.standaloneDir);
|
|
110
|
+
const artifactRoot = path.resolve(options.artifactDir);
|
|
111
|
+
const appRoot = path.resolve(options.appPath);
|
|
112
|
+
await copyPathMaterializingSymlinks(standaloneRoot, artifactRoot, {
|
|
113
|
+
standaloneRoot,
|
|
114
|
+
appRoot,
|
|
115
|
+
sourceRoot: await resolveSourceRoot(appRoot)
|
|
116
|
+
});
|
|
117
|
+
await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
|
|
118
|
+
}
|
|
119
|
+
async function restageNextjsArtifact(artifact, appPath) {
|
|
120
|
+
const artifactDir = artifact.directory;
|
|
121
|
+
const standaloneDir = path.join(appPath, ".next", "standalone");
|
|
122
|
+
await rm(artifactDir, {
|
|
123
|
+
recursive: true,
|
|
124
|
+
force: true
|
|
125
|
+
});
|
|
126
|
+
await stageNextjsStandaloneArtifact({
|
|
127
|
+
standaloneDir,
|
|
128
|
+
artifactDir,
|
|
129
|
+
appPath
|
|
130
|
+
});
|
|
131
|
+
const serverSubpath = nextjsServerSubpath(artifact.entrypoint);
|
|
132
|
+
const serverDir = serverSubpath ? path.join(artifactDir, serverSubpath) : artifactDir;
|
|
133
|
+
const publicDir = path.join(appPath, "public");
|
|
134
|
+
if (await directoryExists(publicDir)) await cp(publicDir, path.join(serverDir, "public"), {
|
|
135
|
+
recursive: true,
|
|
136
|
+
verbatimSymlinks: true
|
|
137
|
+
});
|
|
138
|
+
const staticDir = path.join(appPath, ".next", "static");
|
|
139
|
+
if (await directoryExists(staticDir)) await cp(staticDir, path.join(serverDir, ".next", "static"), {
|
|
140
|
+
recursive: true,
|
|
141
|
+
verbatimSymlinks: true
|
|
142
|
+
});
|
|
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
|
+
}
|
|
107
177
|
async function normalizeArtifactSymlinks(artifactDir, appPath) {
|
|
108
178
|
const normalizedArtifactDir = path.resolve(artifactDir);
|
|
109
179
|
const normalizedAppPath = path.resolve(appPath);
|
|
@@ -138,5 +208,77 @@ function isPathWithin(rootPath, candidatePath) {
|
|
|
138
208
|
const relativePath = path.relative(rootPath, candidatePath);
|
|
139
209
|
return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
|
|
140
210
|
}
|
|
211
|
+
async function copyPathMaterializingSymlinks(sourcePath, destinationPath, options) {
|
|
212
|
+
const sourceStat = await lstat(sourcePath);
|
|
213
|
+
if (sourceStat.isSymbolicLink()) {
|
|
214
|
+
const resolvedTarget = await resolveSymlinkTarget(sourcePath, options);
|
|
215
|
+
if (resolvedTarget === null) return;
|
|
216
|
+
await copyPathMaterializingSymlinks(resolvedTarget, destinationPath, options);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (sourceStat.isDirectory()) {
|
|
220
|
+
await mkdir(destinationPath, { recursive: true });
|
|
221
|
+
const entries = await readdir(sourcePath, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) await copyPathMaterializingSymlinks(path.join(sourcePath, entry.name), path.join(destinationPath, entry.name), options);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (sourceStat.isFile()) {
|
|
226
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
227
|
+
await copyFile(sourcePath, destinationPath);
|
|
228
|
+
await chmod(destinationPath, sourceStat.mode);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function resolveSymlinkTarget(symlinkPath, options) {
|
|
232
|
+
const linkTarget = await readlink(symlinkPath);
|
|
233
|
+
const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget);
|
|
234
|
+
if (await pathExists(resolvedTarget)) {
|
|
235
|
+
if (!isPathWithin(options.appRoot, resolvedTarget) && !isPathWithin(options.sourceRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
|
|
236
|
+
return resolvedTarget;
|
|
237
|
+
}
|
|
238
|
+
if (isPathWithin(options.standaloneRoot, resolvedTarget)) {
|
|
239
|
+
const fallbackTarget = path.join(options.appRoot, path.relative(options.standaloneRoot, resolvedTarget));
|
|
240
|
+
if (await pathExists(fallbackTarget)) return fallbackTarget;
|
|
241
|
+
}
|
|
242
|
+
if (isPnpmHoistLink(symlinkPath)) return null;
|
|
243
|
+
throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
|
|
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
|
+
}
|
|
250
|
+
async function pathExists(targetPath) {
|
|
251
|
+
try {
|
|
252
|
+
await stat(targetPath);
|
|
253
|
+
return true;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function directoryExists(targetPath) {
|
|
259
|
+
try {
|
|
260
|
+
return (await stat(targetPath)).isDirectory();
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function resolveSourceRoot(appRoot) {
|
|
266
|
+
let current = path.resolve(appRoot);
|
|
267
|
+
while (true) {
|
|
268
|
+
if (await pathExists(path.join(current, ".git")) || await pathExists(path.join(current, "pnpm-workspace.yaml")) || await pathExists(path.join(current, "bun.lock")) || await pathExists(path.join(current, "bun.lockb")) || await packageJsonDeclaresWorkspaces(current)) return current;
|
|
269
|
+
const parent = path.dirname(current);
|
|
270
|
+
if (parent === current) return path.resolve(appRoot);
|
|
271
|
+
current = parent;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function packageJsonDeclaresWorkspaces(directory) {
|
|
275
|
+
try {
|
|
276
|
+
const content = await readFile(path.join(directory, "package.json"), "utf8");
|
|
277
|
+
const parsed = JSON.parse(content);
|
|
278
|
+
return Boolean(parsed.workspaces);
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
141
283
|
//#endregion
|
|
142
284
|
export { PREVIEW_BUILD_TYPES, PreviewBuildStrategy, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild };
|
|
@@ -96,29 +96,5 @@ function createPreviewPromoteProgress(output, enabled) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
function createPreviewUpdateEnvProgress(output, enabled) {
|
|
100
|
-
if (!enabled) return;
|
|
101
|
-
const write = (line) => {
|
|
102
|
-
output.write(`${line}\n`);
|
|
103
|
-
};
|
|
104
|
-
return {
|
|
105
|
-
onVersionCreated(versionId) {
|
|
106
|
-
write(`Creating updated deployment ${versionId}...`);
|
|
107
|
-
},
|
|
108
|
-
onStartRequested() {
|
|
109
|
-
write("Starting deployment...");
|
|
110
|
-
},
|
|
111
|
-
onStatusChange(status) {
|
|
112
|
-
write(`Status: ${status}`);
|
|
113
|
-
},
|
|
114
|
-
onRunning(url) {
|
|
115
|
-
if (url) {
|
|
116
|
-
write(`Deployment is running at ${url}.`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
write("Deployment is running.");
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
99
|
//#endregion
|
|
124
|
-
export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress
|
|
100
|
+
export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress };
|
|
@@ -3,6 +3,18 @@ import { PreviewBuildStrategy } from "./preview-build.js";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
|
|
5
5
|
//#region src/lib/app/preview-provider.ts
|
|
6
|
+
var PreviewDomainApiError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
code;
|
|
9
|
+
hint;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(`${options.summary}: ${options.message}${options.hint ? ` ${options.hint}` : ""}`);
|
|
12
|
+
this.name = "PreviewDomainApiError";
|
|
13
|
+
this.status = options.status;
|
|
14
|
+
this.code = options.code ?? null;
|
|
15
|
+
this.hint = options.hint ?? null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
6
18
|
function createPreviewAppProvider(client, options) {
|
|
7
19
|
const sdk = new ComputeClient(client);
|
|
8
20
|
return {
|
|
@@ -35,6 +47,43 @@ function createPreviewAppProvider(client, options) {
|
|
|
35
47
|
name: appResult.value.name
|
|
36
48
|
};
|
|
37
49
|
},
|
|
50
|
+
async listDomains(appId) {
|
|
51
|
+
return listComputeServiceDomains(client, appId);
|
|
52
|
+
},
|
|
53
|
+
async addDomain(options) {
|
|
54
|
+
const result = await client.POST("/v1/compute-services/{computeServiceId}/domains", {
|
|
55
|
+
params: { path: { computeServiceId: options.appId } },
|
|
56
|
+
body: { hostname: options.hostname }
|
|
57
|
+
});
|
|
58
|
+
if (result.error || !result.data) {
|
|
59
|
+
if (result.response.status === 409) {
|
|
60
|
+
const existing = (await listComputeServiceDomains(client, options.appId)).find((domain) => sameHostname(domain.hostname, options.hostname));
|
|
61
|
+
if (existing) return {
|
|
62
|
+
domain: existing,
|
|
63
|
+
existing: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
throw domainApiCallError("Failed to add custom domain", result.response, result.error);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
domain: normalizeDomainRecord(result.data.data),
|
|
70
|
+
existing: false
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
async showDomain(domainId) {
|
|
74
|
+
const result = await client.GET("/v1/domains/{domainId}", { params: { path: { domainId } } });
|
|
75
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to show custom domain", result.response, result.error);
|
|
76
|
+
return normalizeDomainRecord(result.data.data);
|
|
77
|
+
},
|
|
78
|
+
async removeDomain(domainId) {
|
|
79
|
+
const result = await client.DELETE("/v1/domains/{domainId}", { params: { path: { domainId } } });
|
|
80
|
+
if (result.error) throw domainApiCallError("Failed to remove custom domain", result.response, result.error);
|
|
81
|
+
},
|
|
82
|
+
async retryDomain(domainId) {
|
|
83
|
+
const result = await client.POST("/v1/domains/{domainId}/retry", { params: { path: { domainId } } });
|
|
84
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to retry custom domain", result.response, result.error);
|
|
85
|
+
return normalizeDomainRecord(result.data.data);
|
|
86
|
+
},
|
|
38
87
|
async promoteDeployment(options) {
|
|
39
88
|
const promoteResult = await sdk.promote({
|
|
40
89
|
serviceId: options.appId,
|
|
@@ -263,6 +312,46 @@ async function listComputeServices(client, options) {
|
|
|
263
312
|
liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
|
|
264
313
|
}));
|
|
265
314
|
}
|
|
315
|
+
async function listComputeServiceDomains(client, computeServiceId) {
|
|
316
|
+
const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", { params: { path: { computeServiceId } } });
|
|
317
|
+
if (result.error || !result.data) throw domainApiCallError("Failed to list custom domains", result.response, result.error);
|
|
318
|
+
return result.data.data.map((domain) => normalizeDomainRecord(domain));
|
|
319
|
+
}
|
|
320
|
+
function normalizeDomainRecord(domain) {
|
|
321
|
+
return {
|
|
322
|
+
id: domain.id,
|
|
323
|
+
type: domain.type,
|
|
324
|
+
url: domain.url,
|
|
325
|
+
hostname: domain.hostname,
|
|
326
|
+
computeServiceId: domain.computeServiceId,
|
|
327
|
+
status: domain.status,
|
|
328
|
+
foundryStatus: domain.foundryStatus,
|
|
329
|
+
failureReason: domain.failureReason,
|
|
330
|
+
failureCategory: domain.failureCategory,
|
|
331
|
+
certExpiresAt: domain.certExpiresAt,
|
|
332
|
+
createdAt: domain.createdAt,
|
|
333
|
+
updatedAt: domain.updatedAt,
|
|
334
|
+
dnsRecords: normalizeDomainDnsRecords(domain.dnsRecords)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function normalizeDomainDnsRecords(records) {
|
|
338
|
+
if (!Array.isArray(records)) return [];
|
|
339
|
+
return records.map((record) => {
|
|
340
|
+
if (typeof record.type !== "string" || typeof record.name !== "string" || typeof record.value !== "string") return null;
|
|
341
|
+
return {
|
|
342
|
+
type: record.type,
|
|
343
|
+
name: record.name,
|
|
344
|
+
value: record.value,
|
|
345
|
+
ttl: typeof record.ttl === "number" ? record.ttl : null
|
|
346
|
+
};
|
|
347
|
+
}).filter((record) => Boolean(record));
|
|
348
|
+
}
|
|
349
|
+
function sameHostname(left, right) {
|
|
350
|
+
return normalizeHostnameForComparison(left) === normalizeHostnameForComparison(right);
|
|
351
|
+
}
|
|
352
|
+
function normalizeHostnameForComparison(hostname) {
|
|
353
|
+
return hostname.trim().replace(/\.$/, "").toLowerCase();
|
|
354
|
+
}
|
|
266
355
|
async function createBranchApp(client, options) {
|
|
267
356
|
const branch = await resolveOrCreateBranch(client, {
|
|
268
357
|
projectId: options.projectId,
|
|
@@ -301,6 +390,15 @@ function apiCallError(summary, response, error) {
|
|
|
301
390
|
const hint = error.error?.hint ? ` ${error.error.hint}` : "";
|
|
302
391
|
return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
|
|
303
392
|
}
|
|
393
|
+
function domainApiCallError(summary, response, error) {
|
|
394
|
+
return new PreviewDomainApiError({
|
|
395
|
+
summary,
|
|
396
|
+
status: response.status,
|
|
397
|
+
code: error.error?.code ?? null,
|
|
398
|
+
message: error.error?.message ?? `Management API returned HTTP ${response.status}.`,
|
|
399
|
+
hint: error.error?.hint ?? null
|
|
400
|
+
});
|
|
401
|
+
}
|
|
304
402
|
async function findAppForDeployment(sdk, deploymentId) {
|
|
305
403
|
const projectsResult = await sdk.listProjects();
|
|
306
404
|
if (projectsResult.isErr()) throw new Error(projectsResult.error.message);
|
|
@@ -330,4 +428,4 @@ function toAbsoluteUrl(url) {
|
|
|
330
428
|
return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
|
|
331
429
|
}
|
|
332
430
|
//#endregion
|
|
333
|
-
export { createPreviewAppProvider };
|
|
431
|
+
export { PreviewDomainApiError, createPreviewAppProvider };
|
|
@@ -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
|
}))
|
package/dist/presenters/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderDeployOutputRows } from "../lib/app/deploy-output.js";
|
|
2
|
+
import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
|
|
2
3
|
import { renderList, renderShow, serializeList } from "../output/patterns.js";
|
|
3
4
|
//#region src/presenters/app.ts
|
|
4
5
|
function renderAppBuild(context, descriptor, result) {
|
|
@@ -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,
|
|
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 };
|