@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
|
@@ -18,9 +18,9 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
18
18
|
requireExplicit: true,
|
|
19
19
|
command: "add"
|
|
20
20
|
});
|
|
21
|
-
if (!scope) throw usageError(`prisma-cli project env add requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --
|
|
21
|
+
if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
|
|
22
22
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
|
|
23
|
-
const resolved = resolveScopeToApi(scope);
|
|
23
|
+
const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: true });
|
|
24
24
|
if (await findVariableByNaturalKey(client, projectId, key, resolved)) throw new CliError({
|
|
25
25
|
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
26
26
|
domain: "app",
|
|
@@ -28,11 +28,26 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
28
28
|
why: "A variable with this key already exists in the targeted scope.",
|
|
29
29
|
fix: "Use `prisma-cli project env update` to change an existing variable's value.",
|
|
30
30
|
exitCode: 1,
|
|
31
|
-
nextSteps: [`prisma-cli project env update ${key}=<new-value>
|
|
31
|
+
nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
32
32
|
});
|
|
33
|
+
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
|
|
34
|
+
scope: {
|
|
35
|
+
kind: "role",
|
|
36
|
+
role: "preview"
|
|
37
|
+
},
|
|
38
|
+
descriptor: {
|
|
39
|
+
kind: "role",
|
|
40
|
+
role: "preview"
|
|
41
|
+
},
|
|
42
|
+
apiTarget: {
|
|
43
|
+
class: "preview",
|
|
44
|
+
branchId: null
|
|
45
|
+
}
|
|
46
|
+
}) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
33
47
|
const { data, error, response } = await client.POST("/v1/environment-variables", { body: {
|
|
34
48
|
projectId,
|
|
35
49
|
class: resolved.apiTarget.class,
|
|
50
|
+
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
36
51
|
key,
|
|
37
52
|
value
|
|
38
53
|
} });
|
|
@@ -44,7 +59,7 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
44
59
|
scope: resolved.descriptor,
|
|
45
60
|
variable: toMetadata(data.data, resolved.descriptor)
|
|
46
61
|
},
|
|
47
|
-
warnings
|
|
62
|
+
warnings,
|
|
48
63
|
nextSteps: []
|
|
49
64
|
};
|
|
50
65
|
}
|
|
@@ -54,9 +69,9 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
54
69
|
requireExplicit: true,
|
|
55
70
|
command: "update"
|
|
56
71
|
});
|
|
57
|
-
if (!scope) throw usageError(`prisma-cli project env update requires --role`, "Writing without an explicit scope is rejected.", "Pass --role production or --
|
|
72
|
+
if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
|
|
58
73
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
|
|
59
|
-
const resolved = resolveScopeToApi(scope);
|
|
74
|
+
const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
|
|
60
75
|
const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
|
|
61
76
|
if (!existing) throw new CliError({
|
|
62
77
|
code: "ENV_VARIABLE_NOT_FOUND",
|
|
@@ -65,7 +80,7 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
65
80
|
why: "No variable with this key exists in the targeted scope.",
|
|
66
81
|
fix: "Use `prisma-cli project env add` to create a new variable.",
|
|
67
82
|
exitCode: 1,
|
|
68
|
-
nextSteps: [`prisma-cli project env add ${key}=<value>
|
|
83
|
+
nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
|
|
69
84
|
});
|
|
70
85
|
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
71
86
|
params: { path: { envVarId: existing.id } },
|
|
@@ -89,7 +104,7 @@ async function runEnvList(context, flags) {
|
|
|
89
104
|
command: "list"
|
|
90
105
|
}) ?? defaultRoleScope();
|
|
91
106
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
|
|
92
|
-
const resolved = resolveScopeToApi(scope);
|
|
107
|
+
const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
|
|
93
108
|
const variables = await listVariables(client, projectId, resolved);
|
|
94
109
|
return {
|
|
95
110
|
command: "project.env.list",
|
|
@@ -99,18 +114,18 @@ async function runEnvList(context, flags) {
|
|
|
99
114
|
variables: variables.map((row) => toMetadata(row, resolved.descriptor))
|
|
100
115
|
},
|
|
101
116
|
warnings: [],
|
|
102
|
-
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value
|
|
117
|
+
nextSteps: variables.length === 0 ? [`prisma-cli project env add KEY=value ${formatScopeFlag(scope)}`] : []
|
|
103
118
|
};
|
|
104
119
|
}
|
|
105
|
-
async function
|
|
106
|
-
if (!key) throw usageError("prisma-cli project env
|
|
120
|
+
async function runEnvRemove(context, key, flags) {
|
|
121
|
+
if (!key) throw usageError("prisma-cli project env remove requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env remove STRIPE_KEY --role production"], "app");
|
|
107
122
|
const scope = resolveEnvScope(flags, {
|
|
108
123
|
requireExplicit: true,
|
|
109
|
-
command: "
|
|
124
|
+
command: "remove"
|
|
110
125
|
});
|
|
111
|
-
if (!scope) throw usageError("prisma-cli project env
|
|
126
|
+
if (!scope) throw usageError("prisma-cli project env remove requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env remove ${key} --role production`], "app");
|
|
112
127
|
const { client, projectId } = await requireClientAndProject(context, flags.projectRef);
|
|
113
|
-
const resolved = resolveScopeToApi(scope);
|
|
128
|
+
const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
|
|
114
129
|
const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
|
|
115
130
|
if (!existing) throw new CliError({
|
|
116
131
|
code: "ENV_VARIABLE_NOT_FOUND",
|
|
@@ -119,12 +134,12 @@ async function runEnvRm(context, key, flags) {
|
|
|
119
134
|
why: "No variable with this key exists in the targeted scope, so there is nothing to remove.",
|
|
120
135
|
fix: "Run prisma-cli project env list with the same scope to see the available variables.",
|
|
121
136
|
exitCode: 1,
|
|
122
|
-
nextSteps: [`prisma-cli project env list
|
|
137
|
+
nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`]
|
|
123
138
|
});
|
|
124
139
|
const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: existing.id } } });
|
|
125
140
|
if (error) throw apiCallError(`Failed to remove ${key}`, response, error);
|
|
126
141
|
return {
|
|
127
|
-
command: "project.env.
|
|
142
|
+
command: "project.env.remove",
|
|
128
143
|
result: {
|
|
129
144
|
projectId,
|
|
130
145
|
scope: resolved.descriptor,
|
|
@@ -151,8 +166,8 @@ async function requireClientAndProject(context, explicitProject) {
|
|
|
151
166
|
})).project.id
|
|
152
167
|
};
|
|
153
168
|
}
|
|
154
|
-
function resolveScopeToApi(scope) {
|
|
155
|
-
return {
|
|
169
|
+
async function resolveScopeToApi(client, projectId, scope, options) {
|
|
170
|
+
if (scope.kind === "role") return {
|
|
156
171
|
scope,
|
|
157
172
|
descriptor: {
|
|
158
173
|
kind: "role",
|
|
@@ -163,6 +178,96 @@ function resolveScopeToApi(scope) {
|
|
|
163
178
|
branchId: null
|
|
164
179
|
}
|
|
165
180
|
};
|
|
181
|
+
const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
|
|
182
|
+
if (branch.isDefault) throw new CliError({
|
|
183
|
+
code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
|
|
184
|
+
domain: "app",
|
|
185
|
+
summary: `Branch "${scope.branchName}" is the default branch`,
|
|
186
|
+
why: "Production variables are project-level only; branch overrides apply to preview branches.",
|
|
187
|
+
fix: "Use --role production for the default branch.",
|
|
188
|
+
exitCode: 1,
|
|
189
|
+
nextSteps: ["prisma-cli project env list --role production"]
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
scope,
|
|
193
|
+
descriptor: {
|
|
194
|
+
kind: "branch",
|
|
195
|
+
branchName: branch.gitName,
|
|
196
|
+
branchId: branch.id
|
|
197
|
+
},
|
|
198
|
+
apiTarget: {
|
|
199
|
+
class: "preview",
|
|
200
|
+
branchId: branch.id
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function formatScopeFlag(scope) {
|
|
205
|
+
if (scope.kind === "role") return `--role ${scope.role}`;
|
|
206
|
+
return `--branch ${scope.branchName}`;
|
|
207
|
+
}
|
|
208
|
+
async function listBranchesByName(client, projectId, branchName) {
|
|
209
|
+
const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
210
|
+
path: { projectId },
|
|
211
|
+
query: { gitName: branchName }
|
|
212
|
+
} });
|
|
213
|
+
if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
|
|
214
|
+
return data.data;
|
|
215
|
+
}
|
|
216
|
+
async function resolveExistingBranch(client, projectId, branchName) {
|
|
217
|
+
const branch = (await listBranchesByName(client, projectId, branchName))[0];
|
|
218
|
+
if (!branch) throw new CliError({
|
|
219
|
+
code: "ENV_BRANCH_NOT_FOUND",
|
|
220
|
+
domain: "app",
|
|
221
|
+
summary: `Branch "${branchName}" not found`,
|
|
222
|
+
why: "Branch update, list, and remove commands only target existing preview branches.",
|
|
223
|
+
fix: "Create the branch by deploying it, or use `project env add --branch` to create its first override.",
|
|
224
|
+
exitCode: 1,
|
|
225
|
+
nextSteps: [`prisma-cli project env add KEY=value --branch ${branchName}`]
|
|
226
|
+
});
|
|
227
|
+
return branch;
|
|
228
|
+
}
|
|
229
|
+
async function resolveOrCreateBranch(client, projectId, branchName) {
|
|
230
|
+
const existing = (await listBranchesByName(client, projectId, branchName))[0];
|
|
231
|
+
if (existing) return existing;
|
|
232
|
+
if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
|
|
233
|
+
code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
|
|
234
|
+
domain: "app",
|
|
235
|
+
summary: `Cannot create branch "${branchName}" from project env`,
|
|
236
|
+
why: "Creating the first branch would make it the project default, but branch overrides are preview-only.",
|
|
237
|
+
fix: "Create or deploy the default branch first, then add the branch override.",
|
|
238
|
+
exitCode: 1,
|
|
239
|
+
nextSteps: ["prisma-cli app deploy --branch main"]
|
|
240
|
+
});
|
|
241
|
+
const { data, error, response } = await client.POST("/v1/projects/{projectId}/branches", {
|
|
242
|
+
params: { path: { projectId } },
|
|
243
|
+
body: {
|
|
244
|
+
gitName: branchName,
|
|
245
|
+
isDefault: false
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
if (error || !data) {
|
|
249
|
+
if (response?.status === 409) {
|
|
250
|
+
const raced = (await listBranchesByName(client, projectId, branchName))[0];
|
|
251
|
+
if (raced) return raced;
|
|
252
|
+
}
|
|
253
|
+
throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
|
|
254
|
+
}
|
|
255
|
+
return data.data;
|
|
256
|
+
}
|
|
257
|
+
async function projectHasDefaultBranch(client, projectId) {
|
|
258
|
+
let cursor;
|
|
259
|
+
while (true) {
|
|
260
|
+
const query = {};
|
|
261
|
+
if (cursor !== void 0) query.cursor = cursor;
|
|
262
|
+
const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
263
|
+
path: { projectId },
|
|
264
|
+
query
|
|
265
|
+
} });
|
|
266
|
+
if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
|
|
267
|
+
if (result.data.data.some((branch) => branch.isDefault)) return true;
|
|
268
|
+
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
|
|
269
|
+
cursor = result.data.pagination.nextCursor;
|
|
270
|
+
}
|
|
166
271
|
}
|
|
167
272
|
async function findVariableByNaturalKey(client, projectId, key, resolved) {
|
|
168
273
|
const { data, error, response } = await client.GET("/v1/environment-variables", { params: { query: {
|
|
@@ -171,7 +276,7 @@ async function findVariableByNaturalKey(client, projectId, key, resolved) {
|
|
|
171
276
|
key
|
|
172
277
|
} } });
|
|
173
278
|
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
174
|
-
return data.data.filter((row) =>
|
|
279
|
+
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
175
280
|
}
|
|
176
281
|
async function listVariables(client, projectId, resolved) {
|
|
177
282
|
const collected = [];
|
|
@@ -189,20 +294,41 @@ async function listVariables(client, projectId, resolved) {
|
|
|
189
294
|
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
|
|
190
295
|
cursor = result.data.pagination.nextCursor;
|
|
191
296
|
}
|
|
192
|
-
return collected;
|
|
297
|
+
return materializeEffectiveRows(collected, resolved);
|
|
193
298
|
}
|
|
194
299
|
function rowMatchesScope(row, resolved) {
|
|
195
|
-
|
|
300
|
+
if (row.class !== resolved.apiTarget.class) return false;
|
|
301
|
+
if (resolved.apiTarget.branchId === null) return row.branchId === null;
|
|
302
|
+
return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
|
|
196
303
|
}
|
|
197
|
-
function
|
|
304
|
+
function rowMatchesExactScope(row, resolved) {
|
|
305
|
+
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
306
|
+
}
|
|
307
|
+
function materializeEffectiveRows(rows, resolved) {
|
|
308
|
+
if (resolved.apiTarget.branchId === null) return rows;
|
|
309
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const row of rows) if (row.branchId === null && !byKey.has(row.key)) byKey.set(row.key, row);
|
|
311
|
+
for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
|
|
312
|
+
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
313
|
+
}
|
|
314
|
+
function toMetadata(row, requestedScope) {
|
|
315
|
+
const rowScope = row.branchId === null ? {
|
|
316
|
+
kind: "role",
|
|
317
|
+
role: row.class
|
|
318
|
+
} : requestedScope;
|
|
198
319
|
return {
|
|
199
320
|
id: row.id,
|
|
200
321
|
key: row.key,
|
|
201
|
-
scope,
|
|
322
|
+
scope: rowScope,
|
|
323
|
+
source: formatDescriptorLabel(rowScope),
|
|
202
324
|
isManagedBySystem: row.isManagedBySystem,
|
|
203
325
|
updatedAt: row.updatedAt
|
|
204
326
|
};
|
|
205
327
|
}
|
|
328
|
+
function formatDescriptorLabel(scope) {
|
|
329
|
+
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
330
|
+
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
331
|
+
}
|
|
206
332
|
function apiCallError(summary, response, error) {
|
|
207
333
|
const status = response?.status ?? 0;
|
|
208
334
|
const apiCode = error?.error?.code;
|
|
@@ -220,4 +346,4 @@ function apiCallError(summary, response, error) {
|
|
|
220
346
|
});
|
|
221
347
|
}
|
|
222
348
|
//#endregion
|
|
223
|
-
export { runEnvAdd, runEnvList,
|
|
349
|
+
export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };
|