Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pnpm prisma-cli auth login
pnpm prisma-cli app deploy --env DATABASE_URL=postgresql://example
pnpm prisma-cli project env add --file .env --role preview
pnpm prisma-cli project env list --role preview
pnpm prisma-cli project env pull
```

If you want local project scripts that look like the future command shape, add:
Expand Down
45 changes: 43 additions & 2 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,9 @@ prisma-cli app deploy --entry src/server.ts --http-port 3000
Manage durable, platform-stored environment variables for the resolved
project. The `env` namespace operates on the
platform-managed `/v1/environment-variables` API; values are stored
encrypted at rest and **never returned** by the platform — read-back
is not supported in Beta.
encrypted at rest. Production values are never returned. Preview values are
returned only by the explicit local-development pull flow, which writes to a
local dotenv file and never prints values to terminal or JSON output.

### Scope flags

Expand Down Expand Up @@ -978,6 +979,46 @@ prisma-cli project env list --role preview
prisma-cli project env list --branch feature/foo
```

### `prisma-cli project env pull [output-file] [--role preview | --branch <git-name>]`

Purpose:

- pull preview environment variable values into a local dotenv file for
development.

Behavior:

- requires auth and a resolved project; accepts `--project <id-or-name>` as an explicit fallback
- defaults the output file to `.env.local`
- optional `output-file` overrides the destination, e.g. `.env`
- explicit `--role preview` pulls the Project preview map
- explicit `--branch` pulls the effective preview Branch snapshot: preview
defaults plus Branch overrides
- with no scope and an active local preview Git branch, pulls the matching
Platform Branch snapshot when the Branch exists; otherwise pulls the preview
map and marks the local branch target as not created yet
- with no scope on `main`, `production`, or outside a Git branch, pulls the
Project preview map
- `--role production` and production Branch targets fail with
`PRODUCTION_ENV_PULL_UNSUPPORTED`
- refuses to write outside the current project directory
- refuses to write to a Git-tracked file
- existing files require interactive confirmation or `--yes`
- values are written to the local file only; human output and JSON output carry
keys, sources, target metadata, and file metadata, never values
- first CLI implementation depends on the Management API exposing
`POST /v1/environment-variables/pull`; missing support returns
`FEATURE_UNAVAILABLE`

Examples:

```bash
prisma-cli project env pull
prisma-cli project env pull .env
prisma-cli project env pull --role preview
prisma-cli project env pull --branch feature/foo
```

### `prisma-cli project env remove KEY (--role <production|preview> | --branch <git-name>)`

Purpose:
Expand Down
2 changes: 2 additions & 0 deletions docs/product/resource-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ Rules:
creates a new deployment
- command output must not print secret values
- listing variables should show names only
- pulling preview variables writes values to a local dotenv file only; production
values remain write-only

The `env` word is reserved for environment-variable ergonomics. The current
top-level target-context group is `branch`, not `env`.
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ npx prisma-cli project env add DATABASE_URL=postgresql://example --role preview
npx prisma-cli project env add --file .env --role preview
npx prisma-cli project env list
npx prisma-cli project env list --role preview
npx prisma-cli project env pull
```

The beta package exposes `prisma-cli` so it can coexist with the existing
Expand Down Expand Up @@ -96,7 +97,8 @@ npx prisma-cli app promote <deployment-id>
- `--no-interactive` and `--yes` for automation.
- `PRISMA_SERVICE_TOKEN` for headless authenticated commands.
- Stable command groups, flags, and error codes for scripts and agents.
- Environment variable values are not printed back to the terminal.
- Environment variable values are not printed back to the terminal. Preview
values can be pulled into a local dotenv file for development.

### Agent skills

Expand Down
44 changes: 43 additions & 1 deletion packages/cli/src/commands/env.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Command, Option } from "commander";

import { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate } from "../controllers/app-env";
import { runEnvAdd, runEnvList, runEnvPull, runEnvRemove, runEnvUpdate } from "../controllers/app-env";
import {
renderEnvAdd,
renderEnvList,
renderEnvPull,
renderEnvRm,
renderEnvUpdate,
serializeEnvAdd,
serializeEnvList,
serializeEnvPull,
serializeEnvRm,
serializeEnvUpdate,
} from "../presenters/app-env";
Expand All @@ -18,6 +20,7 @@ import { configureRuntimeCommand, type CliRuntime } from "../shell/runtime";
import type {
EnvAddResult,
EnvListResult,
EnvPullResult,
EnvRmResult,
EnvUpdateResult,
} from "../types/app-env";
Expand All @@ -32,6 +35,7 @@ export function createEnvCommand(runtime: CliRuntime): Command {
env.addCommand(createEnvAddCommand(runtime));
env.addCommand(createEnvUpdateCommand(runtime));
env.addCommand(createEnvListCommand(runtime));
env.addCommand(createEnvPullCommand(runtime));
env.addCommand(createEnvRemoveCommand(runtime));

return env;
Expand Down Expand Up @@ -154,6 +158,44 @@ function createEnvListCommand(runtime: CliRuntime): Command {
return command;
}

function createEnvPullCommand(runtime: CliRuntime): Command {
const command = attachCommandDescriptor(
configureRuntimeCommand(new Command("pull"), runtime),
"project.env.pull",
);

command
.argument("[output-file]", "Local dotenv file to write", ".env.local")
.addOption(
new Option(
"--role <role>",
"Project template scope",
).choices(["production", "preview"]),
)
.addOption(new Option("--branch <git-name>", "Preview branch resolved scope"))
.addOption(new Option("--project <id-or-name>", "Project id or name"));
addGlobalFlags(command);

command.action(async (outputFile: string, options) => {
const roleName = (options as { role?: string }).role;
const branchName = (options as { branch?: string }).branch;
const projectRef = (options as { project?: string }).project;

await runCommand<EnvPullResult>(
runtime,
"project.env.pull",
options as Record<string, unknown>,
(context) => runEnvPull(context, outputFile, { roleName, branchName, projectRef }),
{
renderHuman: (context, descriptor, result) => renderEnvPull(context, descriptor, result),
renderJson: (result) => serializeEnvPull(result),
},
);
});

return command;
}

function createEnvRemoveCommand(runtime: CliRuntime): Command {
const command = attachCommandDescriptor(
configureRuntimeCommand(new Command("remove"), runtime),
Expand Down
63 changes: 62 additions & 1 deletion packages/cli/src/controllers/app-env-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ManagementApiClient } from "@prisma/management-api-sdk";

import type { EnvVarRole } from "../lib/app/env-config";
import { authRequiredError, CliError } from "../shell/errors";
import { authRequiredError, CliError, featureUnavailableError } from "../shell/errors";
import type { EnvScopeDescriptor, EnvVariableMetadata } from "../types/app-env";

export interface ResolvedEnvApiScope {
Expand All @@ -18,6 +18,13 @@ export interface RawEnvironmentVariable {
updatedAt: string;
}

export interface RawPulledEnvironmentVariable {
key: string;
value: string;
source: string;
isManagedBySystem?: boolean;
}

interface ApiErrorBody {
error?: {
code?: string;
Expand All @@ -26,6 +33,60 @@ interface ApiErrorBody {
};
}

type PullEnvPost = (
path: "/v1/environment-variables/pull",
options: {
body: {
projectId: string;
class: "preview";
branchId?: string;
};
signal: AbortSignal;
},
) => Promise<{
data?: {
data?: {
variables?: RawPulledEnvironmentVariable[];
};
};
error?: ApiErrorBody;
response?: Response;
}>;

export async function pullPreviewEnvironmentVariables(
client: ManagementApiClient,
options: {
projectId: string;
branchId: string | null;
signal: AbortSignal;
},
): Promise<RawPulledEnvironmentVariable[]> {
const post = client.POST as unknown as PullEnvPost;
const { data, error, response } = await post("/v1/environment-variables/pull", {
body: {
projectId: options.projectId,
class: "preview",
...(options.branchId !== null ? { branchId: options.branchId } : {}),
},
signal: options.signal,
});

if (error || !data?.data?.variables) {
if (response?.status === 404 || response?.status === 405 || response?.status === 501) {
throw featureUnavailableError(
"Preview environment variable pull is not available yet",
"The CLI command is ready, but the platform endpoint for returning preview values is not available in this environment.",
"Retry after the Management API exposes POST /v1/environment-variables/pull.",
["prisma-cli project env list --role preview"],
"app",
);
}
throw apiCallError("Failed to pull preview environment variables", response, error);
}

return [...data.data.variables].sort((left, right) => left.key.localeCompare(right.key));
}

export async function findVariableByNaturalKey(
client: ManagementApiClient,
projectId: string,
Expand Down
Loading
Loading