@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.5

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 CHANGED
@@ -57,6 +57,8 @@ 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
61
+ npx prisma-cli project env list
60
62
  npx prisma-cli project env list --role preview
61
63
  ```
62
64
 
@@ -73,7 +75,7 @@ The beta package exposes `prisma-cli` so it can coexist with the existing
73
75
  | `auth` | Log in, log out, and inspect the active Prisma account. |
74
76
  | `project` | List projects, show the resolved project, and manage project environment variables. |
75
77
  | `git` | Connect or disconnect a project from a GitHub repository. |
76
- | `branch` | Inspect the Prisma branch that maps to the current work context. |
78
+ | `branch` | List Prisma branches for the resolved project. |
77
79
  | `app` | Build, run, deploy, inspect, open, stream logs, promote, roll back, and remove apps. |
78
80
 
79
81
  Common examples:
@@ -82,7 +84,7 @@ Common examples:
82
84
  npx prisma-cli version
83
85
  npx prisma-cli auth whoami
84
86
  npx prisma-cli project show
85
- npx prisma-cli branch show
87
+ npx prisma-cli branch list
86
88
  npx prisma-cli app deploy --branch feat-login --framework nextjs
87
89
  npx prisma-cli app promote <deployment-id>
88
90
  ```
@@ -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));
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;
@@ -75,21 +76,32 @@ function createDeployCommand(runtime) {
75
76
  const envAssignments = options.env;
76
77
  const projectRef = options.project;
77
78
  const createProjectName = options.createProject;
78
- await runCommand(runtime, "app.deploy", options, (context) => runAppDeploy(context, appName, {
79
- projectRef,
80
- createProjectName,
81
- branchName,
82
- entrypoint: entry,
83
- framework,
84
- httpPort,
85
- envAssignments
86
- }), {
79
+ const prod = options.prod;
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
+ }, {
87
96
  renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
88
97
  renderJson: (result) => serializeAppDeploy(result)
89
98
  });
90
99
  });
91
100
  return command;
92
101
  }
102
+ function hasFlag(argv, flag) {
103
+ return argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
104
+ }
93
105
  function createShowCommand(runtime) {
94
106
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "app.show");
95
107
  command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name"));
@@ -2,16 +2,14 @@ import { attachCommandDescriptor } from "../../shell/command-meta.js";
2
2
  import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
3
3
  import { configureRuntimeCommand } from "../../shell/runtime.js";
4
4
  import { runCommand } from "../../shell/command-runner.js";
5
- import { runBranchList, runBranchShow, runBranchUse } from "../../controllers/branch.js";
6
- import { renderBranchList, renderBranchShow, renderBranchUse, serializeBranchList, serializeBranchShow } from "../../presenters/branch.js";
5
+ import { runBranchList } from "../../controllers/branch.js";
6
+ import { renderBranchList, serializeBranchList } from "../../presenters/branch.js";
7
7
  import { Command } from "commander";
8
8
  //#region src/commands/branch/index.ts
9
9
  function createBranchCommand(runtime) {
10
10
  const branch = attachCommandDescriptor(configureRuntimeCommand(new Command("branch"), runtime), "branch");
11
11
  addCompactGlobalFlags(branch);
12
12
  branch.addCommand(createBranchListCommand(runtime));
13
- branch.addCommand(createBranchShowCommand(runtime));
14
- branch.addCommand(createBranchUseCommand(runtime));
15
13
  return branch;
16
14
  }
17
15
  function createBranchListCommand(runtime) {
@@ -25,28 +23,5 @@ function createBranchListCommand(runtime) {
25
23
  });
26
24
  return command;
27
25
  }
28
- function createBranchShowCommand(runtime) {
29
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "branch.show");
30
- addGlobalFlags(command);
31
- command.action(async (options) => {
32
- await runCommand(runtime, "branch.show", options, (context) => runBranchShow(context), {
33
- renderHuman: (context, descriptor, result) => renderBranchShow(context, descriptor, result),
34
- renderJson: (result) => serializeBranchShow(result)
35
- });
36
- });
37
- return command;
38
- }
39
- function createBranchUseCommand(runtime) {
40
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("use"), runtime), "branch.use");
41
- command.argument("[name]", "Branch name");
42
- addGlobalFlags(command);
43
- command.action(async (branchName, options) => {
44
- await runCommand(runtime, "branch.use", options, (context) => runBranchUse(context, branchName), {
45
- renderHuman: (context, descriptor, result) => renderBranchUse(context, descriptor, result),
46
- renderJson: (result) => serializeBranchShow(result)
47
- });
48
- });
49
- return command;
50
- }
51
26
  //#endregion
52
27
  export { createBranchCommand };
@@ -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("<assignment>", "Variable assignment as KEY=VALUE or KEY from the current environment").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"));
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("<assignment>", "Variable assignment as KEY=VALUE or KEY from the current environment").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"));
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,179 @@
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) {
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
+ scope: resolved.descriptor,
44
+ variables,
45
+ file: {
46
+ path: filePath,
47
+ count: variables.length
48
+ }
49
+ },
50
+ warnings,
51
+ nextSteps: []
52
+ };
53
+ }
54
+ async function runEnvUpdateFile(context, client, projectId, resolved, filePath, assignments) {
55
+ const existing = await findVariablesByNaturalKey(client, projectId, assignments.map((assignment) => assignment.key), resolved, context.runtime.signal);
56
+ const missingKeys = assignments.map((assignment) => assignment.key).filter((key) => !existing.has(key));
57
+ if (missingKeys.length > 0) throw new CliError({
58
+ code: "ENV_VARIABLE_NOT_FOUND",
59
+ domain: "app",
60
+ summary: `${missingKeys.length} environment variable(s) not found in ${formatScopeLabel(resolved.scope)}`,
61
+ why: `Missing keys: ${formatKeyList(missingKeys)}.`,
62
+ fix: "Split the input file by key state: add missing keys and update existing keys separately.",
63
+ exitCode: 1,
64
+ nextSteps: splitFileNextSteps(filePath, resolved.scope, {
65
+ missingKeys,
66
+ first: "add-missing"
67
+ }),
68
+ meta: { keys: missingKeys }
69
+ });
70
+ const variables = [];
71
+ for (const assignment of assignments) {
72
+ const existingVariable = existing.get(assignment.key);
73
+ if (!existingVariable) continue;
74
+ try {
75
+ const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
76
+ params: { path: { envVarId: existingVariable.id } },
77
+ body: { value: assignment.value },
78
+ signal: context.runtime.signal
79
+ });
80
+ if (error || !data) throw apiCallError(`Failed to update value for ${assignment.key}`, response, error);
81
+ variables.push(toMetadata(data.data, resolved.descriptor));
82
+ } catch (error) {
83
+ throw envFileApplyFailedError("update", filePath, resolved.scope, assignment.key, variables, error);
84
+ }
85
+ }
86
+ return {
87
+ command: "project.env.update",
88
+ result: {
89
+ projectId,
90
+ scope: resolved.descriptor,
91
+ variables,
92
+ file: {
93
+ path: filePath,
94
+ count: variables.length
95
+ }
96
+ },
97
+ warnings: [],
98
+ nextSteps: []
99
+ };
100
+ }
101
+ async function findVariablesByNaturalKey(client, projectId, keys, resolved, signal) {
102
+ const found = /* @__PURE__ */ new Map();
103
+ for (const key of keys) {
104
+ const row = await findVariableByNaturalKey(client, projectId, key, resolved, signal);
105
+ if (row) found.set(key, row);
106
+ }
107
+ return found;
108
+ }
109
+ async function missingPreviewDefaultWarnings(client, projectId, scope, keys, signal) {
110
+ if (scope.kind !== "branch") return [];
111
+ const previewScope = {
112
+ scope: {
113
+ kind: "role",
114
+ role: "preview"
115
+ },
116
+ descriptor: {
117
+ kind: "role",
118
+ role: "preview"
119
+ },
120
+ apiTarget: {
121
+ class: "preview",
122
+ branchId: null
123
+ }
124
+ };
125
+ const missing = [];
126
+ for (const key of keys) if (!await findVariableByNaturalKey(client, projectId, key, previewScope, signal)) missing.push(key);
127
+ if (missing.length === 0) return [];
128
+ if (missing.length === 1) return [`Variable "${missing[0]}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`];
129
+ return [`Variables ${formatKeyList(missing)} do not exist in preview. They will only exist on ${formatScopeLabel(scope)}.`];
130
+ }
131
+ function envFileApplyFailedError(command, filePath, scope, failedKey, writtenVariables, error) {
132
+ const writtenKeys = writtenVariables.map((variable) => variable.key);
133
+ const cause = error instanceof CliError ? error.summary : error instanceof Error ? error.message : "Unknown error.";
134
+ return new CliError({
135
+ code: "ENV_FILE_APPLY_FAILED",
136
+ domain: "app",
137
+ summary: `Failed to ${command} "${failedKey}" from "${filePath}"`,
138
+ why: writtenKeys.length === 0 ? `No variables were written before ${failedKey} failed. Cause: ${cause}` : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
139
+ fix: "Inspect the target scope, then retry the remaining keys once the API issue is resolved.",
140
+ exitCode: 1,
141
+ nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`, retryStepForApplyFailure(command, filePath, scope, writtenKeys)],
142
+ meta: {
143
+ file: filePath,
144
+ failedKey,
145
+ writtenKeys
146
+ }
147
+ });
148
+ }
149
+ function retryStepForApplyFailure(command, filePath, scope, writtenKeys) {
150
+ if (command === "update") return `prisma-cli project env update --file ${filePath} ${formatScopeFlag(scope)}`;
151
+ if (writtenKeys.length === 0) return `prisma-cli project env add --file ${filePath} ${formatScopeFlag(scope)}`;
152
+ return `prisma-cli project env add --file <remaining.env> ${formatScopeFlag(scope)}`;
153
+ }
154
+ function splitFileNextSteps(filePath, scope, options) {
155
+ const scopeFlag = formatScopeFlag(scope);
156
+ const existingFile = `${filePath}.existing`;
157
+ const newFile = `${filePath}.new`;
158
+ if (options.first === "update-existing") return [
159
+ `# existing keys: ${formatKeyList(options.existingKeys)}`,
160
+ `prisma-cli project env update --file ${existingFile} ${scopeFlag}`,
161
+ "# new keys only",
162
+ `prisma-cli project env add --file ${newFile} ${scopeFlag}`
163
+ ];
164
+ return [
165
+ `# missing keys: ${formatKeyList(options.missingKeys)}`,
166
+ `prisma-cli project env add --file ${newFile} ${scopeFlag}`,
167
+ "# existing keys only",
168
+ `prisma-cli project env update --file ${existingFile} ${scopeFlag}`
169
+ ];
170
+ }
171
+ function formatKeyList(keys) {
172
+ return keys.map((key) => `"${key}"`).join(", ");
173
+ }
174
+ function formatScopeFlag(scope) {
175
+ if (scope.kind === "role") return `--role ${scope.role}`;
176
+ return `--branch ${scope.branchName}`;
177
+ }
178
+ //#endregion
179
+ export { runEnvAddFile, runEnvUpdateFile };