@prisma/cli 3.0.0-beta.1 → 3.0.0-beta.11
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 +5 -3
- package/dist/adapters/git.js +8 -3
- package/dist/adapters/local-state.js +12 -4
- package/dist/adapters/mock-api.js +81 -2
- package/dist/adapters/token-storage.js +64 -23
- package/dist/cli.js +18 -3
- package/dist/cli2.js +9 -5
- package/dist/commands/app/index.js +84 -59
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/database/index.js +159 -0
- package/dist/commands/env.js +8 -4
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +286 -131
- package/dist/controllers/app.js +699 -244
- package/dist/controllers/auth.js +8 -8
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/database.js +318 -0
- package/dist/controllers/project.js +156 -85
- package/dist/lib/app/branch-database-deploy.js +325 -0
- package/dist/lib/app/branch-database.js +215 -0
- package/dist/lib/app/bun-project.js +12 -5
- package/dist/lib/app/compute-config.js +147 -0
- package/dist/lib/app/deploy-output.js +10 -1
- package/dist/lib/app/deploy-plan.js +58 -0
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/local-dev.js +8 -49
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-build-settings.js +376 -0
- package/dist/lib/app/preview-build.js +314 -97
- package/dist/lib/app/preview-provider.js +178 -65
- package/dist/lib/app/production-deploy-gate.js +161 -0
- package/dist/lib/auth/auth-ops.js +30 -21
- package/dist/lib/auth/guard.js +3 -2
- package/dist/lib/auth/login.js +116 -14
- package/dist/lib/database/provider.js +167 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +53 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/interactive-setup.js +1 -1
- package/dist/lib/project/local-pin.js +182 -33
- package/dist/lib/project/resolution.js +204 -50
- package/dist/lib/project/setup.js +66 -19
- package/dist/output/patterns.js +1 -1
- package/dist/presenters/app-env.js +149 -14
- package/dist/presenters/app.js +178 -23
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/database.js +274 -0
- package/dist/presenters/project.js +57 -39
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/command-meta.js +103 -13
- package/dist/shell/command-runner.js +57 -19
- package/dist/shell/diagnostics-output.js +57 -0
- package/dist/shell/errors.js +12 -1
- package/dist/shell/help.js +30 -20
- package/dist/shell/output.js +10 -1
- package/dist/shell/runtime.js +11 -7
- package/dist/shell/ui.js +42 -3
- package/dist/shell/update-check.js +247 -0
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/dist/use-cases/project.js +2 -1
- package/package.json +26 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { envVarNames } from "./env-vars.js";
|
|
2
2
|
import { PreviewBuildStrategy } from "./preview-build.js";
|
|
3
|
+
import { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable } from "./preview-branch-database.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
|
|
5
6
|
//#region src/lib/app/preview-provider.ts
|
|
@@ -19,7 +20,10 @@ function createPreviewAppProvider(client, options) {
|
|
|
19
20
|
const sdk = new ComputeClient(client);
|
|
20
21
|
return {
|
|
21
22
|
async createProject(options) {
|
|
22
|
-
const projectResult = await sdk.createProject({
|
|
23
|
+
const projectResult = await sdk.createProject({
|
|
24
|
+
name: options.name,
|
|
25
|
+
signal: options.signal
|
|
26
|
+
});
|
|
23
27
|
if (projectResult.isErr()) throw new Error(projectResult.error.message);
|
|
24
28
|
return {
|
|
25
29
|
id: projectResult.value.id,
|
|
@@ -29,17 +33,52 @@ function createPreviewAppProvider(client, options) {
|
|
|
29
33
|
async listApps(projectId, options) {
|
|
30
34
|
return listComputeServices(client, {
|
|
31
35
|
projectId,
|
|
32
|
-
branchGitName: options?.branchName
|
|
36
|
+
branchGitName: options?.branchName,
|
|
37
|
+
signal: options?.signal
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
async resolveBranch(projectId, options) {
|
|
41
|
+
const branch = await resolveOrCreateBranch(client, {
|
|
42
|
+
projectId,
|
|
43
|
+
gitName: options.branchName,
|
|
44
|
+
signal: options.signal
|
|
33
45
|
});
|
|
46
|
+
return {
|
|
47
|
+
id: branch.id,
|
|
48
|
+
name: branch.gitName,
|
|
49
|
+
role: branch.role
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async createBranchDatabase(options) {
|
|
53
|
+
return createBranchDatabase(client, options);
|
|
54
|
+
},
|
|
55
|
+
async deleteBranchDatabase(options) {
|
|
56
|
+
return deleteBranchDatabase(client, options);
|
|
57
|
+
},
|
|
58
|
+
async listEnvironmentVariables(options) {
|
|
59
|
+
return listEnvironmentVariables(client, options);
|
|
60
|
+
},
|
|
61
|
+
async createEnvironmentVariable(options) {
|
|
62
|
+
return createEnvironmentVariable(client, options);
|
|
63
|
+
},
|
|
64
|
+
async updateEnvironmentVariable(options) {
|
|
65
|
+
return updateEnvironmentVariable(client, options);
|
|
66
|
+
},
|
|
67
|
+
async deleteEnvironmentVariable(options) {
|
|
68
|
+
return deleteEnvironmentVariable(client, options);
|
|
34
69
|
},
|
|
35
|
-
async removeApp(appId) {
|
|
36
|
-
const appResult = await sdk.showService({
|
|
70
|
+
async removeApp(appId, options) {
|
|
71
|
+
const appResult = await sdk.showService({
|
|
72
|
+
serviceId: appId,
|
|
73
|
+
signal: options?.signal
|
|
74
|
+
});
|
|
37
75
|
if (appResult.isErr()) throw new Error(appResult.error.message);
|
|
38
76
|
const destroyResult = await sdk.destroyService({
|
|
39
77
|
serviceId: appId,
|
|
40
78
|
keepService: false,
|
|
41
79
|
timeoutSeconds: 120,
|
|
42
|
-
pollIntervalMs: 2e3
|
|
80
|
+
pollIntervalMs: 2e3,
|
|
81
|
+
signal: options?.signal
|
|
43
82
|
});
|
|
44
83
|
if (destroyResult.isErr()) throw new Error(destroyResult.error.message);
|
|
45
84
|
return {
|
|
@@ -47,17 +86,18 @@ function createPreviewAppProvider(client, options) {
|
|
|
47
86
|
name: appResult.value.name
|
|
48
87
|
};
|
|
49
88
|
},
|
|
50
|
-
async listDomains(appId) {
|
|
51
|
-
return listComputeServiceDomains(client, appId);
|
|
89
|
+
async listDomains(appId, options) {
|
|
90
|
+
return listComputeServiceDomains(client, appId, options?.signal);
|
|
52
91
|
},
|
|
53
92
|
async addDomain(options) {
|
|
54
93
|
const result = await client.POST("/v1/compute-services/{computeServiceId}/domains", {
|
|
55
94
|
params: { path: { computeServiceId: options.appId } },
|
|
56
|
-
body: { hostname: options.hostname }
|
|
95
|
+
body: { hostname: options.hostname },
|
|
96
|
+
signal: options.signal
|
|
57
97
|
});
|
|
58
98
|
if (result.error || !result.data) {
|
|
59
99
|
if (result.response.status === 409) {
|
|
60
|
-
const existing = (await listComputeServiceDomains(client, options.appId)).find((domain) => sameHostname(domain.hostname, options.hostname));
|
|
100
|
+
const existing = (await listComputeServiceDomains(client, options.appId, options.signal)).find((domain) => sameHostname(domain.hostname, options.hostname));
|
|
61
101
|
if (existing) return {
|
|
62
102
|
domain: existing,
|
|
63
103
|
existing: true
|
|
@@ -70,17 +110,26 @@ function createPreviewAppProvider(client, options) {
|
|
|
70
110
|
existing: false
|
|
71
111
|
};
|
|
72
112
|
},
|
|
73
|
-
async showDomain(domainId) {
|
|
74
|
-
const result = await client.GET("/v1/domains/{domainId}", {
|
|
113
|
+
async showDomain(domainId, options) {
|
|
114
|
+
const result = await client.GET("/v1/domains/{domainId}", {
|
|
115
|
+
params: { path: { domainId } },
|
|
116
|
+
signal: options?.signal
|
|
117
|
+
});
|
|
75
118
|
if (result.error || !result.data) throw domainApiCallError("Failed to show custom domain", result.response, result.error);
|
|
76
119
|
return normalizeDomainRecord(result.data.data);
|
|
77
120
|
},
|
|
78
|
-
async removeDomain(domainId) {
|
|
79
|
-
const result = await client.DELETE("/v1/domains/{domainId}", {
|
|
121
|
+
async removeDomain(domainId, options) {
|
|
122
|
+
const result = await client.DELETE("/v1/domains/{domainId}", {
|
|
123
|
+
params: { path: { domainId } },
|
|
124
|
+
signal: options?.signal
|
|
125
|
+
});
|
|
80
126
|
if (result.error) throw domainApiCallError("Failed to remove custom domain", result.response, result.error);
|
|
81
127
|
},
|
|
82
|
-
async retryDomain(domainId) {
|
|
83
|
-
const result = await client.POST("/v1/domains/{domainId}/retry", {
|
|
128
|
+
async retryDomain(domainId, options) {
|
|
129
|
+
const result = await client.POST("/v1/domains/{domainId}/retry", {
|
|
130
|
+
params: { path: { domainId } },
|
|
131
|
+
signal: options?.signal
|
|
132
|
+
});
|
|
84
133
|
if (result.error || !result.data) throw domainApiCallError("Failed to retry custom domain", result.response, result.error);
|
|
85
134
|
return normalizeDomainRecord(result.data.data);
|
|
86
135
|
},
|
|
@@ -90,6 +139,7 @@ function createPreviewAppProvider(client, options) {
|
|
|
90
139
|
versionId: options.deploymentId,
|
|
91
140
|
timeoutSeconds: 120,
|
|
92
141
|
pollIntervalMs: 2e3,
|
|
142
|
+
signal: options.signal,
|
|
93
143
|
progress: options.progress
|
|
94
144
|
});
|
|
95
145
|
if (promoteResult.isErr()) throw new Error(promoteResult.error.message);
|
|
@@ -103,7 +153,8 @@ function createPreviewAppProvider(client, options) {
|
|
|
103
153
|
projectId: options.projectId,
|
|
104
154
|
branchName: options.branchName,
|
|
105
155
|
appName: options.appName,
|
|
106
|
-
region: options.region
|
|
156
|
+
region: options.region,
|
|
157
|
+
signal: options.signal
|
|
107
158
|
}) : {
|
|
108
159
|
appId: void 0,
|
|
109
160
|
appName: options.appName,
|
|
@@ -113,7 +164,9 @@ function createPreviewAppProvider(client, options) {
|
|
|
113
164
|
strategy: new PreviewBuildStrategy({
|
|
114
165
|
appPath: path.resolve(options.cwd),
|
|
115
166
|
entrypoint: options.entrypoint,
|
|
116
|
-
buildType: options.buildType
|
|
167
|
+
buildType: options.buildType,
|
|
168
|
+
signal: options.signal,
|
|
169
|
+
buildSettings: options.buildSettings
|
|
117
170
|
}),
|
|
118
171
|
projectId: options.projectId,
|
|
119
172
|
serviceId: resolvedApp.appId,
|
|
@@ -124,6 +177,7 @@ function createPreviewAppProvider(client, options) {
|
|
|
124
177
|
timeoutSeconds: 120,
|
|
125
178
|
pollIntervalMs: 2e3,
|
|
126
179
|
interaction: options.interaction,
|
|
180
|
+
signal: options.signal,
|
|
127
181
|
progress: options.progress
|
|
128
182
|
});
|
|
129
183
|
if (deployResult.isErr()) throw new Error(deployResult.error.message);
|
|
@@ -150,6 +204,7 @@ function createPreviewAppProvider(client, options) {
|
|
|
150
204
|
envVars: options.envVars,
|
|
151
205
|
timeoutSeconds: 120,
|
|
152
206
|
pollIntervalMs: 2e3,
|
|
207
|
+
signal: options.signal,
|
|
153
208
|
progress: options.progress
|
|
154
209
|
});
|
|
155
210
|
if (updateResult.isErr()) throw new Error(updateResult.error.message);
|
|
@@ -158,10 +213,17 @@ function createPreviewAppProvider(client, options) {
|
|
|
158
213
|
versionId: updateResult.value.versionId,
|
|
159
214
|
timeoutSeconds: 120,
|
|
160
215
|
pollIntervalMs: 2e3,
|
|
216
|
+
signal: options.signal,
|
|
161
217
|
progress: options.promoteProgress
|
|
162
218
|
});
|
|
163
219
|
if (promoteResult.isErr()) throw new Error(promoteResult.error.message);
|
|
164
|
-
const [serviceResult, versionResult] = await Promise.all([sdk.showService({
|
|
220
|
+
const [serviceResult, versionResult] = await Promise.all([sdk.showService({
|
|
221
|
+
serviceId: options.appId,
|
|
222
|
+
signal: options.signal
|
|
223
|
+
}), sdk.showVersion({
|
|
224
|
+
versionId: updateResult.value.versionId,
|
|
225
|
+
signal: options.signal
|
|
226
|
+
})]);
|
|
165
227
|
if (serviceResult.isErr()) throw new Error(serviceResult.error.message);
|
|
166
228
|
if (versionResult.isErr()) throw new Error(versionResult.error.message);
|
|
167
229
|
return {
|
|
@@ -184,7 +246,13 @@ function createPreviewAppProvider(client, options) {
|
|
|
184
246
|
};
|
|
185
247
|
},
|
|
186
248
|
async listAppEnvNames(options) {
|
|
187
|
-
const [serviceResult, versionResult] = await Promise.all([sdk.showService({
|
|
249
|
+
const [serviceResult, versionResult] = await Promise.all([sdk.showService({
|
|
250
|
+
serviceId: options.appId,
|
|
251
|
+
signal: options.signal
|
|
252
|
+
}), sdk.showVersion({
|
|
253
|
+
versionId: options.deploymentId,
|
|
254
|
+
signal: options.signal
|
|
255
|
+
})]);
|
|
188
256
|
if (serviceResult.isErr()) throw new Error(serviceResult.error.message);
|
|
189
257
|
if (versionResult.isErr()) throw new Error(versionResult.error.message);
|
|
190
258
|
return {
|
|
@@ -206,8 +274,14 @@ function createPreviewAppProvider(client, options) {
|
|
|
206
274
|
variables: envVarNames(versionResult.value.envVars)
|
|
207
275
|
};
|
|
208
276
|
},
|
|
209
|
-
async listDeployments(appId) {
|
|
210
|
-
const [appResult, versionsResult] = await Promise.all([sdk.showService({
|
|
277
|
+
async listDeployments(appId, options) {
|
|
278
|
+
const [appResult, versionsResult] = await Promise.all([sdk.showService({
|
|
279
|
+
serviceId: appId,
|
|
280
|
+
signal: options?.signal
|
|
281
|
+
}), sdk.listVersions({
|
|
282
|
+
serviceId: appId,
|
|
283
|
+
signal: options?.signal
|
|
284
|
+
})]);
|
|
211
285
|
if (appResult.isErr()) throw new Error(appResult.error.message);
|
|
212
286
|
if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
|
|
213
287
|
return {
|
|
@@ -230,14 +304,17 @@ function createPreviewAppProvider(client, options) {
|
|
|
230
304
|
}))
|
|
231
305
|
};
|
|
232
306
|
},
|
|
233
|
-
async showDeployment(deploymentId) {
|
|
234
|
-
const deploymentResult = await sdk.showVersion({
|
|
307
|
+
async showDeployment(deploymentId, options) {
|
|
308
|
+
const deploymentResult = await sdk.showVersion({
|
|
309
|
+
versionId: deploymentId,
|
|
310
|
+
signal: options?.signal
|
|
311
|
+
});
|
|
235
312
|
if (deploymentResult.isErr()) {
|
|
236
313
|
if (ApiError.is(deploymentResult.error) && deploymentResult.error.statusCode === 404) return null;
|
|
237
314
|
throw new Error(deploymentResult.error.message);
|
|
238
315
|
}
|
|
239
316
|
return {
|
|
240
|
-
app: await findAppForDeployment(sdk, deploymentId),
|
|
317
|
+
app: await findAppForDeployment(sdk, deploymentId, options?.signal),
|
|
241
318
|
deployment: {
|
|
242
319
|
id: deploymentResult.value.id,
|
|
243
320
|
status: deploymentResult.value.status,
|
|
@@ -263,22 +340,28 @@ function createPreviewAppProvider(client, options) {
|
|
|
263
340
|
};
|
|
264
341
|
}
|
|
265
342
|
async function listBranches(client, options) {
|
|
266
|
-
const result = await client.GET("/v1/projects/{projectId}/branches", {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
343
|
+
const result = await client.GET("/v1/projects/{projectId}/branches", {
|
|
344
|
+
params: {
|
|
345
|
+
path: { projectId: options.projectId },
|
|
346
|
+
query: { gitName: options.gitName }
|
|
347
|
+
},
|
|
348
|
+
signal: options.signal
|
|
349
|
+
});
|
|
270
350
|
if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
|
|
271
|
-
return result.data.data
|
|
351
|
+
return result.data.data.map((branch) => ({
|
|
352
|
+
id: branch.id,
|
|
353
|
+
gitName: branch.gitName,
|
|
354
|
+
isDefault: branch.isDefault,
|
|
355
|
+
role: branch.role
|
|
356
|
+
}));
|
|
272
357
|
}
|
|
273
358
|
async function resolveOrCreateBranch(client, options) {
|
|
274
359
|
const existing = (await listBranches(client, options))[0];
|
|
275
360
|
if (existing) return existing;
|
|
276
361
|
const result = await client.POST("/v1/projects/{projectId}/branches", {
|
|
277
362
|
params: { path: { projectId: options.projectId } },
|
|
278
|
-
body: {
|
|
279
|
-
|
|
280
|
-
isDefault: options.gitName === "main"
|
|
281
|
-
}
|
|
363
|
+
body: { gitName: options.gitName },
|
|
364
|
+
signal: options.signal
|
|
282
365
|
});
|
|
283
366
|
if (result.error || !result.data) {
|
|
284
367
|
if (result.response.status === 409) {
|
|
@@ -287,17 +370,26 @@ async function resolveOrCreateBranch(client, options) {
|
|
|
287
370
|
}
|
|
288
371
|
throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
|
|
289
372
|
}
|
|
290
|
-
|
|
373
|
+
const branch = result.data.data;
|
|
374
|
+
return {
|
|
375
|
+
id: branch.id,
|
|
376
|
+
gitName: branch.gitName,
|
|
377
|
+
isDefault: branch.isDefault,
|
|
378
|
+
role: branch.role
|
|
379
|
+
};
|
|
291
380
|
}
|
|
292
381
|
async function listComputeServices(client, options) {
|
|
293
382
|
const services = [];
|
|
294
383
|
let cursor;
|
|
295
384
|
while (true) {
|
|
296
|
-
const result = await client.GET("/v1/compute-services", {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
385
|
+
const result = await client.GET("/v1/compute-services", {
|
|
386
|
+
params: { query: {
|
|
387
|
+
projectId: options.projectId,
|
|
388
|
+
branchGitName: options.branchGitName,
|
|
389
|
+
cursor
|
|
390
|
+
} },
|
|
391
|
+
signal: options.signal
|
|
392
|
+
});
|
|
301
393
|
if (result.error || !result.data) throw apiCallError("Failed to list apps", result.response, result.error);
|
|
302
394
|
services.push(...result.data.data);
|
|
303
395
|
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
@@ -312,8 +404,11 @@ async function listComputeServices(client, options) {
|
|
|
312
404
|
liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
|
|
313
405
|
}));
|
|
314
406
|
}
|
|
315
|
-
async function listComputeServiceDomains(client, computeServiceId) {
|
|
316
|
-
const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", {
|
|
407
|
+
async function listComputeServiceDomains(client, computeServiceId, signal) {
|
|
408
|
+
const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", {
|
|
409
|
+
params: { path: { computeServiceId } },
|
|
410
|
+
signal
|
|
411
|
+
});
|
|
317
412
|
if (result.error || !result.data) throw domainApiCallError("Failed to list custom domains", result.response, result.error);
|
|
318
413
|
return result.data.data.map((domain) => normalizeDomainRecord(domain));
|
|
319
414
|
}
|
|
@@ -355,19 +450,24 @@ function normalizeHostnameForComparison(hostname) {
|
|
|
355
450
|
async function createBranchApp(client, options) {
|
|
356
451
|
const branch = await resolveOrCreateBranch(client, {
|
|
357
452
|
projectId: options.projectId,
|
|
358
|
-
gitName: options.branchName
|
|
453
|
+
gitName: options.branchName,
|
|
454
|
+
signal: options.signal
|
|
455
|
+
});
|
|
456
|
+
const result = await client.POST("/v1/compute-services", {
|
|
457
|
+
body: {
|
|
458
|
+
projectId: options.projectId,
|
|
459
|
+
branchId: branch.id,
|
|
460
|
+
displayName: options.appName,
|
|
461
|
+
...options.region ? { regionId: options.region } : {}
|
|
462
|
+
},
|
|
463
|
+
signal: options.signal
|
|
359
464
|
});
|
|
360
|
-
const result = await client.POST("/v1/compute-services", { body: {
|
|
361
|
-
projectId: options.projectId,
|
|
362
|
-
branchId: branch.id,
|
|
363
|
-
displayName: options.appName,
|
|
364
|
-
...options.region ? { regionId: options.region } : {}
|
|
365
|
-
} });
|
|
366
465
|
if (result.error || !result.data) {
|
|
367
466
|
if (result.response.status === 409) {
|
|
368
467
|
const matched = (await listComputeServices(client, {
|
|
369
468
|
projectId: options.projectId,
|
|
370
|
-
branchGitName: options.branchName
|
|
469
|
+
branchGitName: options.branchName,
|
|
470
|
+
signal: options.signal
|
|
371
471
|
})).find((app) => app.name === options.appName);
|
|
372
472
|
if (matched) return {
|
|
373
473
|
appId: matched.id,
|
|
@@ -399,30 +499,43 @@ function domainApiCallError(summary, response, error) {
|
|
|
399
499
|
hint: error.error?.hint ?? null
|
|
400
500
|
});
|
|
401
501
|
}
|
|
402
|
-
async function findAppForDeployment(sdk, deploymentId) {
|
|
403
|
-
const projectsResult = await sdk.listProjects();
|
|
502
|
+
async function findAppForDeployment(sdk, deploymentId, signal) {
|
|
503
|
+
const projectsResult = await sdk.listProjects({ signal });
|
|
404
504
|
if (projectsResult.isErr()) throw new Error(projectsResult.error.message);
|
|
405
505
|
for (const project of projectsResult.value) {
|
|
406
|
-
const servicesResult = await sdk.listServices({
|
|
506
|
+
const servicesResult = await sdk.listServices({
|
|
507
|
+
projectId: project.id,
|
|
508
|
+
signal
|
|
509
|
+
});
|
|
407
510
|
if (servicesResult.isErr()) throw new Error(servicesResult.error.message);
|
|
408
511
|
for (const service of servicesResult.value) {
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
411
|
-
const app = {
|
|
412
|
-
id: detailResult.value.id,
|
|
413
|
-
name: detailResult.value.name,
|
|
414
|
-
region: detailResult.value.region ?? null,
|
|
415
|
-
liveDeploymentId: detailResult.value.latestVersionId ?? null,
|
|
416
|
-
liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
|
|
417
|
-
};
|
|
418
|
-
if (app.liveDeploymentId === deploymentId) return app;
|
|
419
|
-
const versionsResult = await sdk.listVersions({ serviceId: service.id });
|
|
420
|
-
if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
|
|
421
|
-
if (versionsResult.value.some((version) => version.id === deploymentId)) return app;
|
|
512
|
+
const app = await findServiceAppForDeployment(sdk, service.id, deploymentId, signal);
|
|
513
|
+
if (app) return app;
|
|
422
514
|
}
|
|
423
515
|
}
|
|
424
516
|
return null;
|
|
425
517
|
}
|
|
518
|
+
async function findServiceAppForDeployment(sdk, serviceId, deploymentId, signal) {
|
|
519
|
+
const detailResult = await sdk.showService({
|
|
520
|
+
serviceId,
|
|
521
|
+
signal
|
|
522
|
+
});
|
|
523
|
+
if (detailResult.isErr()) throw new Error(detailResult.error.message);
|
|
524
|
+
const app = {
|
|
525
|
+
id: detailResult.value.id,
|
|
526
|
+
name: detailResult.value.name,
|
|
527
|
+
region: detailResult.value.region ?? null,
|
|
528
|
+
liveDeploymentId: detailResult.value.latestVersionId ?? null,
|
|
529
|
+
liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
|
|
530
|
+
};
|
|
531
|
+
if (app.liveDeploymentId === deploymentId) return app;
|
|
532
|
+
const versionsResult = await sdk.listVersions({
|
|
533
|
+
serviceId,
|
|
534
|
+
signal
|
|
535
|
+
});
|
|
536
|
+
if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
|
|
537
|
+
return versionsResult.value.some((version) => version.id === deploymentId) ? app : null;
|
|
538
|
+
}
|
|
426
539
|
function toAbsoluteUrl(url) {
|
|
427
540
|
if (!url) return null;
|
|
428
541
|
return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
import { confirmPrompt } from "../../shell/prompt.js";
|
|
3
|
+
import { canPrompt } from "../../shell/runtime.js";
|
|
4
|
+
//#region src/lib/app/production-deploy-gate.ts
|
|
5
|
+
async function enforceProductionDeployGate(context, provider, options) {
|
|
6
|
+
if (options.branchKind !== "production") return { firstProductionDeploy: false };
|
|
7
|
+
if (!options.appId) {
|
|
8
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
9
|
+
return { firstProductionDeploy: true };
|
|
10
|
+
}
|
|
11
|
+
const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
|
|
12
|
+
throw productionDeployInspectionFailedError(error);
|
|
13
|
+
}));
|
|
14
|
+
if (!currentLiveDeployment) {
|
|
15
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
16
|
+
return { firstProductionDeploy: true };
|
|
17
|
+
}
|
|
18
|
+
if (!options.prod) throw productionDeployRequiresFlagError();
|
|
19
|
+
if (context.flags.yes) {
|
|
20
|
+
renderProductionDeployYesLine(context);
|
|
21
|
+
return { firstProductionDeploy: false };
|
|
22
|
+
}
|
|
23
|
+
if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
|
|
24
|
+
renderProductionDeployConfirmation(context, currentLiveDeployment);
|
|
25
|
+
if (!await confirmPrompt({
|
|
26
|
+
input: context.runtime.stdin,
|
|
27
|
+
output: context.output.stderr,
|
|
28
|
+
message: "Deploy to production?",
|
|
29
|
+
initialValue: false
|
|
30
|
+
})) throw productionDeployCancelledError();
|
|
31
|
+
return { firstProductionDeploy: false };
|
|
32
|
+
}
|
|
33
|
+
function resolveCurrentProductionDeployment(result) {
|
|
34
|
+
if (result.deployments.length === 0) return null;
|
|
35
|
+
if (result.app.liveDeploymentId) {
|
|
36
|
+
const live = result.deployments.find((deployment) => deployment.id === result.app.liveDeploymentId);
|
|
37
|
+
if (live) return live;
|
|
38
|
+
}
|
|
39
|
+
return result.deployments.find((deployment) => deployment.live === true) ?? result.deployments[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
function renderFirstProductionDeployLine(context, appName) {
|
|
42
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
43
|
+
context.output.stderr.write(`First deploy of "${appName}" -- promoting to production.\n\n`);
|
|
44
|
+
}
|
|
45
|
+
function renderProductionDeployYesLine(context) {
|
|
46
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
47
|
+
context.output.stderr.write("Deploying to production (--prod --yes).\n\n");
|
|
48
|
+
}
|
|
49
|
+
function renderProductionDeployConfirmation(context, currentLiveDeployment) {
|
|
50
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
51
|
+
const lines = [
|
|
52
|
+
"This will deploy to production and replace the live deployment.",
|
|
53
|
+
"",
|
|
54
|
+
` Current live: ${currentLiveDeployment.id} deployed ${formatDeploymentAge(currentLiveDeployment.createdAt)}`,
|
|
55
|
+
" New deploy: will be built from your local code",
|
|
56
|
+
""
|
|
57
|
+
];
|
|
58
|
+
context.output.stderr.write(`${lines.join("\n")}\n`);
|
|
59
|
+
}
|
|
60
|
+
function formatDeploymentAge(createdAt) {
|
|
61
|
+
const createdAtMs = Date.parse(createdAt);
|
|
62
|
+
if (!Number.isFinite(createdAtMs)) return createdAt;
|
|
63
|
+
const elapsedMs = Math.max(0, Date.now() - createdAtMs);
|
|
64
|
+
for (const unit of [
|
|
65
|
+
{
|
|
66
|
+
label: "day",
|
|
67
|
+
ms: 1440 * 60 * 1e3
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "hour",
|
|
71
|
+
ms: 3600 * 1e3
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "minute",
|
|
75
|
+
ms: 60 * 1e3
|
|
76
|
+
}
|
|
77
|
+
]) if (elapsedMs >= unit.ms) {
|
|
78
|
+
const value = Math.floor(elapsedMs / unit.ms);
|
|
79
|
+
return `${value} ${unit.label}${value === 1 ? "" : "s"} ago`;
|
|
80
|
+
}
|
|
81
|
+
return "less than a minute ago";
|
|
82
|
+
}
|
|
83
|
+
function productionDeployRequiresFlagError() {
|
|
84
|
+
return new CliError({
|
|
85
|
+
code: "PROD_DEPLOY_REQUIRES_FLAG",
|
|
86
|
+
domain: "app",
|
|
87
|
+
summary: "Production deploy requires --prod",
|
|
88
|
+
why: "The resolved Branch is production and this App already has a production deployment.",
|
|
89
|
+
fix: "Re-run with --prod, or deploy from a preview Branch.",
|
|
90
|
+
exitCode: 2,
|
|
91
|
+
nextActions: [{
|
|
92
|
+
kind: "run-command",
|
|
93
|
+
journey: "deploy-app",
|
|
94
|
+
label: "Deploy to production",
|
|
95
|
+
command: "prisma-cli app deploy --prod"
|
|
96
|
+
}, {
|
|
97
|
+
kind: "run-command",
|
|
98
|
+
journey: "deploy-app",
|
|
99
|
+
label: "Create a preview branch",
|
|
100
|
+
commands: ["git checkout -b <branch-name>", "prisma-cli app deploy"]
|
|
101
|
+
}],
|
|
102
|
+
humanLines: [
|
|
103
|
+
"This would deploy to production.",
|
|
104
|
+
"",
|
|
105
|
+
"Production deploys require explicit intent. Re-run with:",
|
|
106
|
+
"",
|
|
107
|
+
" prisma-cli app deploy --prod",
|
|
108
|
+
"",
|
|
109
|
+
"Or deploy a preview from a feature branch:",
|
|
110
|
+
"",
|
|
111
|
+
" git checkout -b <branch-name>",
|
|
112
|
+
" prisma-cli app deploy"
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function productionDeployConfirmationRequiredError(appName) {
|
|
117
|
+
return new CliError({
|
|
118
|
+
code: "CONFIRMATION_REQUIRED",
|
|
119
|
+
domain: "app",
|
|
120
|
+
summary: "Production deploy requires confirmation in the current mode",
|
|
121
|
+
why: "This command cannot prompt for production deploy confirmation in the current mode.",
|
|
122
|
+
fix: `Pass --prod --yes to confirm deployment of "${appName}" to production.`,
|
|
123
|
+
exitCode: 1,
|
|
124
|
+
nextSteps: ["prisma-cli app deploy --prod --yes"],
|
|
125
|
+
nextActions: [{
|
|
126
|
+
kind: "run-command",
|
|
127
|
+
journey: "deploy-app",
|
|
128
|
+
label: "Deploy to production non-interactively",
|
|
129
|
+
command: "prisma-cli app deploy --prod --yes"
|
|
130
|
+
}]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function productionDeployCancelledError() {
|
|
134
|
+
return new CliError({
|
|
135
|
+
code: "CONFIRMATION_REQUIRED",
|
|
136
|
+
domain: "app",
|
|
137
|
+
summary: "Production deploy cancelled",
|
|
138
|
+
why: null,
|
|
139
|
+
fix: null,
|
|
140
|
+
exitCode: 0,
|
|
141
|
+
humanLines: ["Cancelled."]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function productionDeployInspectionFailedError(error) {
|
|
145
|
+
return new CliError({
|
|
146
|
+
code: "DEPLOY_FAILED",
|
|
147
|
+
domain: "app",
|
|
148
|
+
summary: "Failed to inspect production deployments",
|
|
149
|
+
why: error instanceof Error ? error.message : String(error),
|
|
150
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
151
|
+
debug: formatDebugDetails(error),
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
nextSteps: ["prisma-cli app list-deploys"]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function formatDebugDetails(error) {
|
|
157
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
158
|
+
return typeof error === "string" ? error : null;
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { enforceProductionDeployGate };
|