@prisma/cli 3.0.0-beta.4 → 3.0.0-beta.6
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 +1 -0
- package/dist/adapters/local-state.js +1 -1
- package/dist/cli2.js +2 -3
- package/dist/commands/app/index.js +21 -11
- 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 +71 -82
- package/dist/controllers/app.js +55 -4
- package/dist/controllers/branch.js +5 -0
- package/dist/lib/app/branch-database-deploy.js +323 -0
- package/dist/lib/app/branch-database.js +316 -0
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/preview-branch-database.js +102 -0
- package/dist/lib/app/preview-provider.js +19 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/resolution.js +49 -4
- package/dist/presenters/app-env.js +137 -13
- package/dist/presenters/app.js +165 -20
- package/dist/presenters/branch.js +10 -3
- package/dist/presenters/project.js +41 -17
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/command-meta.js +6 -0
- package/dist/shell/command-runner.js +17 -1
- package/dist/shell/diagnostics-output.js +63 -0
- package/dist/shell/output.js +10 -1
- package/dist/shell/runtime.js +1 -1
- package/dist/shell/ui.js +20 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Useful next commands:
|
|
|
57
57
|
npx prisma-cli app logs
|
|
58
58
|
npx prisma-cli app open
|
|
59
59
|
npx prisma-cli project env add DATABASE_URL=postgresql://example --role preview
|
|
60
|
+
npx prisma-cli project env add --file .env --role preview
|
|
60
61
|
npx prisma-cli project env list
|
|
61
62
|
npx prisma-cli project env list --role preview
|
|
62
63
|
```
|
package/dist/cli2.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "./shell/errors.js";
|
|
2
2
|
import { createShellUi } from "./shell/ui.js";
|
|
3
|
-
import { writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output.js";
|
|
3
|
+
import { formatUnexpectedError, writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output.js";
|
|
4
4
|
import { attachCommandDescriptor } from "./shell/command-meta.js";
|
|
5
5
|
import { addCompactGlobalFlags } from "./shell/global-flags.js";
|
|
6
6
|
import { configureRuntimeCommand, createCommandContext } from "./shell/runtime.js";
|
|
@@ -33,8 +33,7 @@ async function runCli(options = {}) {
|
|
|
33
33
|
return process.exitCode ?? 0;
|
|
34
34
|
} catch (error) {
|
|
35
35
|
if (error instanceof CommanderError) return error.code === "commander.helpDisplayed" ? 0 : 2;
|
|
36
|
-
|
|
37
|
-
runtime.stderr.write(`${message}\n`);
|
|
36
|
+
runtime.stderr.write(formatUnexpectedError(error, runtime.argv.includes("--trace")));
|
|
38
37
|
return 1;
|
|
39
38
|
} finally {
|
|
40
39
|
runtime.stdin;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usageError } from "../../shell/errors.js";
|
|
1
2
|
import { attachCommandDescriptor } from "../../shell/command-meta.js";
|
|
2
3
|
import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
|
|
3
4
|
import { configureRuntimeCommand } from "../../shell/runtime.js";
|
|
@@ -64,7 +65,7 @@ function createDeployCommand(runtime) {
|
|
|
64
65
|
"hono",
|
|
65
66
|
"tanstack-start",
|
|
66
67
|
"bun"
|
|
67
|
-
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
68
|
+
])).addOption(new Option("--entry <path>", "Entrypoint path for Bun deploys")).addOption(new Option("--http-port <port>", "HTTP port override for the deployed app")).addOption(new Option("--env <name=value>", "Environment variable").argParser(collectRepeatableValues)).addOption(new Option("--db", "Create and wire an isolated database for the preview Branch")).addOption(new Option("--no-db", "Skip branch database setup")).addOption(new Option("--prod", "Confirm intent to deploy to production"));
|
|
68
69
|
addGlobalFlags(command);
|
|
69
70
|
command.action(async (options) => {
|
|
70
71
|
const appName = options.app;
|
|
@@ -76,22 +77,31 @@ function createDeployCommand(runtime) {
|
|
|
76
77
|
const projectRef = options.project;
|
|
77
78
|
const createProjectName = options.createProject;
|
|
78
79
|
const prod = options.prod;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
const db = options.db;
|
|
81
|
+
const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
|
|
82
|
+
await runCommand(runtime, "app.deploy", options, (context) => {
|
|
83
|
+
if (hasDbConflict) throw usageError("app deploy accepts either --db or --no-db", "--db requests branch database setup, while --no-db disables it.", "Pass exactly one database setup flag.", ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"], "app");
|
|
84
|
+
return runAppDeploy(context, appName, {
|
|
85
|
+
projectRef,
|
|
86
|
+
createProjectName,
|
|
87
|
+
branchName,
|
|
88
|
+
entrypoint: entry,
|
|
89
|
+
framework,
|
|
90
|
+
httpPort,
|
|
91
|
+
envAssignments,
|
|
92
|
+
prod: prod === true,
|
|
93
|
+
db
|
|
94
|
+
});
|
|
95
|
+
}, {
|
|
89
96
|
renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
|
|
90
97
|
renderJson: (result) => serializeAppDeploy(result)
|
|
91
98
|
});
|
|
92
99
|
});
|
|
93
100
|
return command;
|
|
94
101
|
}
|
|
102
|
+
function hasFlag(argv, flag) {
|
|
103
|
+
return argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
104
|
+
}
|
|
95
105
|
function createShowCommand(runtime) {
|
|
96
106
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "app.show");
|
|
97
107
|
command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
package/dist/commands/env.js
CHANGED
|
@@ -17,16 +17,18 @@ function createEnvCommand(runtime) {
|
|
|
17
17
|
}
|
|
18
18
|
function createEnvAddCommand(runtime) {
|
|
19
19
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("add"), runtime), "project.env.add");
|
|
20
|
-
command.argument("
|
|
20
|
+
command.argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment").addOption(new Option("--file <path>", "Read KEY=VALUE assignments from a dotenv file")).addOption(new Option("--role <role>", "Project template scope (production or preview)").choices(["production", "preview"])).addOption(new Option("--branch <git-name>", "Preview branch override scope")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
|
21
21
|
addGlobalFlags(command);
|
|
22
22
|
command.action(async (assignment, options) => {
|
|
23
23
|
const roleName = options.role;
|
|
24
24
|
const branchName = options.branch;
|
|
25
25
|
const projectRef = options.project;
|
|
26
|
+
const filePath = options.file;
|
|
26
27
|
await runCommand(runtime, "project.env.add", options, (context) => runEnvAdd(context, assignment, {
|
|
27
28
|
roleName,
|
|
28
29
|
branchName,
|
|
29
|
-
projectRef
|
|
30
|
+
projectRef,
|
|
31
|
+
filePath
|
|
30
32
|
}), {
|
|
31
33
|
renderHuman: (context, descriptor, result) => renderEnvAdd(context, descriptor, result),
|
|
32
34
|
renderJson: (result) => serializeEnvAdd(result)
|
|
@@ -36,16 +38,18 @@ function createEnvAddCommand(runtime) {
|
|
|
36
38
|
}
|
|
37
39
|
function createEnvUpdateCommand(runtime) {
|
|
38
40
|
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("update"), runtime), "project.env.update");
|
|
39
|
-
command.argument("
|
|
41
|
+
command.argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment").addOption(new Option("--file <path>", "Read KEY=VALUE assignments from a dotenv file")).addOption(new Option("--role <role>", "Project template scope (production or preview)").choices(["production", "preview"])).addOption(new Option("--branch <git-name>", "Preview branch override scope")).addOption(new Option("--project <id-or-name>", "Project id or name"));
|
|
40
42
|
addGlobalFlags(command);
|
|
41
43
|
command.action(async (assignment, options) => {
|
|
42
44
|
const roleName = options.role;
|
|
43
45
|
const branchName = options.branch;
|
|
44
46
|
const projectRef = options.project;
|
|
47
|
+
const filePath = options.file;
|
|
45
48
|
await runCommand(runtime, "project.env.update", options, (context) => runEnvUpdate(context, assignment, {
|
|
46
49
|
roleName,
|
|
47
50
|
branchName,
|
|
48
|
-
projectRef
|
|
51
|
+
projectRef,
|
|
52
|
+
filePath
|
|
49
53
|
}), {
|
|
50
54
|
renderHuman: (context, descriptor, result) => renderEnvUpdate(context, descriptor, result),
|
|
51
55
|
renderJson: (result) => serializeEnvUpdate(result)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CliError, authRequiredError } from "../shell/errors.js";
|
|
2
|
+
//#region src/controllers/app-env-api.ts
|
|
3
|
+
async function findVariableByNaturalKey(client, projectId, key, resolved, signal) {
|
|
4
|
+
const { data, error, response } = await client.GET("/v1/environment-variables", {
|
|
5
|
+
params: { query: {
|
|
6
|
+
projectId,
|
|
7
|
+
class: resolved.apiTarget.class,
|
|
8
|
+
key
|
|
9
|
+
} },
|
|
10
|
+
signal
|
|
11
|
+
});
|
|
12
|
+
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
13
|
+
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
14
|
+
}
|
|
15
|
+
function toMetadata(row, requestedScope) {
|
|
16
|
+
const rowScope = row.branchId === null ? {
|
|
17
|
+
kind: "role",
|
|
18
|
+
role: row.class
|
|
19
|
+
} : requestedScope;
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
key: row.key,
|
|
23
|
+
scope: rowScope,
|
|
24
|
+
source: formatDescriptorLabel(rowScope),
|
|
25
|
+
isManagedBySystem: row.isManagedBySystem,
|
|
26
|
+
updatedAt: row.updatedAt
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function rowMatchesExactScope(row, resolved) {
|
|
30
|
+
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
31
|
+
}
|
|
32
|
+
function apiCallError(summary, response, error) {
|
|
33
|
+
const status = response?.status ?? 0;
|
|
34
|
+
const apiCode = error?.error?.code;
|
|
35
|
+
const apiMessage = error?.error?.message;
|
|
36
|
+
const apiHint = error?.error?.hint;
|
|
37
|
+
if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
|
|
38
|
+
return new CliError({
|
|
39
|
+
code: apiCode ?? "ENV_API_ERROR",
|
|
40
|
+
domain: "app",
|
|
41
|
+
summary,
|
|
42
|
+
why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
|
|
43
|
+
fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
nextSteps: []
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function formatDescriptorLabel(scope) {
|
|
49
|
+
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
50
|
+
if (scope.kind === "overview") return "overview";
|
|
51
|
+
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { apiCallError, findVariableByNaturalKey, toMetadata };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { CliError } from "../shell/errors.js";
|
|
2
|
+
import { formatScopeLabel } from "../lib/app/env-config.js";
|
|
3
|
+
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
4
|
+
//#region src/controllers/app-env-file.ts
|
|
5
|
+
async function runEnvAddFile(context, client, projectId, resolved, filePath, assignments, verboseContext) {
|
|
6
|
+
const existing = await findVariablesByNaturalKey(client, projectId, assignments.map((assignment) => assignment.key), resolved, context.runtime.signal);
|
|
7
|
+
const existingKeys = assignments.map((assignment) => assignment.key).filter((key) => existing.has(key));
|
|
8
|
+
if (existingKeys.length > 0) throw new CliError({
|
|
9
|
+
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
10
|
+
domain: "app",
|
|
11
|
+
summary: `${existingKeys.length} environment variable(s) already exist in ${formatScopeLabel(resolved.scope)}`,
|
|
12
|
+
why: `Existing keys: ${formatKeyList(existingKeys)}.`,
|
|
13
|
+
fix: "Split the input file by key state: update existing keys and add new keys separately.",
|
|
14
|
+
exitCode: 1,
|
|
15
|
+
nextSteps: splitFileNextSteps(filePath, resolved.scope, {
|
|
16
|
+
existingKeys,
|
|
17
|
+
first: "update-existing"
|
|
18
|
+
}),
|
|
19
|
+
meta: { keys: existingKeys }
|
|
20
|
+
});
|
|
21
|
+
const warnings = await missingPreviewDefaultWarnings(client, projectId, resolved.scope, assignments.map((assignment) => assignment.key), context.runtime.signal);
|
|
22
|
+
const variables = [];
|
|
23
|
+
for (const assignment of assignments) try {
|
|
24
|
+
const { data, error, response } = await client.POST("/v1/environment-variables", {
|
|
25
|
+
body: {
|
|
26
|
+
projectId,
|
|
27
|
+
class: resolved.apiTarget.class,
|
|
28
|
+
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
29
|
+
key: assignment.key,
|
|
30
|
+
value: assignment.value
|
|
31
|
+
},
|
|
32
|
+
signal: context.runtime.signal
|
|
33
|
+
});
|
|
34
|
+
if (error || !data) throw apiCallError(`Failed to add ${assignment.key}`, response, error);
|
|
35
|
+
variables.push(toMetadata(data.data, resolved.descriptor));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw envFileApplyFailedError("add", filePath, resolved.scope, assignment.key, variables, error);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: "project.env.add",
|
|
41
|
+
result: {
|
|
42
|
+
projectId,
|
|
43
|
+
verboseContext,
|
|
44
|
+
scope: resolved.descriptor,
|
|
45
|
+
variables,
|
|
46
|
+
file: {
|
|
47
|
+
path: filePath,
|
|
48
|
+
count: variables.length
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
warnings,
|
|
52
|
+
nextSteps: []
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function runEnvUpdateFile(context, client, projectId, resolved, filePath, assignments, verboseContext) {
|
|
56
|
+
const existing = await findVariablesByNaturalKey(client, projectId, assignments.map((assignment) => assignment.key), resolved, context.runtime.signal);
|
|
57
|
+
const missingKeys = assignments.map((assignment) => assignment.key).filter((key) => !existing.has(key));
|
|
58
|
+
if (missingKeys.length > 0) throw new CliError({
|
|
59
|
+
code: "ENV_VARIABLE_NOT_FOUND",
|
|
60
|
+
domain: "app",
|
|
61
|
+
summary: `${missingKeys.length} environment variable(s) not found in ${formatScopeLabel(resolved.scope)}`,
|
|
62
|
+
why: `Missing keys: ${formatKeyList(missingKeys)}.`,
|
|
63
|
+
fix: "Split the input file by key state: add missing keys and update existing keys separately.",
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
nextSteps: splitFileNextSteps(filePath, resolved.scope, {
|
|
66
|
+
missingKeys,
|
|
67
|
+
first: "add-missing"
|
|
68
|
+
}),
|
|
69
|
+
meta: { keys: missingKeys }
|
|
70
|
+
});
|
|
71
|
+
const variables = [];
|
|
72
|
+
for (const assignment of assignments) {
|
|
73
|
+
const existingVariable = existing.get(assignment.key);
|
|
74
|
+
if (!existingVariable) continue;
|
|
75
|
+
try {
|
|
76
|
+
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
77
|
+
params: { path: { envVarId: existingVariable.id } },
|
|
78
|
+
body: { value: assignment.value },
|
|
79
|
+
signal: context.runtime.signal
|
|
80
|
+
});
|
|
81
|
+
if (error || !data) throw apiCallError(`Failed to update value for ${assignment.key}`, response, error);
|
|
82
|
+
variables.push(toMetadata(data.data, resolved.descriptor));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw envFileApplyFailedError("update", filePath, resolved.scope, assignment.key, variables, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
command: "project.env.update",
|
|
89
|
+
result: {
|
|
90
|
+
projectId,
|
|
91
|
+
verboseContext,
|
|
92
|
+
scope: resolved.descriptor,
|
|
93
|
+
variables,
|
|
94
|
+
file: {
|
|
95
|
+
path: filePath,
|
|
96
|
+
count: variables.length
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
warnings: [],
|
|
100
|
+
nextSteps: []
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function findVariablesByNaturalKey(client, projectId, keys, resolved, signal) {
|
|
104
|
+
const found = /* @__PURE__ */ new Map();
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
const row = await findVariableByNaturalKey(client, projectId, key, resolved, signal);
|
|
107
|
+
if (row) found.set(key, row);
|
|
108
|
+
}
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
async function missingPreviewDefaultWarnings(client, projectId, scope, keys, signal) {
|
|
112
|
+
if (scope.kind !== "branch") return [];
|
|
113
|
+
const previewScope = {
|
|
114
|
+
scope: {
|
|
115
|
+
kind: "role",
|
|
116
|
+
role: "preview"
|
|
117
|
+
},
|
|
118
|
+
descriptor: {
|
|
119
|
+
kind: "role",
|
|
120
|
+
role: "preview"
|
|
121
|
+
},
|
|
122
|
+
apiTarget: {
|
|
123
|
+
class: "preview",
|
|
124
|
+
branchId: null
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const missing = [];
|
|
128
|
+
for (const key of keys) if (!await findVariableByNaturalKey(client, projectId, key, previewScope, signal)) missing.push(key);
|
|
129
|
+
if (missing.length === 0) return [];
|
|
130
|
+
if (missing.length === 1) return [`Variable "${missing[0]}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`];
|
|
131
|
+
return [`Variables ${formatKeyList(missing)} do not exist in preview. They will only exist on ${formatScopeLabel(scope)}.`];
|
|
132
|
+
}
|
|
133
|
+
function envFileApplyFailedError(command, filePath, scope, failedKey, writtenVariables, error) {
|
|
134
|
+
const writtenKeys = writtenVariables.map((variable) => variable.key);
|
|
135
|
+
const cause = error instanceof CliError ? error.summary : error instanceof Error ? error.message : "Unknown error.";
|
|
136
|
+
return new CliError({
|
|
137
|
+
code: "ENV_FILE_APPLY_FAILED",
|
|
138
|
+
domain: "app",
|
|
139
|
+
summary: `Failed to ${command} "${failedKey}" from "${filePath}"`,
|
|
140
|
+
why: writtenKeys.length === 0 ? `No variables were written before ${failedKey} failed. Cause: ${cause}` : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
|
|
141
|
+
fix: "Inspect the target scope, then retry the remaining keys once the API issue is resolved.",
|
|
142
|
+
exitCode: 1,
|
|
143
|
+
nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`, retryStepForApplyFailure(command, filePath, scope, writtenKeys)],
|
|
144
|
+
meta: {
|
|
145
|
+
file: filePath,
|
|
146
|
+
failedKey,
|
|
147
|
+
writtenKeys
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function retryStepForApplyFailure(command, filePath, scope, writtenKeys) {
|
|
152
|
+
if (command === "update") return `prisma-cli project env update --file ${filePath} ${formatScopeFlag(scope)}`;
|
|
153
|
+
if (writtenKeys.length === 0) return `prisma-cli project env add --file ${filePath} ${formatScopeFlag(scope)}`;
|
|
154
|
+
return `prisma-cli project env add --file <remaining.env> ${formatScopeFlag(scope)}`;
|
|
155
|
+
}
|
|
156
|
+
function splitFileNextSteps(filePath, scope, options) {
|
|
157
|
+
const scopeFlag = formatScopeFlag(scope);
|
|
158
|
+
const existingFile = `${filePath}.existing`;
|
|
159
|
+
const newFile = `${filePath}.new`;
|
|
160
|
+
if (options.first === "update-existing") return [
|
|
161
|
+
`# existing keys: ${formatKeyList(options.existingKeys)}`,
|
|
162
|
+
`prisma-cli project env update --file ${existingFile} ${scopeFlag}`,
|
|
163
|
+
"# new keys only",
|
|
164
|
+
`prisma-cli project env add --file ${newFile} ${scopeFlag}`
|
|
165
|
+
];
|
|
166
|
+
return [
|
|
167
|
+
`# missing keys: ${formatKeyList(options.missingKeys)}`,
|
|
168
|
+
`prisma-cli project env add --file ${newFile} ${scopeFlag}`,
|
|
169
|
+
"# existing keys only",
|
|
170
|
+
`prisma-cli project env update --file ${existingFile} ${scopeFlag}`
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
function formatKeyList(keys) {
|
|
174
|
+
return keys.map((key) => `"${key}"`).join(", ");
|
|
175
|
+
}
|
|
176
|
+
function formatScopeFlag(scope) {
|
|
177
|
+
if (scope.kind === "role") return `--role ${scope.role}`;
|
|
178
|
+
return `--branch ${scope.branchName}`;
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { runEnvAddFile, runEnvUpdateFile };
|
|
@@ -5,33 +5,34 @@ import { readLocalGitBranch } from "../lib/git/local-branch.js";
|
|
|
5
5
|
import { requireAuthenticatedAuthState } from "./auth.js";
|
|
6
6
|
import { listRealWorkspaceProjects } from "./project.js";
|
|
7
7
|
import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
|
|
8
|
+
import { readEnvFileAssignments } from "../lib/app/env-file.js";
|
|
9
|
+
import { apiCallError, findVariableByNaturalKey, toMetadata } from "./app-env-api.js";
|
|
10
|
+
import { runEnvAddFile, runEnvUpdateFile } from "./app-env-file.js";
|
|
8
11
|
//#region src/controllers/app-env.ts
|
|
9
12
|
async function runEnvAdd(context, rawAssignment, flags) {
|
|
10
|
-
const
|
|
13
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "add");
|
|
11
14
|
const scope = resolveEnvScope(flags, {
|
|
12
15
|
requireExplicit: true,
|
|
13
16
|
command: "add"
|
|
14
17
|
});
|
|
15
|
-
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>.", [
|
|
16
|
-
const
|
|
18
|
+
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");
|
|
19
|
+
const input = await resolveEnvWriteInput(context, source, "add");
|
|
20
|
+
const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env add");
|
|
17
21
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
18
22
|
createBranchIfMissing: true,
|
|
19
23
|
signal: context.runtime.signal
|
|
20
24
|
});
|
|
21
|
-
if (
|
|
25
|
+
if (input.kind === "file") return runEnvAddFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
|
|
26
|
+
if (await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal)) throw new CliError({
|
|
22
27
|
code: "ENV_VARIABLE_ALREADY_EXISTS",
|
|
23
28
|
domain: "app",
|
|
24
|
-
summary: `Variable "${key}" already exists in ${formatScopeLabel(scope)}`,
|
|
29
|
+
summary: `Variable "${input.key}" already exists in ${formatScopeLabel(scope)}`,
|
|
25
30
|
why: "A variable with this key already exists in the targeted scope.",
|
|
26
31
|
fix: "Use `prisma-cli project env update` to change an existing variable's value.",
|
|
27
32
|
exitCode: 1,
|
|
28
|
-
nextSteps: [`prisma-cli project env update ${key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
33
|
+
nextSteps: [`prisma-cli project env update ${input.key}=<new-value> ${formatScopeFlag(scope)}`]
|
|
29
34
|
});
|
|
30
|
-
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, key, {
|
|
31
|
-
scope: {
|
|
32
|
-
kind: "role",
|
|
33
|
-
role: "preview"
|
|
34
|
-
},
|
|
35
|
+
const warnings = scope.kind === "branch" && !await findVariableByNaturalKey(client, projectId, input.key, {
|
|
35
36
|
descriptor: {
|
|
36
37
|
kind: "role",
|
|
37
38
|
role: "preview"
|
|
@@ -40,22 +41,23 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
40
41
|
class: "preview",
|
|
41
42
|
branchId: null
|
|
42
43
|
}
|
|
43
|
-
}, context.runtime.signal) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
44
|
+
}, context.runtime.signal) ? [`Variable "${input.key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
|
|
44
45
|
const { data, error, response } = await client.POST("/v1/environment-variables", {
|
|
45
46
|
body: {
|
|
46
47
|
projectId,
|
|
47
48
|
class: resolved.apiTarget.class,
|
|
48
49
|
...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
|
|
49
|
-
key,
|
|
50
|
-
value
|
|
50
|
+
key: input.key,
|
|
51
|
+
value: input.value
|
|
51
52
|
},
|
|
52
53
|
signal: context.runtime.signal
|
|
53
54
|
});
|
|
54
|
-
if (error || !data) throw apiCallError(`Failed to add ${key}`, response, error);
|
|
55
|
+
if (error || !data) throw apiCallError(`Failed to add ${input.key}`, response, error);
|
|
55
56
|
return {
|
|
56
57
|
command: "project.env.add",
|
|
57
58
|
result: {
|
|
58
59
|
projectId,
|
|
60
|
+
verboseContext,
|
|
59
61
|
scope: resolved.descriptor,
|
|
60
62
|
variable: toMetadata(data.data, resolved.descriptor)
|
|
61
63
|
},
|
|
@@ -64,37 +66,40 @@ async function runEnvAdd(context, rawAssignment, flags) {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
async function runEnvUpdate(context, rawAssignment, flags) {
|
|
67
|
-
const
|
|
69
|
+
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "update");
|
|
68
70
|
const scope = resolveEnvScope(flags, {
|
|
69
71
|
requireExplicit: true,
|
|
70
72
|
command: "update"
|
|
71
73
|
});
|
|
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>.", [
|
|
73
|
-
const
|
|
74
|
+
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");
|
|
75
|
+
const input = await resolveEnvWriteInput(context, source, "update");
|
|
76
|
+
const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env update");
|
|
74
77
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
75
78
|
createBranchIfMissing: false,
|
|
76
79
|
signal: context.runtime.signal
|
|
77
80
|
});
|
|
78
|
-
|
|
81
|
+
if (input.kind === "file") return runEnvUpdateFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
|
|
82
|
+
const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
|
|
79
83
|
if (!existing) throw new CliError({
|
|
80
84
|
code: "ENV_VARIABLE_NOT_FOUND",
|
|
81
85
|
domain: "app",
|
|
82
|
-
summary: `Variable "${key}" not found in ${formatScopeLabel(scope)}`,
|
|
86
|
+
summary: `Variable "${input.key}" not found in ${formatScopeLabel(scope)}`,
|
|
83
87
|
why: "No variable with this key exists in the targeted scope.",
|
|
84
88
|
fix: "Use `prisma-cli project env add` to create a new variable.",
|
|
85
89
|
exitCode: 1,
|
|
86
|
-
nextSteps: [`prisma-cli project env add ${key}=<value> ${formatScopeFlag(scope)}`]
|
|
90
|
+
nextSteps: [`prisma-cli project env add ${input.key}=<value> ${formatScopeFlag(scope)}`]
|
|
87
91
|
});
|
|
88
92
|
const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
|
|
89
93
|
params: { path: { envVarId: existing.id } },
|
|
90
|
-
body: { value },
|
|
94
|
+
body: { value: input.value },
|
|
91
95
|
signal: context.runtime.signal
|
|
92
96
|
});
|
|
93
|
-
if (error || !data) throw apiCallError(`Failed to update value for ${key}`, response, error);
|
|
97
|
+
if (error || !data) throw apiCallError(`Failed to update value for ${input.key}`, response, error);
|
|
94
98
|
return {
|
|
95
99
|
command: "project.env.update",
|
|
96
100
|
result: {
|
|
97
101
|
projectId,
|
|
102
|
+
verboseContext,
|
|
98
103
|
scope: resolved.descriptor,
|
|
99
104
|
variable: toMetadata(data.data, resolved.descriptor)
|
|
100
105
|
},
|
|
@@ -102,13 +107,39 @@ async function runEnvUpdate(context, rawAssignment, flags) {
|
|
|
102
107
|
nextSteps: []
|
|
103
108
|
};
|
|
104
109
|
}
|
|
110
|
+
function resolveEnvWriteSource(rawAssignment, filePath, command) {
|
|
111
|
+
if (filePath !== void 0 && rawAssignment !== void 0) throw usageError(`prisma-cli project env ${command} accepts either KEY=VALUE or --file`, "The command received both a positional assignment and a dotenv file path.", "Pass one input source.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
112
|
+
if (filePath !== void 0) {
|
|
113
|
+
if (filePath.length === 0) throw usageError(`prisma-cli project env ${command} --file requires a path`, "The --file flag was passed without a file path.", "Pass a readable dotenv file path.", [`prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
114
|
+
return {
|
|
115
|
+
kind: "file",
|
|
116
|
+
filePath
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (rawAssignment === void 0) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE or --file`, "No environment variable input was supplied.", "Pass a single KEY=VALUE assignment or a dotenv file path.", [`prisma-cli project env ${command} KEY=value --role preview`, `prisma-cli project env ${command} --file .env --role preview`], "app");
|
|
120
|
+
return {
|
|
121
|
+
kind: "single",
|
|
122
|
+
rawAssignment
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function resolveEnvWriteInput(context, source, command) {
|
|
126
|
+
if (source.kind === "file") return {
|
|
127
|
+
kind: "file",
|
|
128
|
+
filePath: source.filePath,
|
|
129
|
+
assignments: await readEnvFileAssignments(context.runtime.cwd, source.filePath, command)
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
kind: "single",
|
|
133
|
+
...parseKeyValuePositional(source.rawAssignment, command, context.runtime.env)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
105
136
|
async function runEnvList(context, flags) {
|
|
106
137
|
const explicit = resolveEnvScope(flags, {
|
|
107
138
|
requireExplicit: false,
|
|
108
139
|
command: "list"
|
|
109
140
|
});
|
|
110
|
-
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
|
|
111
|
-
const resolved = await resolveListScopeToApi(client, projectId, explicit, {
|
|
141
|
+
const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env list");
|
|
142
|
+
const resolved = await resolveListScopeToApi(client, projectId, explicit ?? void 0, {
|
|
112
143
|
cwd: context.runtime.cwd,
|
|
113
144
|
signal: context.runtime.signal
|
|
114
145
|
});
|
|
@@ -121,6 +152,7 @@ async function runEnvList(context, flags) {
|
|
|
121
152
|
command: "project.env.list",
|
|
122
153
|
result: {
|
|
123
154
|
projectId,
|
|
155
|
+
verboseContext,
|
|
124
156
|
scope: resolved.descriptor,
|
|
125
157
|
target: resolved.target,
|
|
126
158
|
variables: variables.map((row) => toMetadata(row, resolved.descriptor))
|
|
@@ -136,7 +168,7 @@ async function runEnvRemove(context, key, flags) {
|
|
|
136
168
|
command: "remove"
|
|
137
169
|
});
|
|
138
170
|
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");
|
|
139
|
-
const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env remove");
|
|
171
|
+
const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env remove");
|
|
140
172
|
const resolved = await resolveScopeToApi(client, projectId, scope, {
|
|
141
173
|
createBranchIfMissing: false,
|
|
142
174
|
signal: context.runtime.signal
|
|
@@ -160,6 +192,7 @@ async function runEnvRemove(context, key, flags) {
|
|
|
160
192
|
command: "project.env.remove",
|
|
161
193
|
result: {
|
|
162
194
|
projectId,
|
|
195
|
+
verboseContext,
|
|
163
196
|
scope: resolved.descriptor,
|
|
164
197
|
key
|
|
165
198
|
},
|
|
@@ -172,15 +205,21 @@ async function requireClientAndProject(context, explicitProject, commandName) {
|
|
|
172
205
|
const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
|
|
173
206
|
if (!client) throw authRequiredError(["prisma-cli auth login"]);
|
|
174
207
|
if (!authState.workspace) throw workspaceRequiredError();
|
|
208
|
+
const target = await resolveProjectTarget({
|
|
209
|
+
context,
|
|
210
|
+
workspace: authState.workspace,
|
|
211
|
+
explicitProject,
|
|
212
|
+
listProjects: () => listRealWorkspaceProjects(client, authState.workspace, context.runtime.signal),
|
|
213
|
+
commandName
|
|
214
|
+
});
|
|
175
215
|
return {
|
|
176
216
|
client,
|
|
177
|
-
projectId:
|
|
178
|
-
|
|
217
|
+
projectId: target.project.id,
|
|
218
|
+
verboseContext: {
|
|
179
219
|
workspace: authState.workspace,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
})).project.id
|
|
220
|
+
project: target.project,
|
|
221
|
+
resolution: target.resolution
|
|
222
|
+
}
|
|
184
223
|
};
|
|
185
224
|
}
|
|
186
225
|
async function resolveScopeToApi(client, projectId, scope, options) {
|
|
@@ -410,18 +449,6 @@ async function projectHasDefaultBranch(client, projectId, signal) {
|
|
|
410
449
|
cursor = result.data.pagination.nextCursor;
|
|
411
450
|
}
|
|
412
451
|
}
|
|
413
|
-
async function findVariableByNaturalKey(client, projectId, key, resolved, signal) {
|
|
414
|
-
const { data, error, response } = await client.GET("/v1/environment-variables", {
|
|
415
|
-
params: { query: {
|
|
416
|
-
projectId,
|
|
417
|
-
class: resolved.apiTarget.class,
|
|
418
|
-
key
|
|
419
|
-
} },
|
|
420
|
-
signal
|
|
421
|
-
});
|
|
422
|
-
if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
|
|
423
|
-
return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
|
|
424
|
-
}
|
|
425
452
|
async function listVariables(client, projectId, resolved, signal) {
|
|
426
453
|
return materializeEffectiveRows(await collectEnvironmentVariables(client, projectId, signal, {
|
|
427
454
|
className: resolved.apiTarget.class,
|
|
@@ -461,9 +488,6 @@ function rowMatchesScope(row, resolved) {
|
|
|
461
488
|
if (resolved.apiTarget.branchId === null) return row.branchId === null;
|
|
462
489
|
return row.branchId === null || row.branchId === resolved.apiTarget.branchId;
|
|
463
490
|
}
|
|
464
|
-
function rowMatchesExactScope(row, resolved) {
|
|
465
|
-
return row.class === resolved.apiTarget.class && row.branchId === resolved.apiTarget.branchId;
|
|
466
|
-
}
|
|
467
491
|
function materializeEffectiveRows(rows, resolved) {
|
|
468
492
|
if (resolved.apiTarget.branchId === null) return rows;
|
|
469
493
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -471,40 +495,5 @@ function materializeEffectiveRows(rows, resolved) {
|
|
|
471
495
|
for (const row of rows) if (row.branchId === resolved.apiTarget.branchId) byKey.set(row.key, row);
|
|
472
496
|
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
473
497
|
}
|
|
474
|
-
function toMetadata(row, requestedScope) {
|
|
475
|
-
const rowScope = row.branchId === null ? {
|
|
476
|
-
kind: "role",
|
|
477
|
-
role: row.class
|
|
478
|
-
} : requestedScope;
|
|
479
|
-
return {
|
|
480
|
-
id: row.id,
|
|
481
|
-
key: row.key,
|
|
482
|
-
scope: rowScope,
|
|
483
|
-
source: formatDescriptorLabel(rowScope),
|
|
484
|
-
isManagedBySystem: row.isManagedBySystem,
|
|
485
|
-
updatedAt: row.updatedAt
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
function formatDescriptorLabel(scope) {
|
|
489
|
-
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
490
|
-
if (scope.kind === "overview") return "overview";
|
|
491
|
-
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
492
|
-
}
|
|
493
|
-
function apiCallError(summary, response, error) {
|
|
494
|
-
const status = response?.status ?? 0;
|
|
495
|
-
const apiCode = error?.error?.code;
|
|
496
|
-
const apiMessage = error?.error?.message;
|
|
497
|
-
const apiHint = error?.error?.hint;
|
|
498
|
-
if (status === 401 || status === 403) return authRequiredError(["prisma auth login"]);
|
|
499
|
-
return new CliError({
|
|
500
|
-
code: apiCode ?? "ENV_API_ERROR",
|
|
501
|
-
domain: "app",
|
|
502
|
-
summary,
|
|
503
|
-
why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
|
|
504
|
-
fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
|
|
505
|
-
exitCode: 1,
|
|
506
|
-
nextSteps: []
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
498
|
//#endregion
|
|
510
499
|
export { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate };
|