diff --git a/.github/workflows/AGENTS.md b/.github/workflows/AGENTS.md
new file mode 100644
index 0000000..8f26217
--- /dev/null
+++ b/.github/workflows/AGENTS.md
@@ -0,0 +1,3 @@
+# GitHub Workflows
+
+- Pin all GitHub Actions to their commit SHA, not a tag. Append the tag as a comment for readability (e.g., `uses: actions/checkout@abc123... # v4.3.2`).
diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml
new file mode 100644
index 0000000..93ab484
--- /dev/null
+++ b/.github/workflows/pr-quality.yml
@@ -0,0 +1,81 @@
+name: PR Quality
+
+# This workflow owns required PR status checks. Add only jobs that should block
+# merging when they fail.
+
+on:
+ pull_request:
+
+concurrency:
+ group: pr-quality-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ typecheck:
+ name: Type Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
+
+ - name: Set up Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version-file: .node-version
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Type check
+ run: pnpm --recursive exec tsc --noEmit
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+
+ - name: Set up Biome
+ uses: biomejs/setup-biome@4c91541eaada48f67d7dbd7833600ce162b68f51 # v2.7.1
+
+ - name: Lint
+ run: biome ci .
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
+
+ - name: Set up Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version-file: .node-version
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Test
+ run: pnpm --recursive test
diff --git a/.node-version b/.node-version
new file mode 100644
index 0000000..1d9b783
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+22.12.0
diff --git a/biome.jsonc b/biome.jsonc
new file mode 100644
index 0000000..9d7b8c0
--- /dev/null
+++ b/biome.jsonc
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "ignoreUnknown": false
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "complexity": {
+ "noExcessiveCognitiveComplexity": "on",
+ "noForEach": "on",
+ "noImplicitCoercions": "on"
+ },
+ "nursery": {
+ "noConditionalExpect": "on",
+ "noFloatingPromises": "on",
+ "noForIn": "on",
+ "noLoopFunc": "on",
+ "useDisposables": "on"
+ },
+ "performance": {
+ "noAwaitInLoops": "on",
+ "noBarrelFile": "on",
+ "useTopLevelRegex": "on"
+ },
+ "style": {
+ "useCollapsedElseIf": "on",
+ "useCollapsedIf": "on",
+ "noNestedTernary": "on",
+ "noParameterAssign": "on"
+ },
+ "suspicious": {
+ "noVar": "on",
+ "useGuardForIn": "on",
+ "useStaticResponseMethods": "on"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ }
+}
diff --git a/examples/next-smoke/app/globals.css b/examples/next-smoke/app/globals.css
index 91c4672..7b2f897 100644
--- a/examples/next-smoke/app/globals.css
+++ b/examples/next-smoke/app/globals.css
@@ -21,28 +21,27 @@ body {
body {
background:
- radial-gradient(circle at top left, rgba(42, 125, 99, 0.18), transparent 28rem),
- radial-gradient(circle at bottom right, rgba(197, 123, 74, 0.16), transparent 30rem),
+ radial-gradient(
+ circle at top left,
+ rgba(42, 125, 99, 0.18),
+ transparent 28rem
+ ),
+ radial-gradient(
+ circle at bottom right,
+ rgba(197, 123, 74, 0.16),
+ transparent 30rem
+ ),
var(--bg);
color: var(--text);
font-family:
- "Iowan Old Style",
- "Palatino Linotype",
- "Book Antiqua",
- Palatino,
- Georgia,
+ "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, Georgia,
serif;
}
code {
font-family:
- "SFMono-Regular",
- ui-monospace,
- "Cascadia Mono",
- "Segoe UI Mono",
- Menlo,
- Consolas,
- monospace;
+ SFMono-Regular, ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo,
+ Consolas, monospace;
background: rgba(42, 44, 39, 0.06);
border-radius: 0.5rem;
padding: 0.12rem 0.4rem;
diff --git a/examples/next-smoke/app/page.tsx b/examples/next-smoke/app/page.tsx
index d257002..658edee 100644
--- a/examples/next-smoke/app/page.tsx
+++ b/examples/next-smoke/app/page.tsx
@@ -5,7 +5,8 @@ export default function Home() {
Prisma CLI
Next.js smoke app
- This app exists to manually test the local source Prisma CLI from inside this repository.
+ This app exists to manually test the local source Prisma CLI from
+ inside this repository.
-
diff --git a/examples/next-smoke/tsconfig.json b/examples/next-smoke/tsconfig.json
index bb35a09..d73edca 100644
--- a/examples/next-smoke/tsconfig.json
+++ b/examples/next-smoke/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -30,7 +26,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
- "exclude": [
- "node_modules"
- ]
+ "exclude": ["node_modules"]
}
diff --git a/package.json b/package.json
index bdc72d6..e1a2a58 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,16 @@
{
"name": "prisma-cli",
"private": true,
+ "engines": {
+ "node": ">=22.12.0"
+ },
"packageManager": "pnpm@10.30.0",
"scripts": {
"build:cli": "pnpm --filter @prisma/cli build",
"build:compute": "pnpm --filter @prisma/compute build",
+ "format": "biome format . --write",
+ "lint": "biome check .",
+ "lint:fix": "biome check . --write",
"lint:skills": "node scripts/validate-skills.mjs",
"prepare": "skills add ./skills --skill '*' --agent universal claude-code -y",
"prepare:cli-publish": "node scripts/prepare-cli-publish.mjs",
@@ -15,6 +21,7 @@
"prisma": "tsx packages/cli/src/bin.ts"
},
"devDependencies": {
+ "@biomejs/biome": "2.4.16",
"gray-matter": "^4.0.3",
"pkg-pr-new": "^0.0.75",
"skills": "^1.5.9",
diff --git a/packages/cli/src/adapters/git.ts b/packages/cli/src/adapters/git.ts
index ba73cee..8fd4845 100644
--- a/packages/cli/src/adapters/git.ts
+++ b/packages/cli/src/adapters/git.ts
@@ -1,3 +1,4 @@
+// biome-ignore-all lint/performance/useTopLevelRegex: Existing git URL parsing regexes are kept inline for readability.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@@ -11,13 +12,20 @@ export interface GitHubRepositoryReference {
url: string;
}
-export async function readGitOriginRemote(cwd: string, signal?: AbortSignal): Promise {
+export async function readGitOriginRemote(
+ cwd: string,
+ signal?: AbortSignal,
+): Promise {
try {
- const { stdout } = await execFileAsync("git", ["config", "--get", "remote.origin.url"], {
- cwd,
- timeout: 5_000,
- signal,
- });
+ const { stdout } = await execFileAsync(
+ "git",
+ ["config", "--get", "remote.origin.url"],
+ {
+ cwd,
+ timeout: 5_000,
+ signal,
+ },
+ );
const remote = stdout.trim();
return remote.length > 0 ? remote : null;
} catch (error) {
@@ -30,9 +38,13 @@ function isAbortError(error: unknown): boolean {
return error instanceof Error && error.name === "AbortError";
}
-export function parseGitHubRepositoryUrl(value: string): GitHubRepositoryReference | null {
+export function parseGitHubRepositoryUrl(
+ value: string,
+): GitHubRepositoryReference | null {
const input = value.trim();
- const shorthand = input.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
+ const shorthand = input.match(
+ /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/,
+ );
if (shorthand) {
return toGitHubRepositoryReference(shorthand[1], shorthand[2]);
@@ -49,7 +61,11 @@ export function parseGitHubRepositoryUrl(value: string): GitHubRepositoryReferen
return null;
}
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:" && parsed.protocol !== "ssh:") {
+ if (
+ parsed.protocol !== "https:" &&
+ parsed.protocol !== "http:" &&
+ parsed.protocol !== "ssh:"
+ ) {
return null;
}
@@ -64,7 +80,10 @@ export function parseGitHubRepositoryUrl(value: string): GitHubRepositoryReferen
return toGitHubRepositoryReference(owner, name);
}
-function toGitHubRepositoryReference(owner: string | undefined, name: string | undefined): GitHubRepositoryReference | null {
+function toGitHubRepositoryReference(
+ owner: string | undefined,
+ name: string | undefined,
+): GitHubRepositoryReference | null {
if (!owner || !name || owner.includes("/") || name.includes("/")) {
return null;
}
diff --git a/packages/cli/src/adapters/local-state.ts b/packages/cli/src/adapters/local-state.ts
index b9726a3..a885800 100644
--- a/packages/cli/src/adapters/local-state.ts
+++ b/packages/cli/src/adapters/local-state.ts
@@ -60,28 +60,36 @@ export function resolveLocalStateFilePath(stateDir: string): string {
export class LocalStateStore {
private readonly stateFilePath: string;
- constructor(stateDir: string, private readonly signal?: AbortSignal) {
+ constructor(
+ stateDir: string,
+ private readonly signal?: AbortSignal,
+ ) {
this.stateFilePath = resolveLocalStateFilePath(stateDir);
}
async read(): Promise {
this.signal?.throwIfAborted();
try {
- const raw = await readFile(this.stateFilePath, { encoding: "utf8", signal: this.signal });
+ const raw = await readFile(this.stateFilePath, {
+ encoding: "utf8",
+ signal: this.signal,
+ });
const parsed = JSON.parse(raw) as Partial;
return {
auth: parsed.auth ?? structuredClone(DEFAULT_STATE.auth),
project: {
rememberedByWorkspace: parsed.project?.rememberedByWorkspace ?? {},
lastResolved: parsed.project?.lastResolved ?? null,
- repositoryConnectionsByProject: parsed.project?.repositoryConnectionsByProject ?? {},
+ repositoryConnectionsByProject:
+ parsed.project?.repositoryConnectionsByProject ?? {},
},
branch: {
active: parsed.branch?.active ?? DEFAULT_STATE.branch.active,
},
app: {
selectedByProject: parsed.app?.selectedByProject ?? {},
- knownLiveDeploymentByProject: parsed.app?.knownLiveDeploymentByProject ?? {},
+ knownLiveDeploymentByProject:
+ parsed.app?.knownLiveDeploymentByProject ?? {},
},
};
} catch (error) {
@@ -98,11 +106,15 @@ export class LocalStateStore {
// mkdir does not accept AbortSignal; check before the filesystem boundary.
await mkdir(path.dirname(this.stateFilePath), { recursive: true });
this.signal?.throwIfAborted();
- await writeFile(this.stateFilePath, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8" });
+ await writeFile(this.stateFilePath, `${JSON.stringify(state, null, 2)}\n`, {
+ encoding: "utf8",
+ });
this.signal?.throwIfAborted();
}
- async setAuthSession(session: NonNullable): Promise {
+ async setAuthSession(
+ session: NonNullable,
+ ): Promise {
const state = await this.read();
state.auth = session;
await this.write(state);
@@ -123,7 +135,9 @@ export class LocalStateStore {
return state;
}
- async readRememberedProject(workspaceId: string): Promise {
+ async readRememberedProject(
+ workspaceId: string,
+ ): Promise {
const state = await this.read();
return state.project.rememberedByWorkspace[workspaceId] ?? null;
}
@@ -133,7 +147,9 @@ export class LocalStateStore {
return state.project.lastResolved;
}
- async setRememberedProject(project: RememberedProjectState): Promise {
+ async setRememberedProject(
+ project: RememberedProjectState,
+ ): Promise {
const state = await this.read();
state.project.rememberedByWorkspace[project.workspaceId] = project;
state.project.lastResolved = project;
@@ -141,7 +157,9 @@ export class LocalStateStore {
return state;
}
- async readRepositoryConnection(projectId: string): Promise {
+ async readRepositoryConnection(
+ projectId: string,
+ ): Promise {
const state = await this.read();
return state.project.repositoryConnectionsByProject[projectId] ?? null;
}
@@ -168,14 +186,20 @@ export class LocalStateStore {
return state.app.selectedByProject[projectId] ?? null;
}
- async setSelectedApp(projectId: string, app: SelectedAppState): Promise {
+ async setSelectedApp(
+ projectId: string,
+ app: SelectedAppState,
+ ): Promise {
const state = await this.read();
state.app.selectedByProject[projectId] = app;
await this.write(state);
return state;
}
- async clearSelectedApp(projectId: string, appId: string): Promise {
+ async clearSelectedApp(
+ projectId: string,
+ appId: string,
+ ): Promise {
const state = await this.read();
const selectedApp = state.app.selectedByProject[projectId];
@@ -188,12 +212,19 @@ export class LocalStateStore {
return state;
}
- async readKnownLiveDeployment(projectId: string, appId: string): Promise {
+ async readKnownLiveDeployment(
+ projectId: string,
+ appId: string,
+ ): Promise {
const state = await this.read();
return state.app.knownLiveDeploymentByProject[projectId]?.[appId] ?? null;
}
- async setKnownLiveDeployment(projectId: string, appId: string, deploymentId: string): Promise {
+ async setKnownLiveDeployment(
+ projectId: string,
+ appId: string,
+ deploymentId: string,
+ ): Promise {
const state = await this.read();
state.app.knownLiveDeploymentByProject[projectId] ??= {};
state.app.knownLiveDeploymentByProject[projectId][appId] = deploymentId;
@@ -201,9 +232,13 @@ export class LocalStateStore {
return state;
}
- async clearKnownLiveDeployment(projectId: string, appId: string): Promise {
+ async clearKnownLiveDeployment(
+ projectId: string,
+ appId: string,
+ ): Promise {
const state = await this.read();
- const projectDeployments = state.app.knownLiveDeploymentByProject[projectId];
+ const projectDeployments =
+ state.app.knownLiveDeploymentByProject[projectId];
if (!projectDeployments || !(appId in projectDeployments)) {
return state;
diff --git a/packages/cli/src/adapters/mock-api.ts b/packages/cli/src/adapters/mock-api.ts
index 6ac694f..5e5f3ff 100644
--- a/packages/cli/src/adapters/mock-api.ts
+++ b/packages/cli/src/adapters/mock-api.ts
@@ -88,7 +88,10 @@ export class MockApi {
this.data = data;
}
- static async load(fixturePath: string, signal?: AbortSignal): Promise {
+ static async load(
+ fixturePath: string,
+ signal?: AbortSignal,
+ ): Promise {
signal?.throwIfAborted();
const raw = await readFile(fixturePath, { encoding: "utf8", signal });
return new MockApi(JSON.parse(raw) as MockApiData);
@@ -103,15 +106,22 @@ export class MockApi {
}
listUsersForProvider(providerId: AuthProviderId): UserRecord[] {
- return this.data.users.filter((user) => user.providerIds.includes(providerId));
+ return this.data.users.filter((user) =>
+ user.providerIds.includes(providerId),
+ );
}
getUser(userId: string): UserRecord | undefined {
return this.data.users.find((user) => user.id === userId);
}
- getUserForProvider(providerId: AuthProviderId, userId: string): UserRecord | undefined {
- return this.listUsersForProvider(providerId).find((user) => user.id === userId);
+ getUserForProvider(
+ providerId: AuthProviderId,
+ userId: string,
+ ): UserRecord | undefined {
+ return this.listUsersForProvider(providerId).find(
+ (user) => user.id === userId,
+ );
}
listUserWorkspaces(userId: string): WorkspaceRecord[] {
@@ -119,49 +129,81 @@ export class MockApi {
.filter((membership) => membership.userId === userId)
.map((membership) => membership.workspaceId);
- return this.data.workspaces.filter((workspace) => workspaceIds.includes(workspace.id));
+ return this.data.workspaces.filter((workspace) =>
+ workspaceIds.includes(workspace.id),
+ );
}
getWorkspace(workspaceId: string): WorkspaceRecord | undefined {
- return this.data.workspaces.find((workspace) => workspace.id === workspaceId);
+ return this.data.workspaces.find(
+ (workspace) => workspace.id === workspaceId,
+ );
}
- getUserWorkspace(userId: string, workspaceId: string): WorkspaceRecord | undefined {
- return this.listUserWorkspaces(userId).find((workspace) => workspace.id === workspaceId);
+ getUserWorkspace(
+ userId: string,
+ workspaceId: string,
+ ): WorkspaceRecord | undefined {
+ return this.listUserWorkspaces(userId).find(
+ (workspace) => workspace.id === workspaceId,
+ );
}
listProjectsForWorkspace(workspaceId: string): ProjectRecord[] {
- return this.data.projects.filter((project) => project.workspaceId === workspaceId);
+ return this.data.projects.filter(
+ (project) => project.workspaceId === workspaceId,
+ );
}
getProject(projectId: string): ProjectRecord | undefined {
return this.data.projects.find((project) => project.id === projectId);
}
- getProjectForWorkspace(workspaceId: string, projectId: string): ProjectRecord | undefined {
- return this.listProjectsForWorkspace(workspaceId).find((project) => project.id === projectId);
+ getProjectForWorkspace(
+ workspaceId: string,
+ projectId: string,
+ ): ProjectRecord | undefined {
+ return this.listProjectsForWorkspace(workspaceId).find(
+ (project) => project.id === projectId,
+ );
}
listBranchesForProject(projectId: string): BranchRecord[] {
- return this.data.branches.filter((branch) => branch.projectId === projectId);
+ return this.data.branches.filter(
+ (branch) => branch.projectId === projectId,
+ );
}
- getBranchForProject(projectId: string, name: string): BranchRecord | undefined {
- return this.listBranchesForProject(projectId).find((branch) => branch.name === name);
+ getBranchForProject(
+ projectId: string,
+ name: string,
+ ): BranchRecord | undefined {
+ return this.listBranchesForProject(projectId).find(
+ (branch) => branch.name === name,
+ );
}
getDeployment(deploymentId: string): DeploymentRecord | undefined {
- return this.data.deployments.find((deployment) => deployment.id === deploymentId);
+ return this.data.deployments.find(
+ (deployment) => deployment.id === deploymentId,
+ );
}
- listDatabasesForProject(projectId: string, branchName?: string): DatabaseRecord[] {
- return (this.data.databases ?? []).filter((database) =>
- database.projectId === projectId && (!branchName || database.branchName === branchName)
+ listDatabasesForProject(
+ projectId: string,
+ branchName?: string,
+ ): DatabaseRecord[] {
+ return (this.data.databases ?? []).filter(
+ (database) =>
+ database.projectId === projectId &&
+ (!branchName || database.branchName === branchName),
);
}
getDatabase(databaseId: string): DatabaseRecord | undefined {
- return (this.data.databases ?? []).find((database) => database.id === databaseId);
+ return (this.data.databases ?? []).find(
+ (database) => database.id === databaseId,
+ );
}
createDatabase(input: {
@@ -169,14 +211,21 @@ export class MockApi {
name: string;
branchName?: string;
region?: string;
- }): { database: DatabaseRecord; connection: DatabaseConnectionRecord; connectionString: string } {
+ }): {
+ database: DatabaseRecord;
+ connection: DatabaseConnectionRecord;
+ connectionString: string;
+ } {
this.data.databases ??= [];
this.data.databaseConnections ??= [];
const database: DatabaseRecord = {
id: `db_${this.data.databases.length + 1_000}`,
projectId: input.projectId,
- branchId: input.branchName ? this.getBranchForProject(input.projectId, input.branchName)?.id ?? null : null,
+ branchId: input.branchName
+ ? (this.getBranchForProject(input.projectId, input.branchName)?.id ??
+ null)
+ : null,
branchName: input.branchName ?? null,
name: input.name,
region: input.region ?? null,
@@ -207,23 +256,35 @@ export class MockApi {
return undefined;
}
- this.data.databases = this.data.databases.filter((candidate) => candidate.id !== databaseId);
- this.data.databaseConnections = this.data.databaseConnections.filter((connection) => connection.databaseId !== databaseId);
+ this.data.databases = this.data.databases.filter(
+ (candidate) => candidate.id !== databaseId,
+ );
+ this.data.databaseConnections = this.data.databaseConnections.filter(
+ (connection) => connection.databaseId !== databaseId,
+ );
return database;
}
listDatabaseConnections(databaseId: string): DatabaseConnectionRecord[] {
- return (this.data.databaseConnections ?? []).filter((connection) => connection.databaseId === databaseId);
+ return (this.data.databaseConnections ?? []).filter(
+ (connection) => connection.databaseId === databaseId,
+ );
}
- getDatabaseConnection(connectionId: string): DatabaseConnectionRecord | undefined {
- return (this.data.databaseConnections ?? []).find((connection) => connection.id === connectionId);
+ getDatabaseConnection(
+ connectionId: string,
+ ): DatabaseConnectionRecord | undefined {
+ return (this.data.databaseConnections ?? []).find(
+ (connection) => connection.id === connectionId,
+ );
}
createDatabaseConnection(input: {
databaseId: string;
name: string;
- }): { connection: DatabaseConnectionRecord; connectionString: string } | undefined {
+ }):
+ | { connection: DatabaseConnectionRecord; connectionString: string }
+ | undefined {
const database = this.getDatabase(input.databaseId);
if (!database) {
return undefined;
@@ -242,23 +303,27 @@ export class MockApi {
return { connection, connectionString };
}
- removeDatabaseConnection(connectionId: string): DatabaseConnectionRecord | undefined {
+ removeDatabaseConnection(
+ connectionId: string,
+ ): DatabaseConnectionRecord | undefined {
this.data.databaseConnections ??= [];
const connection = this.getDatabaseConnection(connectionId);
if (!connection) {
return undefined;
}
- this.data.databaseConnections = this.data.databaseConnections.filter((candidate) => candidate.id !== connectionId);
+ this.data.databaseConnections = this.data.databaseConnections.filter(
+ (candidate) => candidate.id !== connectionId,
+ );
return connection;
}
}
export type {
+ BranchRecord,
DatabaseConnectionRecord,
DatabaseRecord,
DeploymentRecord,
- BranchRecord,
ProjectRecord,
ProviderRecord,
UserRecord,
diff --git a/packages/cli/src/adapters/token-storage.ts b/packages/cli/src/adapters/token-storage.ts
index 5d9e9c4..ba9bff6 100644
--- a/packages/cli/src/adapters/token-storage.ts
+++ b/packages/cli/src/adapters/token-storage.ts
@@ -1,8 +1,9 @@
-import { CredentialsStore } from "@prisma/credentials-store";
-import type { TokenStorage, Tokens } from "@prisma/management-api-sdk";
+// biome-ignore-all lint/performance/noAwaitInLoops: Lock acquisition retries must run sequentially.
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
+import { CredentialsStore } from "@prisma/credentials-store";
+import type { TokenStorage, Tokens } from "@prisma/management-api-sdk";
import { getAuthFilePath } from "../lib/auth/client";
interface StoredCredential {
@@ -11,7 +12,9 @@ interface StoredCredential {
refreshToken?: unknown;
}
-function findLatestValidTokens(allCredentials: StoredCredential[]): Tokens | null {
+function findLatestValidTokens(
+ allCredentials: StoredCredential[],
+): Tokens | null {
for (let i = allCredentials.length - 1; i >= 0; i -= 1) {
const credential = allCredentials[i];
if (!credential) continue;
@@ -63,7 +66,10 @@ export class FileTokenStorage implements TokenStorage {
private readonly credentialsStore: CredentialsStore;
private readonly lockFilePath: string;
- constructor(env: NodeJS.ProcessEnv = process.env, private readonly signal?: AbortSignal) {
+ constructor(
+ env: NodeJS.ProcessEnv = process.env,
+ private readonly signal?: AbortSignal,
+ ) {
const authFilePath = getAuthFilePath(env);
this.credentialsStore = new CredentialsStore(authFilePath);
this.lockFilePath = `${authFilePath}.lock`;
@@ -76,7 +82,7 @@ export class FileTokenStorage implements TokenStorage {
const all = await this.credentialsStore.getCredentials();
this.signal?.throwIfAborted();
return findLatestValidTokens(all as StoredCredential[]);
- } catch (error) {
+ } catch (_error) {
if (this.signal?.aborted) throw this.signal.reason;
return null;
}
@@ -161,10 +167,12 @@ export class FileTokenStorage implements TokenStorage {
private async getStaleRefreshLockId(): Promise {
this.signal?.throwIfAborted();
- const lockId = await fs.readFile(this.lockFilePath, { encoding: "utf8", signal: this.signal }).catch((error) => {
- if (this.signal?.aborted) throw error;
- return null;
- });
+ const lockId = await fs
+ .readFile(this.lockFilePath, { encoding: "utf8", signal: this.signal })
+ .catch((error) => {
+ if (this.signal?.aborted) throw error;
+ return null;
+ });
if (lockId === null) return null;
this.signal?.throwIfAborted();
@@ -176,7 +184,9 @@ export class FileTokenStorage implements TokenStorage {
}
private async releaseRefreshLock(lockId: string): Promise {
- const currentLockId = await fs.readFile(this.lockFilePath, { encoding: "utf8" }).catch(() => null);
+ const currentLockId = await fs
+ .readFile(this.lockFilePath, { encoding: "utf8" })
+ .catch(() => null);
if (currentLockId !== lockId) return;
// unlink does not accept AbortSignal; refresh-lock cleanup must run even after cancellation.
await fs.unlink(this.lockFilePath).catch(() => {});
diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts
index 3c425d4..a1774a6 100644
--- a/packages/cli/src/bin.ts
+++ b/packages/cli/src/bin.ts
@@ -5,9 +5,8 @@ import { runCli } from "./cli";
import { runUpdateDiscoveryWorker } from "./shell/update-check";
if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") {
- runUpdateDiscoveryWorker().then(() => {
- process.exitCode = 0;
- });
+ await runUpdateDiscoveryWorker();
+ process.exitCode = 0;
} else {
const controller = new AbortController();
@@ -20,10 +19,10 @@ if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") {
process.once("SIGINT", abortCli);
process.once("SIGTERM", abortCli);
- runCli({ signal: controller.signal }).then((exitCode) => {
- process.exitCode = exitCode;
- }).finally(() => {
+ try {
+ process.exitCode = await runCli({ signal: controller.signal });
+ } finally {
process.off("SIGINT", abortCli);
process.off("SIGTERM", abortCli);
- });
+ }
}
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 099ff35..9e9b829 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -14,9 +14,18 @@ import { getCliName, getCliVersion } from "./lib/version";
import { attachCommandDescriptor } from "./shell/command-meta";
import { CliError } from "./shell/errors";
import { addCompactGlobalFlags } from "./shell/global-flags";
-import { formatUnexpectedError, writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output";
+import {
+ formatUnexpectedError,
+ writeHumanError,
+ writeJsonError,
+ writeJsonSuccess,
+} from "./shell/output";
import { disposePromptState } from "./shell/prompt";
-import { configureRuntimeCommand, createCommandContext, type CliRuntime } from "./shell/runtime";
+import {
+ type CliRuntime,
+ configureRuntimeCommand,
+ createCommandContext,
+} from "./shell/runtime";
import { createShellUi } from "./shell/ui";
import { maybeWriteCachedUpdateNotification } from "./shell/update-check";
@@ -50,7 +59,9 @@ export async function runCli(options: RunCliOptions = {}): Promise {
return error.code === "commander.helpDisplayed" ? 0 : 2;
}
- runtime.stderr.write(formatUnexpectedError(error, runtime.argv.includes("--trace")));
+ runtime.stderr.write(
+ formatUnexpectedError(error, runtime.argv.includes("--trace")),
+ );
return 1;
} finally {
disposePromptState(runtime.stdin);
@@ -58,15 +69,16 @@ export async function runCli(options: RunCliOptions = {}): Promise {
}
export function createProgram(runtime: CliRuntime): Command {
- const program = attachCommandDescriptor(configureRuntimeCommand(new Command(), runtime), "root");
+ const program = attachCommandDescriptor(
+ configureRuntimeCommand(new Command(), runtime),
+ "root",
+ );
addCompactGlobalFlags(program);
program.addOption(new Option("--version", "Print the CLI version and exit."));
- program
- .name("prisma")
- .showSuggestionAfterError();
+ program.name("prisma").showSuggestionAfterError();
program.addCommand(createVersionCommand(runtime));
program.addCommand(createAuthCommand(runtime));
@@ -85,7 +97,10 @@ async function handleVersionFlag(runtime: CliRuntime): Promise {
try {
if (wantsJson) {
- const context = await createCommandContext(runtime, buildVersionFlagFlags(runtime));
+ const context = await createCommandContext(
+ runtime,
+ buildVersionFlagFlags(runtime),
+ );
const success = await runVersion(context);
writeJsonSuccess(output, {
command: success.command,
@@ -127,7 +142,10 @@ function buildVersionFlagFlags(runtime: CliRuntime) {
};
}
-function resolveBareHelpCommand(program: Command, argv: string[]): Command | null {
+function resolveBareHelpCommand(
+ program: Command,
+ argv: string[],
+): Command | null {
if (argv.length === 0) {
return program;
}
@@ -136,7 +154,8 @@ function resolveBareHelpCommand(program: Command, argv: string[]): Command | nul
return null;
}
- const candidate = program.commands.find((command) => command.name() === argv[0]) ?? null;
+ const candidate =
+ program.commands.find((command) => command.name() === argv[0]) ?? null;
if (!candidate) {
return null;
diff --git a/packages/cli/src/commands/app/index.ts b/packages/cli/src/commands/app/index.ts
index c7052fd..df0d6cf 100644
--- a/packages/cli/src/commands/app/index.ts
+++ b/packages/cli/src/commands/app/index.ts
@@ -14,10 +14,11 @@ import {
runAppPromote,
runAppRemove,
runAppRollback,
- runAppShow,
runAppRun,
+ runAppShow,
runAppShowDeploy,
} from "../../controllers/app";
+import { PREVIEW_BUILD_TYPES } from "../../lib/app/preview-build";
import {
renderAppBuild,
renderAppDeploy,
@@ -30,8 +31,8 @@ import {
renderAppPromote,
renderAppRemove,
renderAppRollback,
- renderAppShow,
renderAppRun,
+ renderAppShow,
renderAppShowDeploy,
serializeAppBuild,
serializeAppDeploy,
@@ -44,16 +45,18 @@ import {
serializeAppPromote,
serializeAppRemove,
serializeAppRollback,
- serializeAppShow,
serializeAppRun,
+ serializeAppShow,
serializeAppShowDeploy,
} from "../../presenters/app";
import { attachCommandDescriptor } from "../../shell/command-meta";
-import { usageError } from "../../shell/errors";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
import { runCommand, runStreamingCommand } from "../../shell/command-runner";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
-import { PREVIEW_BUILD_TYPES } from "../../lib/app/preview-build";
+import { usageError } from "../../shell/errors";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type {
AppBuildResult,
AppDeployResult,
@@ -66,13 +69,16 @@ import type {
AppPromoteResult,
AppRemoveResult,
AppRollbackResult,
- AppShowResult,
AppRunResult,
AppShowDeployResult,
+ AppShowResult,
} from "../../types/app";
export function createAppCommand(runtime: CliRuntime): Command {
- const app = attachCommandDescriptor(configureRuntimeCommand(new Command("app"), runtime), "app");
+ const app = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("app"), runtime),
+ "app",
+ );
addCompactGlobalFlags(app);
@@ -99,7 +105,9 @@ function createBuildCommand(runtime: CliRuntime): Command {
);
command
- .addOption(new Option("--entry ", "Entrypoint path for Bun or auto builds"))
+ .addOption(
+ new Option("--entry ", "Entrypoint path for Bun or auto builds"),
+ )
.addOption(
new Option("--build-type ", "Local build type")
.choices([...PREVIEW_BUILD_TYPES])
@@ -117,7 +125,8 @@ function createBuildCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppBuild(context, entry, buildType),
{
- renderHuman: (context, descriptor, result) => renderAppBuild(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppBuild(context, descriptor, result),
renderJson: (result) => serializeAppBuild(result),
},
);
@@ -133,7 +142,9 @@ function createRunCommand(runtime: CliRuntime): Command {
);
command
- .addOption(new Option("--entry ", "Entrypoint path for Bun or auto runs"))
+ .addOption(
+ new Option("--entry ", "Entrypoint path for Bun or auto runs"),
+ )
.addOption(
new Option("--build-type ", "Local framework type")
.choices(["auto", "bun", "nextjs"])
@@ -153,7 +164,8 @@ function createRunCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppRun(context, entry, buildType, port),
{
- renderHuman: (context, descriptor, result) => renderAppRun(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppRun(context, descriptor, result),
renderJson: (result) => serializeAppRun(result),
},
);
@@ -171,19 +183,40 @@ function createDeployCommand(runtime: CliRuntime): Command {
command
.addOption(new Option("--app ", "App name"))
.addOption(new Option("--project ", "Project id or name"))
- .addOption(new Option("--create-project ", "Create and link a new Project before deploying"))
+ .addOption(
+ new Option(
+ "--create-project ",
+ "Create and link a new Project before deploying",
+ ),
+ )
.addOption(new Option("--branch ", "Branch name"))
.addOption(
- new Option("--framework ", "Framework to deploy")
- .choices(["nextjs", "hono", "tanstack-start", "bun"]),
+ new Option("--framework ", "Framework to deploy").choices([
+ "nextjs",
+ "hono",
+ "tanstack-start",
+ "bun",
+ ]),
)
.addOption(new Option("--entry ", "Entrypoint path for Bun deploys"))
- .addOption(new Option("--http-port ", "HTTP port override for the deployed app"))
.addOption(
- new Option("--env ", "Environment variable assignment or dotenv file")
- .argParser(collectRepeatableValues),
+ new Option(
+ "--http-port ",
+ "HTTP port override for the deployed app",
+ ),
+ )
+ .addOption(
+ new Option(
+ "--env ",
+ "Environment variable assignment or dotenv file",
+ ).argParser(collectRepeatableValues),
+ )
+ .addOption(
+ new Option(
+ "--db",
+ "Create and wire a Prisma Postgres database for this deploy target",
+ ),
)
- .addOption(new Option("--db", "Create and wire a Prisma Postgres database for this deploy target"))
.addOption(new Option("--no-db", "Skip database setup"))
.addOption(new Option("--prod", "Confirm intent to deploy to production"));
addGlobalFlags(command);
@@ -196,10 +229,12 @@ function createDeployCommand(runtime: CliRuntime): Command {
const httpPort = (options as { httpPort?: string }).httpPort;
const envAssignments = (options as { env?: string[] }).env;
const projectRef = (options as { project?: string }).project;
- const createProjectName = (options as { createProject?: string }).createProject;
+ const createProjectName = (options as { createProject?: string })
+ .createProject;
const prod = (options as { prod?: boolean }).prod;
const db = (options as { db?: boolean }).db;
- const hasDbConflict = hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
+ const hasDbConflict =
+ hasFlag(runtime.argv, "--db") && hasFlag(runtime.argv, "--no-db");
await runCommand(
runtime,
@@ -211,10 +246,7 @@ function createDeployCommand(runtime: CliRuntime): Command {
"app deploy accepts either --db or --no-db",
"--db requests database setup, while --no-db disables it.",
"Pass exactly one database setup flag.",
- [
- "prisma-cli app deploy --db",
- "prisma-cli app deploy --no-db",
- ],
+ ["prisma-cli app deploy --db", "prisma-cli app deploy --no-db"],
"app",
);
}
@@ -232,7 +264,8 @@ function createDeployCommand(runtime: CliRuntime): Command {
});
},
{
- renderHuman: (context, descriptor, result) => renderAppDeploy(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppDeploy(context, descriptor, result),
renderJson: (result) => serializeAppDeploy(result),
},
);
@@ -266,7 +299,8 @@ function createShowCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppShow(context, appName, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppShow(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppShow(context, descriptor, result),
renderJson: (result) => serializeAppShow(result),
},
);
@@ -296,7 +330,8 @@ function createOpenCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppOpen(context, appName, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppOpen(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppOpen(context, descriptor, result),
renderJson: (result) => serializeAppOpen(result),
},
);
@@ -348,9 +383,11 @@ function createDomainAddCommand(runtime: CliRuntime): Command {
runtime,
"app.domain.add",
options as Record,
- (context) => runAppDomainAdd(context, hostname, { appName, projectRef, branchName }),
+ (context) =>
+ runAppDomainAdd(context, hostname, { appName, projectRef, branchName }),
{
- renderHuman: (context, descriptor, result) => renderAppDomainAdd(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppDomainAdd(context, descriptor, result),
renderJson: (result) => serializeAppDomainAdd(result),
},
);
@@ -378,9 +415,15 @@ function createDomainShowCommand(runtime: CliRuntime): Command {
runtime,
"app.domain.show",
options as Record,
- (context) => runAppDomainShow(context, hostname, { appName, projectRef, branchName }),
+ (context) =>
+ runAppDomainShow(context, hostname, {
+ appName,
+ projectRef,
+ branchName,
+ }),
{
- renderHuman: (context, descriptor, result) => renderAppDomainShow(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppDomainShow(context, descriptor, result),
renderJson: (result) => serializeAppDomainShow(result),
},
);
@@ -408,9 +451,15 @@ function createDomainRemoveCommand(runtime: CliRuntime): Command {
runtime,
"app.domain.remove",
options as Record,
- (context) => runAppDomainRemove(context, hostname, { appName, projectRef, branchName }),
+ (context) =>
+ runAppDomainRemove(context, hostname, {
+ appName,
+ projectRef,
+ branchName,
+ }),
{
- renderHuman: (context, descriptor, result) => renderAppDomainRemove(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppDomainRemove(context, descriptor, result),
renderJson: (result) => serializeAppDomainRemove(result),
},
);
@@ -438,9 +487,15 @@ function createDomainRetryCommand(runtime: CliRuntime): Command {
runtime,
"app.domain.retry",
options as Record,
- (context) => runAppDomainRetry(context, hostname, { appName, projectRef, branchName }),
+ (context) =>
+ runAppDomainRetry(context, hostname, {
+ appName,
+ projectRef,
+ branchName,
+ }),
{
- renderHuman: (context, descriptor, result) => renderAppDomainRetry(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppDomainRetry(context, descriptor, result),
renderJson: (result) => serializeAppDomainRetry(result),
},
);
@@ -457,7 +512,9 @@ function createDomainWaitCommand(runtime: CliRuntime): Command {
command.argument("", "Custom domain hostname");
addDomainTargetOptions(command);
- command.addOption(new Option("--timeout ", "Maximum time to wait").default("15m"));
+ command.addOption(
+ new Option("--timeout ", "Maximum time to wait").default("15m"),
+ );
addGlobalFlags(command);
command.action(async (hostname: string, options) => {
@@ -470,7 +527,13 @@ function createDomainWaitCommand(runtime: CliRuntime): Command {
runtime,
"app.domain.wait",
options as Record,
- (context) => runAppDomainWait(context, hostname, { appName, projectRef, branchName, timeout }),
+ (context) =>
+ runAppDomainWait(context, hostname, {
+ appName,
+ projectRef,
+ branchName,
+ timeout,
+ }),
);
});
@@ -505,7 +568,10 @@ function createLogsCommand(runtime: CliRuntime): Command {
return command;
}
-function collectRepeatableValues(value: string, previous: string[] | undefined): string[] {
+function collectRepeatableValues(
+ value: string,
+ previous: string[] | undefined,
+): string[] {
return [...(previous ?? []), value];
}
@@ -530,7 +596,8 @@ function createListDeploysCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppListDeploys(context, appName, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppListDeploys(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppListDeploys(context, descriptor, result),
renderJson: (result) => serializeAppListDeploys(result),
},
);
@@ -555,7 +622,8 @@ function createShowDeployCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppShowDeploy(context, deploymentId),
{
- renderHuman: (context, descriptor, result) => renderAppShowDeploy(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppShowDeploy(context, descriptor, result),
renderJson: (result) => serializeAppShowDeploy(result),
},
);
@@ -586,7 +654,8 @@ function createPromoteCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppPromote(context, deploymentId, appName, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppPromote(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppPromote(context, descriptor, result),
renderJson: (result) => serializeAppPromote(result),
},
);
@@ -618,7 +687,8 @@ function createRollbackCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppRollback(context, appName, deploymentId, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppRollback(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppRollback(context, descriptor, result),
renderJson: (result) => serializeAppRollback(result),
},
);
@@ -648,7 +718,8 @@ function createRemoveCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runAppRemove(context, appName, projectRef),
{
- renderHuman: (context, descriptor, result) => renderAppRemove(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderAppRemove(context, descriptor, result),
renderJson: (result) => serializeAppRemove(result),
},
);
diff --git a/packages/cli/src/commands/auth/index.ts b/packages/cli/src/commands/auth/index.ts
index acb7edc..9162b08 100644
--- a/packages/cli/src/commands/auth/index.ts
+++ b/packages/cli/src/commands/auth/index.ts
@@ -1,15 +1,26 @@
import { Command, Option } from "commander";
-import { runAuthLogin, runAuthLogout, runAuthWhoAmI, type AuthLoginCommandOptions } from "../../controllers/auth";
+import {
+ type AuthLoginCommandOptions,
+ runAuthLogin,
+ runAuthLogout,
+ runAuthWhoAmI,
+} from "../../controllers/auth";
import { renderAuthSuccess } from "../../presenters/auth";
import { attachCommandDescriptor } from "../../shell/command-meta";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
import { runCommand } from "../../shell/command-runner";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type { AuthStateResult } from "../../types/auth";
export function createAuthCommand(runtime: CliRuntime): Command {
- const auth = attachCommandDescriptor(configureRuntimeCommand(new Command("auth"), runtime), "auth");
+ const auth = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("auth"), runtime),
+ "auth",
+ );
addCompactGlobalFlags(auth);
@@ -21,7 +32,10 @@ export function createAuthCommand(runtime: CliRuntime): Command {
}
function createAuthLoginCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("login"), runtime), "auth.login");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("login"), runtime),
+ "auth.login",
+ );
command
.addOption(new Option("--provider ").hideHelp())
@@ -47,7 +61,10 @@ function createAuthLoginCommand(runtime: CliRuntime): Command {
}
function createAuthLogoutCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("logout"), runtime), "auth.logout");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("logout"), runtime),
+ "auth.logout",
+ );
addGlobalFlags(command);
@@ -68,7 +85,10 @@ function createAuthLogoutCommand(runtime: CliRuntime): Command {
}
function createAuthWhoAmICommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("whoami"), runtime), "auth.whoami");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("whoami"), runtime),
+ "auth.whoami",
+ );
addGlobalFlags(command);
diff --git a/packages/cli/src/commands/branch/index.ts b/packages/cli/src/commands/branch/index.ts
index 43f1334..fdf9621 100644
--- a/packages/cli/src/commands/branch/index.ts
+++ b/packages/cli/src/commands/branch/index.ts
@@ -3,13 +3,19 @@ import { Command } from "commander";
import { runBranchList } from "../../controllers/branch";
import { renderBranchList, serializeBranchList } from "../../presenters/branch";
import { attachCommandDescriptor } from "../../shell/command-meta";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
import { runCommand } from "../../shell/command-runner";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type { BranchListResult } from "../../types/branch";
export function createBranchCommand(runtime: CliRuntime): Command {
- const branch = attachCommandDescriptor(configureRuntimeCommand(new Command("branch"), runtime), "branch");
+ const branch = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("branch"), runtime),
+ "branch",
+ );
addCompactGlobalFlags(branch);
@@ -19,7 +25,10 @@ export function createBranchCommand(runtime: CliRuntime): Command {
}
function createBranchListCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "branch.list");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("list"), runtime),
+ "branch.list",
+ );
addGlobalFlags(command);
@@ -30,7 +39,8 @@ function createBranchListCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runBranchList(context),
{
- renderHuman: (context, descriptor, result) => renderBranchList(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderBranchList(context, descriptor, result),
renderJson: (result) => serializeBranchList(result),
},
);
diff --git a/packages/cli/src/commands/database/index.ts b/packages/cli/src/commands/database/index.ts
index d83e355..22efb4b 100644
--- a/packages/cli/src/commands/database/index.ts
+++ b/packages/cli/src/commands/database/index.ts
@@ -29,8 +29,11 @@ import {
} from "../../presenters/database";
import { attachCommandDescriptor } from "../../shell/command-meta";
import { runCommand } from "../../shell/command-runner";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type {
DatabaseConnectionCreateResult,
DatabaseConnectionListResult,
@@ -42,7 +45,10 @@ import type {
} from "../../types/database";
export function createDatabaseCommand(runtime: CliRuntime): Command {
- const database = attachCommandDescriptor(configureRuntimeCommand(new Command("database"), runtime), "database");
+ const database = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("database"), runtime),
+ "database",
+ );
addCompactGlobalFlags(database);
@@ -62,7 +68,10 @@ function addProjectAndBranchOptions(command: Command): Command {
}
function createDatabaseListCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "database.list");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("list"), runtime),
+ "database.list",
+ );
addProjectAndBranchOptions(command);
addGlobalFlags(command);
@@ -77,7 +86,8 @@ function createDatabaseListCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runDatabaseList(context, { projectRef, branchName }),
{
- renderHuman: (context, descriptor, result) => renderDatabaseList(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseList(context, descriptor, result),
renderJson: (result) => serializeDatabaseList(result),
},
);
@@ -87,7 +97,10 @@ function createDatabaseListCommand(runtime: CliRuntime): Command {
}
function createDatabaseShowCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "database.show");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("show"), runtime),
+ "database.show",
+ );
command.argument("", "Database id or name");
addProjectAndBranchOptions(command);
@@ -101,9 +114,11 @@ function createDatabaseShowCommand(runtime: CliRuntime): Command {
runtime,
"database.show",
options as Record,
- (context) => runDatabaseShow(context, databaseRef, { projectRef, branchName }),
+ (context) =>
+ runDatabaseShow(context, databaseRef, { projectRef, branchName }),
{
- renderHuman: (context, descriptor, result) => renderDatabaseShow(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseShow(context, descriptor, result),
renderJson: (result) => serializeDatabaseShow(result),
},
);
@@ -113,7 +128,10 @@ function createDatabaseShowCommand(runtime: CliRuntime): Command {
}
function createDatabaseCreateCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("create"), runtime), "database.create");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("create"), runtime),
+ "database.create",
+ );
command
.argument("", "Database name")
@@ -130,10 +148,13 @@ function createDatabaseCreateCommand(runtime: CliRuntime): Command {
runtime,
"database.create",
options as Record,
- (context) => runDatabaseCreate(context, name, { projectRef, branchName, region }),
+ (context) =>
+ runDatabaseCreate(context, name, { projectRef, branchName, region }),
{
- renderStdout: (context, descriptor, result) => renderDatabaseCreateStdout(context, descriptor, result),
- renderHuman: (context, descriptor, result) => renderDatabaseCreate(context, descriptor, result),
+ renderStdout: (context, descriptor, result) =>
+ renderDatabaseCreateStdout(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseCreate(context, descriptor, result),
renderJson: (result) => serializeDatabaseCreate(result),
},
);
@@ -143,11 +164,19 @@ function createDatabaseCreateCommand(runtime: CliRuntime): Command {
}
function createDatabaseRemoveCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("remove"), runtime), "database.remove");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("remove"), runtime),
+ "database.remove",
+ );
command
.argument("", "Database id or name")
- .addOption(new Option("--confirm ", "Exact database id required to remove"));
+ .addOption(
+ new Option(
+ "--confirm ",
+ "Exact database id required to remove",
+ ),
+ );
addProjectAndBranchOptions(command);
addGlobalFlags(command);
@@ -160,9 +189,15 @@ function createDatabaseRemoveCommand(runtime: CliRuntime): Command {
runtime,
"database.remove",
options as Record,
- (context) => runDatabaseRemove(context, databaseRef, { projectRef, branchName, confirm }),
+ (context) =>
+ runDatabaseRemove(context, databaseRef, {
+ projectRef,
+ branchName,
+ confirm,
+ }),
{
- renderHuman: (context, descriptor, result) => renderDatabaseRemove(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseRemove(context, descriptor, result),
renderJson: (result) => serializeDatabaseRemove(result),
},
);
@@ -172,7 +207,10 @@ function createDatabaseRemoveCommand(runtime: CliRuntime): Command {
}
function createDatabaseConnectionCommand(runtime: CliRuntime): Command {
- const connection = attachCommandDescriptor(configureRuntimeCommand(new Command("connection"), runtime), "database.connection");
+ const connection = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("connection"), runtime),
+ "database.connection",
+ );
addCompactGlobalFlags(connection);
@@ -184,7 +222,10 @@ function createDatabaseConnectionCommand(runtime: CliRuntime): Command {
}
function createDatabaseConnectionListCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "database.connection.list");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("list"), runtime),
+ "database.connection.list",
+ );
command.argument("", "Database id or name");
addProjectAndBranchOptions(command);
@@ -198,9 +239,14 @@ function createDatabaseConnectionListCommand(runtime: CliRuntime): Command {
runtime,
"database.connection.list",
options as Record,
- (context) => runDatabaseConnectionList(context, databaseRef, { projectRef, branchName }),
+ (context) =>
+ runDatabaseConnectionList(context, databaseRef, {
+ projectRef,
+ branchName,
+ }),
{
- renderHuman: (context, descriptor, result) => renderDatabaseConnectionList(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseConnectionList(context, descriptor, result),
renderJson: (result) => serializeDatabaseConnectionList(result),
},
);
@@ -210,7 +256,10 @@ function createDatabaseConnectionListCommand(runtime: CliRuntime): Command {
}
function createDatabaseConnectionCreateCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("create"), runtime), "database.connection.create");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("create"), runtime),
+ "database.connection.create",
+ );
command
.argument("", "Database id or name")
@@ -227,10 +276,17 @@ function createDatabaseConnectionCreateCommand(runtime: CliRuntime): Command {
runtime,
"database.connection.create",
options as Record,
- (context) => runDatabaseConnectionCreate(context, databaseRef, { projectRef, branchName, name }),
+ (context) =>
+ runDatabaseConnectionCreate(context, databaseRef, {
+ projectRef,
+ branchName,
+ name,
+ }),
{
- renderStdout: (context, descriptor, result) => renderDatabaseConnectionCreateStdout(context, descriptor, result),
- renderHuman: (context, descriptor, result) => renderDatabaseConnectionCreate(context, descriptor, result),
+ renderStdout: (context, descriptor, result) =>
+ renderDatabaseConnectionCreateStdout(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseConnectionCreate(context, descriptor, result),
renderJson: (result) => serializeDatabaseConnectionCreate(result),
},
);
@@ -240,11 +296,19 @@ function createDatabaseConnectionCreateCommand(runtime: CliRuntime): Command {
}
function createDatabaseConnectionRemoveCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("remove"), runtime), "database.connection.remove");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("remove"), runtime),
+ "database.connection.remove",
+ );
command
.argument("", "Connection id")
- .addOption(new Option("--confirm ", "Exact connection id required to remove"));
+ .addOption(
+ new Option(
+ "--confirm ",
+ "Exact connection id required to remove",
+ ),
+ );
addGlobalFlags(command);
command.action(async (connectionRef: string, options) => {
@@ -254,9 +318,11 @@ function createDatabaseConnectionRemoveCommand(runtime: CliRuntime): Command {
runtime,
"database.connection.remove",
options as Record,
- (context) => runDatabaseConnectionRemove(context, connectionRef, { confirm }),
+ (context) =>
+ runDatabaseConnectionRemove(context, connectionRef, { confirm }),
{
- renderHuman: (context, descriptor, result) => renderDatabaseConnectionRemove(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderDatabaseConnectionRemove(context, descriptor, result),
renderJson: (result) => serializeDatabaseConnectionRemove(result),
},
);
diff --git a/packages/cli/src/commands/env.ts b/packages/cli/src/commands/env.ts
index 950a4b1..de14f37 100644
--- a/packages/cli/src/commands/env.ts
+++ b/packages/cli/src/commands/env.ts
@@ -1,6 +1,11 @@
import { Command, Option } from "commander";
-import { runEnvAdd, runEnvList, runEnvRemove, runEnvUpdate } from "../controllers/app-env";
+import {
+ runEnvAdd,
+ runEnvList,
+ runEnvRemove,
+ runEnvUpdate,
+} from "../controllers/app-env";
import {
renderEnvAdd,
renderEnvList,
@@ -14,7 +19,7 @@ import {
import { attachCommandDescriptor } from "../shell/command-meta";
import { runCommand } from "../shell/command-runner";
import { addGlobalFlags } from "../shell/global-flags";
-import { configureRuntimeCommand, type CliRuntime } from "../shell/runtime";
+import { type CliRuntime, configureRuntimeCommand } from "../shell/runtime";
import type {
EnvAddResult,
EnvListResult,
@@ -44,15 +49,25 @@ function createEnvAddCommand(runtime: CliRuntime): Command {
);
command
- .argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment")
- .addOption(new Option("--file ", "Read KEY=VALUE assignments from a dotenv file"))
+ .argument(
+ "[assignment]",
+ "Variable assignment as KEY=VALUE or KEY from the current environment",
+ )
+ .addOption(
+ new Option(
+ "--file ",
+ "Read KEY=VALUE assignments from a dotenv file",
+ ),
+ )
.addOption(
new Option(
"--role ",
"Project template scope (production or preview)",
).choices(["production", "preview"]),
)
- .addOption(new Option("--branch ", "Preview branch override scope"))
+ .addOption(
+ new Option("--branch ", "Preview branch override scope"),
+ )
.addOption(new Option("--project ", "Project id or name"));
addGlobalFlags(command);
@@ -66,9 +81,16 @@ function createEnvAddCommand(runtime: CliRuntime): Command {
runtime,
"project.env.add",
options as Record,
- (context) => runEnvAdd(context, assignment, { roleName, branchName, projectRef, filePath }),
+ (context) =>
+ runEnvAdd(context, assignment, {
+ roleName,
+ branchName,
+ projectRef,
+ filePath,
+ }),
{
- renderHuman: (context, descriptor, result) => renderEnvAdd(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderEnvAdd(context, descriptor, result),
renderJson: (result) => serializeEnvAdd(result),
},
);
@@ -84,15 +106,25 @@ function createEnvUpdateCommand(runtime: CliRuntime): Command {
);
command
- .argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment")
- .addOption(new Option("--file ", "Read KEY=VALUE assignments from a dotenv file"))
+ .argument(
+ "[assignment]",
+ "Variable assignment as KEY=VALUE or KEY from the current environment",
+ )
+ .addOption(
+ new Option(
+ "--file ",
+ "Read KEY=VALUE assignments from a dotenv file",
+ ),
+ )
.addOption(
new Option(
"--role ",
"Project template scope (production or preview)",
).choices(["production", "preview"]),
)
- .addOption(new Option("--branch ", "Preview branch override scope"))
+ .addOption(
+ new Option("--branch ", "Preview branch override scope"),
+ )
.addOption(new Option("--project ", "Project id or name"));
addGlobalFlags(command);
@@ -106,9 +138,16 @@ function createEnvUpdateCommand(runtime: CliRuntime): Command {
runtime,
"project.env.update",
options as Record,
- (context) => runEnvUpdate(context, assignment, { roleName, branchName, projectRef, filePath }),
+ (context) =>
+ runEnvUpdate(context, assignment, {
+ roleName,
+ branchName,
+ projectRef,
+ filePath,
+ }),
{
- renderHuman: (context, descriptor, result) => renderEnvUpdate(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderEnvUpdate(context, descriptor, result),
renderJson: (result) => serializeEnvUpdate(result),
},
);
@@ -125,12 +164,14 @@ function createEnvListCommand(runtime: CliRuntime): Command {
command
.addOption(
- new Option(
- "--role ",
- "Project template scope",
- ).choices(["production", "preview"]),
+ new Option("--role ", "Project template scope").choices([
+ "production",
+ "preview",
+ ]),
+ )
+ .addOption(
+ new Option("--branch ", "Preview branch resolved scope"),
)
- .addOption(new Option("--branch ", "Preview branch resolved scope"))
.addOption(new Option("--project ", "Project id or name"));
addGlobalFlags(command);
@@ -145,7 +186,8 @@ function createEnvListCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runEnvList(context, { roleName, branchName, projectRef }),
{
- renderHuman: (context, descriptor, result) => renderEnvList(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderEnvList(context, descriptor, result),
renderJson: (result) => serializeEnvList(result),
},
);
@@ -169,7 +211,9 @@ function createEnvRemoveCommand(runtime: CliRuntime): Command {
"Project template scope (production or preview)",
).choices(["production", "preview"]),
)
- .addOption(new Option("--branch ", "Preview branch override scope"))
+ .addOption(
+ new Option("--branch ", "Preview branch override scope"),
+ )
.addOption(new Option("--project ", "Project id or name"));
addGlobalFlags(command);
@@ -182,9 +226,11 @@ function createEnvRemoveCommand(runtime: CliRuntime): Command {
runtime,
"project.env.remove",
options as Record,
- (context) => runEnvRemove(context, key, { roleName, branchName, projectRef }),
+ (context) =>
+ runEnvRemove(context, key, { roleName, branchName, projectRef }),
{
- renderHuman: (context, descriptor, result) => renderEnvRm(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderEnvRm(context, descriptor, result),
renderJson: (result) => serializeEnvRm(result),
},
);
diff --git a/packages/cli/src/commands/git/index.ts b/packages/cli/src/commands/git/index.ts
index 64ea036..8d396e3 100644
--- a/packages/cli/src/commands/git/index.ts
+++ b/packages/cli/src/commands/git/index.ts
@@ -1,15 +1,24 @@
import { Command } from "commander";
import { runGitConnect, runGitDisconnect } from "../../controllers/project";
-import { renderGitConnect, renderGitDisconnect } from "../../presenters/project";
-import { runCommand } from "../../shell/command-runner";
+import {
+ renderGitConnect,
+ renderGitDisconnect,
+} from "../../presenters/project";
import { attachCommandDescriptor } from "../../shell/command-meta";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
+import { runCommand } from "../../shell/command-runner";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type { ProjectRepositoryConnectionResult } from "../../types/project";
export function createGitCommand(runtime: CliRuntime): Command {
- const git = attachCommandDescriptor(configureRuntimeCommand(new Command("git"), runtime), "git");
+ const git = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("git"), runtime),
+ "git",
+ );
addCompactGlobalFlags(git);
@@ -20,7 +29,10 @@ export function createGitCommand(runtime: CliRuntime): Command {
}
function createGitConnectCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("connect"), runtime), "git.connect");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("connect"), runtime),
+ "git.connect",
+ );
command.argument("[git-url]", "GitHub repository URL");
command.option("--project ", "Project id or name");
@@ -31,11 +43,14 @@ function createGitConnectCommand(runtime: CliRuntime): Command {
runtime,
"git.connect",
options as Record,
- (context) => runGitConnect(context, gitUrl, {
- project: typeof options.project === "string" ? options.project : undefined,
- }),
+ (context) =>
+ runGitConnect(context, gitUrl, {
+ project:
+ typeof options.project === "string" ? options.project : undefined,
+ }),
{
- renderHuman: (context, descriptor, result) => renderGitConnect(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderGitConnect(context, descriptor, result),
},
);
});
@@ -44,7 +59,10 @@ function createGitConnectCommand(runtime: CliRuntime): Command {
}
function createGitDisconnectCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("disconnect"), runtime), "git.disconnect");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("disconnect"), runtime),
+ "git.disconnect",
+ );
command.option("--project ", "Project id or name");
addGlobalFlags(command);
@@ -54,11 +72,14 @@ function createGitDisconnectCommand(runtime: CliRuntime): Command {
runtime,
"git.disconnect",
options as Record,
- (context) => runGitDisconnect(context, {
- project: typeof options.project === "string" ? options.project : undefined,
- }),
+ (context) =>
+ runGitDisconnect(context, {
+ project:
+ typeof options.project === "string" ? options.project : undefined,
+ }),
{
- renderHuman: (context, descriptor, result) => renderGitDisconnect(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderGitDisconnect(context, descriptor, result),
},
);
});
diff --git a/packages/cli/src/commands/project/index.ts b/packages/cli/src/commands/project/index.ts
index 98a6064..b0a96a2 100644
--- a/packages/cli/src/commands/project/index.ts
+++ b/packages/cli/src/commands/project/index.ts
@@ -1,23 +1,38 @@
import { Command } from "commander";
-import { runProjectCreate, runProjectLink, runProjectList, runProjectShow } from "../../controllers/project";
import {
- renderProjectSetup,
+ runProjectCreate,
+ runProjectLink,
+ runProjectList,
+ runProjectShow,
+} from "../../controllers/project";
+import {
renderProjectList,
+ renderProjectSetup,
renderProjectShow,
- serializeProjectSetup,
serializeProjectList,
+ serializeProjectSetup,
serializeProjectShow,
} from "../../presenters/project";
import { attachCommandDescriptor } from "../../shell/command-meta";
-import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags";
import { runCommand } from "../../shell/command-runner";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
-import type { ProjectListResult, ProjectSetupResult, ProjectShowResult } from "../../types/project";
+import {
+ addCompactGlobalFlags,
+ addGlobalFlags,
+} from "../../shell/global-flags";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
+import type {
+ ProjectListResult,
+ ProjectSetupResult,
+ ProjectShowResult,
+} from "../../types/project";
import { createEnvCommand } from "../env";
export function createProjectCommand(runtime: CliRuntime): Command {
- const project = attachCommandDescriptor(configureRuntimeCommand(new Command("project"), runtime), "project");
+ const project = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("project"), runtime),
+ "project",
+ );
addCompactGlobalFlags(project);
@@ -31,7 +46,10 @@ export function createProjectCommand(runtime: CliRuntime): Command {
}
function createProjectCreateCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("create"), runtime), "project.create");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("create"), runtime),
+ "project.create",
+ );
command.argument("", "Project name");
addGlobalFlags(command);
@@ -43,7 +61,8 @@ function createProjectCreateCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runProjectCreate(context, String(name)),
{
- renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderProjectSetup(context, descriptor, result),
renderJson: (result) => serializeProjectSetup(result),
},
);
@@ -53,7 +72,10 @@ function createProjectCreateCommand(runtime: CliRuntime): Command {
}
function createProjectLinkCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("link"), runtime), "project.link");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("link"), runtime),
+ "project.link",
+ );
command.argument("[id-or-name]", "Project id or name");
addGlobalFlags(command);
@@ -63,9 +85,14 @@ function createProjectLinkCommand(runtime: CliRuntime): Command {
runtime,
"project.link",
options as Record,
- (context) => runProjectLink(context, typeof projectRef === "string" ? projectRef : undefined),
+ (context) =>
+ runProjectLink(
+ context,
+ typeof projectRef === "string" ? projectRef : undefined,
+ ),
{
- renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderProjectSetup(context, descriptor, result),
renderJson: (result) => serializeProjectSetup(result),
},
);
@@ -75,7 +102,10 @@ function createProjectLinkCommand(runtime: CliRuntime): Command {
}
function createProjectListCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "project.list");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("list"), runtime),
+ "project.list",
+ );
addGlobalFlags(command);
@@ -86,7 +116,8 @@ function createProjectListCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runProjectList(context),
{
- renderHuman: (context, descriptor, result) => renderProjectList(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderProjectList(context, descriptor, result),
renderJson: (result) => serializeProjectList(result),
},
);
@@ -96,7 +127,10 @@ function createProjectListCommand(runtime: CliRuntime): Command {
}
function createProjectShowCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("show"), runtime), "project.show");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("show"), runtime),
+ "project.show",
+ );
command.option("--project ", "Project id or name");
addGlobalFlags(command);
@@ -110,7 +144,8 @@ function createProjectShowCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runProjectShow(context, projectRef),
{
- renderHuman: (context, descriptor, result) => renderProjectShow(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderProjectShow(context, descriptor, result),
renderJson: (result) => serializeProjectShow(result),
},
);
diff --git a/packages/cli/src/commands/version/index.ts b/packages/cli/src/commands/version/index.ts
index c98b715..c19c8e8 100644
--- a/packages/cli/src/commands/version/index.ts
+++ b/packages/cli/src/commands/version/index.ts
@@ -5,11 +5,14 @@ import { renderVersionSuccess } from "../../presenters/version";
import { attachCommandDescriptor } from "../../shell/command-meta";
import { runCommand } from "../../shell/command-runner";
import { addGlobalFlags } from "../../shell/global-flags";
-import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
+import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type { VersionResult } from "../../types/version";
export function createVersionCommand(runtime: CliRuntime): Command {
- const command = attachCommandDescriptor(configureRuntimeCommand(new Command("version"), runtime), "version");
+ const command = attachCommandDescriptor(
+ configureRuntimeCommand(new Command("version"), runtime),
+ "version",
+ );
addGlobalFlags(command);
@@ -20,7 +23,8 @@ export function createVersionCommand(runtime: CliRuntime): Command {
options as Record,
(context) => runVersion(context),
{
- renderHuman: (context, descriptor, result) => renderVersionSuccess(context, descriptor, result),
+ renderHuman: (context, descriptor, result) =>
+ renderVersionSuccess(context, descriptor, result),
},
);
});
diff --git a/packages/cli/src/controllers/app-env-api.ts b/packages/cli/src/controllers/app-env-api.ts
index 0172656..954fe32 100644
--- a/packages/cli/src/controllers/app-env-api.ts
+++ b/packages/cli/src/controllers/app-env-api.ts
@@ -33,16 +33,19 @@ export async function findVariableByNaturalKey(
resolved: ResolvedEnvApiScope,
signal: AbortSignal,
): Promise {
- const { data, error, response } = await client.GET("/v1/environment-variables", {
- params: {
- query: {
- projectId,
- class: resolved.apiTarget.class,
- key,
+ const { data, error, response } = await client.GET(
+ "/v1/environment-variables",
+ {
+ params: {
+ query: {
+ projectId,
+ class: resolved.apiTarget.class,
+ key,
+ },
},
+ signal,
},
- signal,
- });
+ );
if (error || !data) {
throw apiCallError(`Failed to look up ${key}`, response, error);
}
@@ -76,8 +79,10 @@ export function rowMatchesExactScope(
row: RawEnvironmentVariable,
resolved: ResolvedEnvApiScope,
): boolean {
- return row.class === resolved.apiTarget.class &&
- row.branchId === resolved.apiTarget.branchId;
+ return (
+ row.class === resolved.apiTarget.class &&
+ row.branchId === resolved.apiTarget.branchId
+ );
}
export function apiCallError(
@@ -98,8 +103,11 @@ export function apiCallError(
code: apiCode ?? "ENV_API_ERROR",
domain: "app",
summary,
- why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
- fix: apiHint ?? "Re-run with --trace for the underlying API response details.",
+ why:
+ apiMessage ??
+ `The Management API returned status ${status || "unknown"}.`,
+ fix:
+ apiHint ?? "Re-run with --trace for the underlying API response details.",
exitCode: 1,
nextSteps: [],
});
diff --git a/packages/cli/src/controllers/app-env-file.ts b/packages/cli/src/controllers/app-env-file.ts
index 2b4128c..92d6af9 100644
--- a/packages/cli/src/controllers/app-env-file.ts
+++ b/packages/cli/src/controllers/app-env-file.ts
@@ -1,6 +1,8 @@
+// biome-ignore-all lint/performance/noAwaitInLoops: Environment variable mutations and lookups are intentionally sequential.
+// biome-ignore-all lint/style/noNestedTernary: Existing error formatting expression is intentionally compact.
import type { ManagementApiClient } from "@prisma/management-api-sdk";
-import { formatScopeLabel, type EnvScope } from "../lib/app/env-config";
+import { type EnvScope, formatScopeLabel } from "../lib/app/env-config";
import type { EnvFileAssignment } from "../lib/app/env-file";
import { CliError } from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
@@ -88,9 +90,18 @@ export async function runEnvAddFile(
if (error || !data) {
throw apiCallError(`Failed to add ${assignment.key}`, response, error);
}
- variables.push(toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor));
+ variables.push(
+ toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor),
+ );
} catch (error) {
- throw envFileApplyFailedError("add", filePath, resolved.scope, assignment.key, variables, error);
+ throw envFileApplyFailedError(
+ "add",
+ filePath,
+ resolved.scope,
+ assignment.key,
+ variables,
+ error,
+ );
}
}
@@ -164,11 +175,24 @@ export async function runEnvUpdateFile(
},
);
if (error || !data) {
- throw apiCallError(`Failed to update value for ${assignment.key}`, response, error);
+ throw apiCallError(
+ `Failed to update value for ${assignment.key}`,
+ response,
+ error,
+ );
}
- variables.push(toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor));
+ variables.push(
+ toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor),
+ );
} catch (error) {
- throw envFileApplyFailedError("update", filePath, resolved.scope, assignment.key, variables, error);
+ throw envFileApplyFailedError(
+ "update",
+ filePath,
+ resolved.scope,
+ assignment.key,
+ variables,
+ error,
+ );
}
}
@@ -199,7 +223,13 @@ async function findVariablesByNaturalKey(
const found = new Map();
for (const key of keys) {
- const row = await findVariableByNaturalKey(client, projectId, key, resolved, signal);
+ const row = await findVariableByNaturalKey(
+ client,
+ projectId,
+ key,
+ resolved,
+ signal,
+ );
if (row) {
found.set(key, row);
}
@@ -227,7 +257,15 @@ async function missingPreviewDefaultWarnings(
const missing: string[] = [];
for (const key of keys) {
- if (!(await findVariableByNaturalKey(client, projectId, key, previewScope, signal))) {
+ if (
+ !(await findVariableByNaturalKey(
+ client,
+ projectId,
+ key,
+ previewScope,
+ signal,
+ ))
+ ) {
missing.push(key);
}
}
@@ -256,19 +294,21 @@ function envFileApplyFailedError(
error: unknown,
): CliError {
const writtenKeys = writtenVariables.map((variable) => variable.key);
- const cause = error instanceof CliError
- ? error.summary
- : error instanceof Error
- ? error.message
- : "Unknown error.";
+ const cause =
+ error instanceof CliError
+ ? error.summary
+ : error instanceof Error
+ ? error.message
+ : "Unknown error.";
return new CliError({
code: "ENV_FILE_APPLY_FAILED",
domain: "app",
summary: `Failed to ${command} "${failedKey}" from "${filePath}"`,
- why: writtenKeys.length === 0
- ? `No variables were written before ${failedKey} failed. Cause: ${cause}`
- : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
+ why:
+ writtenKeys.length === 0
+ ? `No variables were written before ${failedKey} failed. Cause: ${cause}`
+ : `Written keys before failure: ${formatKeyList(writtenKeys)}. Cause: ${cause}`,
fix: "Inspect the target scope, then retry the remaining keys once the API issue is resolved.",
exitCode: 1,
nextSteps: [
diff --git a/packages/cli/src/controllers/app-env.ts b/packages/cli/src/controllers/app-env.ts
index d26ab35..a101052 100644
--- a/packages/cli/src/controllers/app-env.ts
+++ b/packages/cli/src/controllers/app-env.ts
@@ -1,25 +1,37 @@
+// biome-ignore-all lint/performance/noAwaitInLoops: API pagination loops are intentionally sequential.
import type { ManagementApiClient } from "@prisma/management-api-sdk";
import {
+ type EnvScope,
+ type EnvVarRole,
formatScopeLabel,
parseKeyValuePositional,
resolveEnvScope,
- type EnvScope,
- type EnvVarRole,
} from "../lib/app/env-config";
-import { readEnvFileAssignments, type EnvFileAssignment } from "../lib/app/env-file";
+import {
+ type EnvFileAssignment,
+ readEnvFileAssignments,
+} from "../lib/app/env-file";
import { requireComputeAuth } from "../lib/auth/guard";
import { readLocalGitBranch } from "../lib/git/local-branch";
-import { authRequiredError, CliError, usageError, workspaceRequiredError } from "../shell/errors";
+import {
+ projectResolutionErrorToCliError,
+ resolveProjectTarget,
+} from "../lib/project/resolution";
+import {
+ authRequiredError,
+ CliError,
+ usageError,
+ workspaceRequiredError,
+} from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
import type { CommandContext } from "../shell/runtime";
-import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution";
import type {
EnvAddResult,
- EnvListTarget,
EnvListResult,
- EnvRmResult,
+ EnvListTarget,
EnvResolvedContext,
+ EnvRmResult,
EnvScopeDescriptor,
EnvUpdateResult,
} from "../types/app-env";
@@ -81,7 +93,10 @@ export async function runEnvAdd(
flags: EnvCommandFlags,
): Promise> {
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "add");
- const scope = resolveEnvScope(flags, { requireExplicit: true, command: "add" });
+ const scope = resolveEnvScope(flags, {
+ requireExplicit: true,
+ command: "add",
+ });
if (!scope) {
throw usageError(
`prisma-cli project env add requires --role or --branch`,
@@ -93,17 +108,35 @@ export async function runEnvAdd(
}
const input = await resolveEnvWriteInput(context, source, "add");
- const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env add");
+ const { client, projectId, verboseContext } = await requireClientAndProject(
+ context,
+ flags.projectRef,
+ "project env add",
+ );
const resolved = await resolveScopeToApi(client, projectId, scope, {
createBranchIfMissing: true,
signal: context.runtime.signal,
});
if (input.kind === "file") {
- return runEnvAddFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
+ return runEnvAddFile(
+ context,
+ client,
+ projectId,
+ resolved,
+ input.filePath,
+ input.assignments,
+ verboseContext,
+ );
}
- const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
+ const existing = await findVariableByNaturalKey(
+ client,
+ projectId,
+ input.key,
+ resolved,
+ context.runtime.signal,
+ );
if (existing) {
throw new CliError({
@@ -121,10 +154,16 @@ export async function runEnvAdd(
const warnings =
scope.kind === "branch" &&
- !(await findVariableByNaturalKey(client, projectId, input.key, {
- descriptor: { kind: "role", role: "preview" },
- apiTarget: { class: "preview", branchId: null },
- }, context.runtime.signal))
+ !(await findVariableByNaturalKey(
+ client,
+ projectId,
+ input.key,
+ {
+ descriptor: { kind: "role", role: "preview" },
+ apiTarget: { class: "preview", branchId: null },
+ },
+ context.runtime.signal,
+ ))
? [
`Variable "${input.key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`,
]
@@ -137,8 +176,8 @@ export async function runEnvAdd(
projectId,
class: resolved.apiTarget.class,
...(resolved.apiTarget.branchId !== null
- ? { branchId: resolved.apiTarget.branchId }
- : {}),
+ ? { branchId: resolved.apiTarget.branchId }
+ : {}),
key: input.key,
value: input.value,
},
@@ -155,7 +194,10 @@ export async function runEnvAdd(
projectId,
verboseContext,
scope: resolved.descriptor,
- variable: toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor),
+ variable: toMetadata(
+ data.data as RawEnvironmentVariable,
+ resolved.descriptor,
+ ),
},
warnings,
nextSteps: [],
@@ -168,7 +210,10 @@ export async function runEnvUpdate(
flags: EnvCommandFlags,
): Promise> {
const source = resolveEnvWriteSource(rawAssignment, flags.filePath, "update");
- const scope = resolveEnvScope(flags, { requireExplicit: true, command: "update" });
+ const scope = resolveEnvScope(flags, {
+ requireExplicit: true,
+ command: "update",
+ });
if (!scope) {
throw usageError(
`prisma-cli project env update requires --role or --branch`,
@@ -180,17 +225,35 @@ export async function runEnvUpdate(
}
const input = await resolveEnvWriteInput(context, source, "update");
- const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env update");
+ const { client, projectId, verboseContext } = await requireClientAndProject(
+ context,
+ flags.projectRef,
+ "project env update",
+ );
const resolved = await resolveScopeToApi(client, projectId, scope, {
createBranchIfMissing: false,
signal: context.runtime.signal,
});
if (input.kind === "file") {
- return runEnvUpdateFile(context, client, projectId, resolved, input.filePath, input.assignments, verboseContext);
+ return runEnvUpdateFile(
+ context,
+ client,
+ projectId,
+ resolved,
+ input.filePath,
+ input.assignments,
+ verboseContext,
+ );
}
- const existing = await findVariableByNaturalKey(client, projectId, input.key, resolved, context.runtime.signal);
+ const existing = await findVariableByNaturalKey(
+ client,
+ projectId,
+ input.key,
+ resolved,
+ context.runtime.signal,
+ );
if (!existing) {
throw new CliError({
@@ -215,7 +278,11 @@ export async function runEnvUpdate(
},
);
if (error || !data) {
- throw apiCallError(`Failed to update value for ${input.key}`, response, error);
+ throw apiCallError(
+ `Failed to update value for ${input.key}`,
+ response,
+ error,
+ );
}
return {
@@ -224,7 +291,10 @@ export async function runEnvUpdate(
projectId,
verboseContext,
scope: resolved.descriptor,
- variable: toMetadata(data.data as RawEnvironmentVariable, resolved.descriptor),
+ variable: toMetadata(
+ data.data as RawEnvironmentVariable,
+ resolved.descriptor,
+ ),
},
warnings: [],
nextSteps: [],
@@ -287,13 +357,21 @@ async function resolveEnvWriteInput(
return {
kind: "file",
filePath: source.filePath,
- assignments: await readEnvFileAssignments(context.runtime.cwd, source.filePath, command),
+ assignments: await readEnvFileAssignments(
+ context.runtime.cwd,
+ source.filePath,
+ command,
+ ),
};
}
return {
kind: "single",
- ...parseKeyValuePositional(source.rawAssignment, command, context.runtime.env),
+ ...parseKeyValuePositional(
+ source.rawAssignment,
+ command,
+ context.runtime.env,
+ ),
};
}
@@ -301,20 +379,38 @@ export async function runEnvList(
context: CommandContext,
flags: EnvCommandFlags,
): Promise> {
- const explicit = resolveEnvScope(flags, { requireExplicit: false, command: "list" });
-
- const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env list");
- const resolved = await resolveListScopeToApi(client, projectId, explicit ?? undefined, {
- cwd: context.runtime.cwd,
- signal: context.runtime.signal,
+ const explicit = resolveEnvScope(flags, {
+ requireExplicit: false,
+ command: "list",
});
- const variables = resolved.kind === "scoped"
- ? await listVariables(client, projectId, {
- scope: resolved.addScope,
- descriptor: resolved.descriptor,
- apiTarget: resolved.apiTarget,
- }, context.runtime.signal)
- : await listOverviewVariables(client, projectId, context.runtime.signal);
+
+ const { client, projectId, verboseContext } = await requireClientAndProject(
+ context,
+ flags.projectRef,
+ "project env list",
+ );
+ const resolved = await resolveListScopeToApi(
+ client,
+ projectId,
+ explicit ?? undefined,
+ {
+ cwd: context.runtime.cwd,
+ signal: context.runtime.signal,
+ },
+ );
+ const variables =
+ resolved.kind === "scoped"
+ ? await listVariables(
+ client,
+ projectId,
+ {
+ scope: resolved.addScope,
+ descriptor: resolved.descriptor,
+ apiTarget: resolved.apiTarget,
+ },
+ context.runtime.signal,
+ )
+ : await listOverviewVariables(client, projectId, context.runtime.signal);
return {
command: "project.env.list",
@@ -326,9 +422,12 @@ export async function runEnvList(
variables: variables.map((row) => toMetadata(row, resolved.descriptor)),
},
warnings: [],
- nextSteps: variables.length === 0
- ? [`prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`]
- : [],
+ nextSteps:
+ variables.length === 0
+ ? [
+ `prisma-cli project env add KEY=value ${formatScopeFlag(resolved.addScope)}`,
+ ]
+ : [],
};
}
@@ -347,7 +446,10 @@ export async function runEnvRemove(
);
}
- const scope = resolveEnvScope(flags, { requireExplicit: true, command: "remove" });
+ const scope = resolveEnvScope(flags, {
+ requireExplicit: true,
+ command: "remove",
+ });
if (!scope) {
throw usageError(
"prisma-cli project env remove requires --role or --branch",
@@ -358,12 +460,22 @@ export async function runEnvRemove(
);
}
- const { client, projectId, verboseContext } = await requireClientAndProject(context, flags.projectRef, "project env remove");
+ const { client, projectId, verboseContext } = await requireClientAndProject(
+ context,
+ flags.projectRef,
+ "project env remove",
+ );
const resolved = await resolveScopeToApi(client, projectId, scope, {
createBranchIfMissing: false,
signal: context.runtime.signal,
});
- const existing = await findVariableByNaturalKey(client, projectId, key, resolved, context.runtime.signal);
+ const existing = await findVariableByNaturalKey(
+ client,
+ projectId,
+ key,
+ resolved,
+ context.runtime.signal,
+ );
if (!existing) {
throw new CliError({
code: "ENV_VARIABLE_NOT_FOUND",
@@ -372,9 +484,7 @@ export async function runEnvRemove(
why: "No variable with this key exists in the targeted scope, so there is nothing to remove.",
fix: "Run prisma-cli project env list with the same scope to see the available variables.",
exitCode: 1,
- nextSteps: [
- `prisma-cli project env list ${formatScopeFlag(scope)}`,
- ],
+ nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`],
});
}
@@ -406,21 +516,30 @@ async function requireClientAndProject(
context: CommandContext,
explicitProject: string | undefined,
commandName: string,
-): Promise<{ client: ManagementApiClient; projectId: string; verboseContext: EnvResolvedContext }> {
+): Promise<{
+ client: ManagementApiClient;
+ projectId: string;
+ verboseContext: EnvResolvedContext;
+}> {
const authState = await requireAuthenticatedAuthState(context);
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError(["prisma-cli auth login"]);
}
- if (!authState.workspace) {
+ const workspace = authState.workspace;
+ if (!workspace) {
throw workspaceRequiredError();
}
const targetResult = await resolveProjectTarget({
context,
- workspace: authState.workspace,
+ workspace,
explicitProject,
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace!, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
commandName,
});
if (targetResult.isErr()) {
@@ -432,7 +551,7 @@ async function requireClientAndProject(
client,
projectId: target.project.id,
verboseContext: {
- workspace: authState.workspace,
+ workspace,
project: target.project,
resolution: target.resolution,
},
@@ -454,8 +573,18 @@ async function resolveScopeToApi(
}
const branch = options.createBranchIfMissing
- ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal)
- : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
+ ? await resolveOrCreateBranch(
+ client,
+ projectId,
+ scope.branchName,
+ options.signal,
+ )
+ : await resolveExistingBranch(
+ client,
+ projectId,
+ scope.branchName,
+ options.signal,
+ );
if (branch.role === "production") {
throw new CliError({
@@ -502,7 +631,9 @@ async function resolveListScopeToApi(
const gitBranch = await readLocalGitBranch(options.cwd, options.signal);
if (gitBranch) {
- const branch = (await listBranchesByName(client, projectId, gitBranch, options.signal))[0];
+ const branch = (
+ await listBranchesByName(client, projectId, gitBranch, options.signal)
+ )[0];
if (!branch) {
return {
kind: "scoped",
@@ -615,7 +746,11 @@ async function listBranchesByName(
},
);
if (error || !data) {
- throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
+ throw apiCallError(
+ `Failed to resolve branch "${branchName}"`,
+ response,
+ error,
+ );
}
return data.data as RawBranchRecord[];
@@ -627,7 +762,9 @@ async function resolveExistingBranch(
branchName: string,
signal: AbortSignal,
): Promise {
- const branch = (await listBranchesByName(client, projectId, branchName, signal))[0];
+ const branch = (
+ await listBranchesByName(client, projectId, branchName, signal)
+ )[0];
if (!branch) {
throw new CliError({
code: "ENV_BRANCH_NOT_FOUND",
@@ -636,7 +773,9 @@ async function resolveExistingBranch(
why: "Branch update, list, and remove commands only target existing preview branches.",
fix: "Create the branch by deploying it, or use `project env add --branch` to create its first override.",
exitCode: 1,
- nextSteps: [`prisma-cli project env add KEY=value --branch ${branchName}`],
+ nextSteps: [
+ `prisma-cli project env add KEY=value --branch ${branchName}`,
+ ],
});
}
return branch;
@@ -648,7 +787,9 @@ async function resolveOrCreateBranch(
branchName: string,
signal: AbortSignal,
): Promise {
- const existing = (await listBranchesByName(client, projectId, branchName, signal))[0];
+ const existing = (
+ await listBranchesByName(client, projectId, branchName, signal)
+ )[0];
if (existing) {
return existing;
}
@@ -675,13 +816,19 @@ async function resolveOrCreateBranch(
);
if (error || !data) {
if (response?.status === 409) {
- const raced = (await listBranchesByName(client, projectId, branchName, signal))[0];
+ const raced = (
+ await listBranchesByName(client, projectId, branchName, signal)
+ )[0];
if (raced) {
return raced;
}
}
- throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
+ throw apiCallError(
+ `Failed to create branch "${branchName}"`,
+ response,
+ error,
+ );
}
return data.data as RawBranchRecord;
@@ -701,21 +848,24 @@ async function projectHasDefaultBranch(
query.cursor = cursor;
}
- const result = await client.GET(
- "/v1/projects/{projectId}/branches",
- {
- params: {
- path: { projectId },
- query,
- },
- signal,
+ const result = await client.GET("/v1/projects/{projectId}/branches", {
+ params: {
+ path: { projectId },
+ query,
},
- );
+ signal,
+ });
if (result.error || !result.data) {
- throw apiCallError("Failed to check project default branch", result.response, result.error);
+ throw apiCallError(
+ "Failed to check project default branch",
+ result.response,
+ result.error,
+ );
}
- if ((result.data.data as RawBranchRecord[]).some((branch) => branch.isDefault)) {
+ if (
+ (result.data.data as RawBranchRecord[]).some((branch) => branch.isDefault)
+ ) {
return true;
}
@@ -732,10 +882,15 @@ async function listVariables(
resolved: ResolvedScope,
signal: AbortSignal,
): Promise {
- const collected = await collectEnvironmentVariables(client, projectId, signal, {
- className: resolved.apiTarget.class,
- filter: (row) => rowMatchesScope(row, resolved),
- });
+ const collected = await collectEnvironmentVariables(
+ client,
+ projectId,
+ signal,
+ {
+ className: resolved.apiTarget.class,
+ filter: (row) => rowMatchesScope(row, resolved),
+ },
+ );
return materializeEffectiveRows(collected, resolved);
}
@@ -745,10 +900,16 @@ async function listOverviewVariables(
projectId: string,
signal: AbortSignal,
): Promise {
- const collected = await collectEnvironmentVariables(client, projectId, signal, {
- filter: (row) =>
- row.branchId === null && (row.class === "production" || row.class === "preview"),
- });
+ const collected = await collectEnvironmentVariables(
+ client,
+ projectId,
+ signal,
+ {
+ filter: (row) =>
+ row.branchId === null &&
+ (row.class === "production" || row.class === "preview"),
+ },
+ );
return collected.sort((left, right) => {
const roleOrder = roleSortOrder(left.class) - roleSortOrder(right.class);
@@ -790,7 +951,9 @@ async function collectEnvironmentVariables(
);
}
- const page = (result.data.data as RawEnvironmentVariable[]).filter(options.filter);
+ const page = (result.data.data as RawEnvironmentVariable[]).filter(
+ options.filter,
+ );
collected.push(...page);
if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) {
@@ -841,5 +1004,7 @@ function materializeEffectiveRows(
}
}
- return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
+ return [...byKey.values()].sort((left, right) =>
+ left.key.localeCompare(right.key),
+ );
}
diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts
index 18769ce..9b6f3e4 100644
--- a/packages/cli/src/controllers/app.ts
+++ b/packages/cli/src/controllers/app.ts
@@ -1,93 +1,44 @@
+// biome-ignore-all lint/performance/noAwaitInLoops: Polling and ordered filesystem probes are intentionally sequential.
+// biome-ignore-all lint/performance/useTopLevelRegex: Existing domain and parsing regexes are kept inline for readability.
+// biome-ignore-all lint/style/noNestedTernary: Existing app presentation expressions are intentionally compact.
import { access, readFile } from "node:fs/promises";
import path from "node:path";
-
-import open from "open";
import type { PortMapping, StreamRecord } from "@prisma/compute-sdk";
import type { ManagementApiClient } from "@prisma/management-api-sdk";
-import { Result, matchError } from "better-result";
+import { matchError, Result } from "better-result";
+import open from "open";
import { FileTokenStorage } from "../adapters/token-storage";
-import { authRequiredError, CliError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors";
-import { writeJsonEvent, type CommandSuccess } from "../shell/output";
-import { canPrompt, type CommandContext } from "../shell/runtime";
-import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt";
-import { renderCommandHeader } from "../shell/ui";
-import type {
- AppBuildResult,
- AppDeployResult,
- AppDeploymentSummary,
- AppDomainAddResult,
- AppDomainDnsRecord,
- AppDomainRemoveResult,
- AppDomainRetryResult,
- AppDomainShowResult,
- AppDomainStatus,
- AppDomainSummary,
- AppDomainTarget,
- AppListDeploysResult,
- AppOpenResult,
- AppPromoteResult,
- AppRemoveResult,
- AppResolvedContext,
- AppRollbackResult,
- AppShowResult,
- AppRunResult,
- AppShowDeployResult,
-} from "../types/app";
-import type { AuthWorkspace } from "../types/auth";
-import type { BranchKind } from "../types/branch";
-import type { ProjectResolution, ProjectSummary } from "../types/project";
-import { requireComputeAuth } from "../lib/auth/guard";
-import { readAuthState } from "../lib/auth/auth-ops";
-import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client";
+import {
+ type BranchDatabaseDeployBranch,
+ maybeSetupBranchDatabase,
+} from "../lib/app/branch-database-deploy";
+import {
+ type BunPackageJsonLike,
+ readBunPackageEntrypoint,
+ readBunPackageJson,
+} from "../lib/app/bun-project";
+import {
+ renderDeployOutputRows,
+ renderDeploySettingsPreview,
+} from "../lib/app/deploy-output";
+import { formatDomainFailureFix } from "../lib/app/domain-guidance";
import { envVarNames, parseEnvInputs } from "../lib/app/env-vars";
-import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output";
import {
DEFAULT_LOCAL_DEV_PORT,
resolveLocalBuildType,
runLocalApp,
} from "../lib/app/local-dev";
-import { readBunPackageEntrypoint, readBunPackageJson, type BunPackageJsonLike } from "../lib/app/bun-project";
-import {
- buildProjectSetupNextActions,
- inferTargetName,
- projectNotFoundError,
- projectResolutionErrorToCliError,
- resolveDurablePlatformMapping,
- resolveProjectTarget,
- type InferredTargetName,
- type ProjectCandidate,
- sortProjects,
-} from "../lib/project/resolution";
-import { promptForProjectSetupChoice } from "../lib/project/interactive-setup";
-import {
- bindProjectToDirectory,
- formatCommandArgument,
- projectCreateFailedError,
- projectDirectoryBindingErrorToCliError,
- projectSetupNameRequiredError,
- resolveProjectForSetup,
- toProjectSummary,
-} from "../lib/project/setup";
-import {
- LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
- readLocalResolutionPin,
- type LocalResolutionPinReadError,
- type LocalResolutionPinReadResult,
-} from "../lib/project/local-pin";
-import { readLocalGitBranch } from "../lib/git/local-branch";
import {
executePreviewBuild,
PREVIEW_BUILD_TYPES,
- RESOLVED_PREVIEW_BUILD_TYPES,
- resolveOrCreatePreviewBuildSettings,
type PreviewBuildSettingsBuildType,
type PreviewBuildSettingsResolution,
- type ResolvedPreviewBuildType,
type PreviewBuildType,
+ RESOLVED_PREVIEW_BUILD_TYPES,
+ resolveOrCreatePreviewBuildSettings,
} from "../lib/app/preview-build";
import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction";
-import { maybeSetupBranchDatabase, type BranchDatabaseDeployBranch } from "../lib/app/branch-database-deploy";
import {
createPreviewDeployProgress,
createPreviewDeployProgressState,
@@ -96,12 +47,78 @@ import {
} from "../lib/app/preview-progress";
import {
createPreviewAppProvider,
- PreviewDomainApiError,
type PreviewAppRecord,
+ PreviewDomainApiError,
type PreviewDomainRecord,
} from "../lib/app/preview-provider";
import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate";
-import { formatDomainFailureFix } from "../lib/app/domain-guidance";
+import { readAuthState } from "../lib/auth/auth-ops";
+import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client";
+import { requireComputeAuth } from "../lib/auth/guard";
+import { readLocalGitBranch } from "../lib/git/local-branch";
+import { promptForProjectSetupChoice } from "../lib/project/interactive-setup";
+import {
+ LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
+ type LocalResolutionPinReadError,
+ type LocalResolutionPinReadResult,
+ readLocalResolutionPin,
+} from "../lib/project/local-pin";
+import {
+ buildProjectSetupNextActions,
+ type InferredTargetName,
+ inferTargetName,
+ type ProjectCandidate,
+ projectNotFoundError,
+ projectResolutionErrorToCliError,
+ resolveDurablePlatformMapping,
+ resolveProjectTarget,
+ sortProjects,
+} from "../lib/project/resolution";
+import {
+ bindProjectToDirectory,
+ formatCommandArgument,
+ projectCreateFailedError,
+ projectDirectoryBindingErrorToCliError,
+ projectSetupNameRequiredError,
+ resolveProjectForSetup,
+ toProjectSummary,
+} from "../lib/project/setup";
+import {
+ authRequiredError,
+ CliError,
+ featureUnavailableError,
+ usageError,
+ workspaceRequiredError,
+} from "../shell/errors";
+import { type CommandSuccess, writeJsonEvent } from "../shell/output";
+import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt";
+import { type CommandContext, canPrompt } from "../shell/runtime";
+import { renderCommandHeader } from "../shell/ui";
+import type {
+ AppBuildResult,
+ AppDeploymentSummary,
+ AppDeployResult,
+ AppDomainAddResult,
+ AppDomainDnsRecord,
+ AppDomainRemoveResult,
+ AppDomainRetryResult,
+ AppDomainShowResult,
+ AppDomainStatus,
+ AppDomainSummary,
+ AppDomainTarget,
+ AppListDeploysResult,
+ AppOpenResult,
+ AppPromoteResult,
+ AppRemoveResult,
+ AppResolvedContext,
+ AppRollbackResult,
+ AppRunResult,
+ AppShowDeployResult,
+ AppShowResult,
+} from "../types/app";
+import type { AuthWorkspace } from "../types/auth";
+import type { BranchKind } from "../types/branch";
+import type { ProjectResolution, ProjectSummary } from "../types/project";
import { requireAuthenticatedAuthState } from "./auth";
import { listRealWorkspaceProjects } from "./project";
import { createSelectPromptPort } from "./select-prompt-port";
@@ -109,7 +126,12 @@ import { createSelectPromptPort } from "./select-prompt-port";
type AppDomainCommand = "add" | "show" | "remove" | "retry" | "wait";
type DeployFramework = "nextjs" | "hono" | "tanstack-start" | "bun";
-const DEPLOY_FRAMEWORKS = ["nextjs", "hono", "tanstack-start", "bun"] as const satisfies readonly DeployFramework[];
+const DEPLOY_FRAMEWORKS = [
+ "nextjs",
+ "hono",
+ "tanstack-start",
+ "bun",
+] as const satisfies readonly DeployFramework[];
const TANSTACK_START_PACKAGES = [
"@tanstack/react-start",
"@tanstack/solid-start",
@@ -119,7 +141,10 @@ const PRISMA_PROJECT_ID_ENV_VAR = "PRISMA_PROJECT_ID";
const PRISMA_APP_ID_ENV_VAR = "PRISMA_APP_ID";
function isRealMode(context: CommandContext): boolean {
- return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
+ return (
+ !context.runtime.fixturePath &&
+ !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH
+ );
}
export async function runAppBuild(
@@ -182,7 +207,11 @@ export async function runAppRun(
const buildType = normalizeBuildType(requestedBuildType);
assertSupportedEntrypoint(buildType, entrypoint, "run");
const port = parseLocalPort(requestedPort);
- const resolvedBuildType = await requireLocalBuildType(context, buildType, "run");
+ const resolvedBuildType = await requireLocalBuildType(
+ context,
+ buildType,
+ "run",
+ );
let runResult: Awaited>;
try {
@@ -238,7 +267,10 @@ export async function runAppDeploy(
): Promise> {
ensurePreviewAppMode(context);
- const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
+ const envProjectId = readDeployEnvOverride(
+ context,
+ PRISMA_PROJECT_ID_ENV_VAR,
+ );
const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
assertExclusiveDeployProjectInputs({
projectRef: options?.projectRef,
@@ -246,7 +278,9 @@ export async function runAppDeploy(
envProjectId,
});
- const skipLocalPin = Boolean(envProjectId || options?.projectRef || options?.createProjectName);
+ const skipLocalPin = Boolean(
+ envProjectId || options?.projectRef || options?.createProjectName,
+ );
const localPinReadResult = skipLocalPin
? Result.ok({ kind: "missing" } satisfies LocalResolutionPinReadResult)
: await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
@@ -263,12 +297,13 @@ export async function runAppDeploy(
requestedFramework: options?.framework,
entrypoint: options?.entrypoint,
});
- const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
- branch,
- createProjectName: options?.createProjectName,
- envProjectId,
- localPin,
- });
+ const { provider, target, projectId } =
+ await requireProviderAndDeployProjectContext(context, options?.projectRef, {
+ branch,
+ createProjectName: options?.createProjectName,
+ envProjectId,
+ localPin,
+ });
let localPinResult: { path: string; written: true } | undefined;
if (target.localPinAction) {
const setupResult = await bindProjectToDirectory(
@@ -282,7 +317,12 @@ export async function runAppDeploy(
}
const projectSetup = setupResult.value;
localPinResult = projectSetup.localPin;
- maybeRenderProjectLinked(context, projectSetup.directory, projectSetup.project.name, projectSetup.localPin.path);
+ maybeRenderProjectLinked(
+ context,
+ projectSetup.directory,
+ projectSetup.project.name,
+ projectSetup.localPin.path,
+ );
}
let framework = await resolveDeployFramework(context, {
@@ -297,12 +337,18 @@ export async function runAppDeploy(
}),
);
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await resolveDeployAppSelection(context, projectId, apps, {
- explicitAppName: appName,
- explicitAppId: envAppId,
- firstDeploy: Boolean(target.localPinAction),
- inferName: () => inferTargetName(context.runtime.cwd, context.runtime.signal),
- });
+ const selectedApp = await resolveDeployAppSelection(
+ context,
+ projectId,
+ apps,
+ {
+ explicitAppName: appName,
+ explicitAppId: envAppId,
+ firstDeploy: Boolean(target.localPinAction),
+ inferName: () =>
+ inferTargetName(context.runtime.cwd, context.runtime.signal),
+ },
+ );
await maybeRenderDeploySetupBlock(context, {
includeDirectory: !target.localPinAction,
@@ -322,18 +368,27 @@ export async function runAppDeploy(
framework = customized.framework;
runtime = customized.runtime;
- const productionDeployGate = await enforceProductionDeployGate(context, provider, {
- appId: selectedApp.appId,
- appName: selectedApp.displayName,
- branchKind: target.branch.kind,
- prod: options?.prod === true,
- });
+ const productionDeployGate = await enforceProductionDeployGate(
+ context,
+ provider,
+ {
+ appId: selectedApp.appId,
+ appName: selectedApp.displayName,
+ branchKind: target.branch.kind,
+ prod: options?.prod === true,
+ },
+ );
// Customization can switch from a Bun-compatible framework to one that
// derives its entrypoint from build output, so validate --entry again after it.
const buildType = framework.buildType;
assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
- const entrypoint = await resolveDeployEntrypoint(context.runtime.cwd, framework, options?.entrypoint, context.runtime.signal);
+ const entrypoint = await resolveDeployEntrypoint(
+ context.runtime.cwd,
+ framework,
+ options?.entrypoint,
+ context.runtime.signal,
+ );
const buildSettingsResolution = await resolveOrCreatePreviewBuildSettings({
appPath: context.runtime.cwd,
buildType,
@@ -341,39 +396,56 @@ export async function runAppDeploy(
});
maybeRenderDeployBuildSettings(context, buildSettingsResolution);
const portMapping = parseDeployPortMapping(String(runtime.port));
- const branchDatabaseSetup = await maybeSetupBranchDatabase(context, provider, projectId, toBranchDatabaseDeployBranch(target.branch), {
- db: options?.db,
- providedEnvVars: envVars,
- firstProductionDeploy: productionDeployGate.firstProductionDeploy,
- });
+ const branchDatabaseSetup = await maybeSetupBranchDatabase(
+ context,
+ provider,
+ projectId,
+ toBranchDatabaseDeployBranch(target.branch),
+ {
+ db: options?.db,
+ providedEnvVars: envVars,
+ firstProductionDeploy: productionDeployGate.firstProductionDeploy,
+ },
+ );
const progressState = createPreviewDeployProgressState();
const deployStartedAt = Date.now();
- const deployResult = await provider.deployApp({
- cwd: context.runtime.cwd,
- projectId,
- branchName: target.branch.name,
- appId: selectedApp.appId,
- appName: selectedApp.appName,
- region: selectedApp.region,
- entrypoint,
- buildType,
- buildSettings: buildSettingsResolution.settings,
- portMapping,
- envVars,
- interaction: undefined,
- signal: context.runtime.signal,
- progress: createPreviewDeployProgress(context.output.stderr, context.ui, !context.flags.json && !context.flags.quiet, progressState),
- }).catch((error) => {
- throw appDeployFailedError(error, progressState);
- });
+ const deployResult = await provider
+ .deployApp({
+ cwd: context.runtime.cwd,
+ projectId,
+ branchName: target.branch.name,
+ appId: selectedApp.appId,
+ appName: selectedApp.appName,
+ region: selectedApp.region,
+ entrypoint,
+ buildType,
+ buildSettings: buildSettingsResolution.settings,
+ portMapping,
+ envVars,
+ interaction: undefined,
+ signal: context.runtime.signal,
+ progress: createPreviewDeployProgress(
+ context.output.stderr,
+ context.ui,
+ !context.flags.json && !context.flags.quiet,
+ progressState,
+ ),
+ })
+ .catch((error) => {
+ throw appDeployFailedError(error, progressState);
+ });
const deployDurationMs = Date.now() - deployStartedAt;
await context.stateStore.setSelectedApp(projectId, {
id: deployResult.app.id,
name: deployResult.app.name,
});
- await context.stateStore.setKnownLiveDeployment(projectId, deployResult.app.id, deployResult.deployment.id);
+ await context.stateStore.setKnownLiveDeployment(
+ projectId,
+ deployResult.app.id,
+ deployResult.deployment.id,
+ );
return {
command: "app.deploy",
@@ -416,7 +488,10 @@ export async function runAppDeploy(
localPin: localPinResult,
},
warnings: branchDatabaseSetup.warnings,
- nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`],
+ nextSteps: [
+ "prisma-cli app list-deploys",
+ `prisma-cli app show-deploy ${deployResult.deployment.id}`,
+ ],
};
}
@@ -427,11 +502,17 @@ export async function runAppListDeploys(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app list-deploys",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app list-deploys",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, appName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ );
if (!selectedApp) {
return {
@@ -447,18 +528,29 @@ export async function runAppListDeploys(
};
}
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app deploy"]);
- });
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to list app deployments", error, [
+ "prisma-cli app deploy",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
deploymentsResult.app,
deploymentsResult.deployments,
);
- const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId)
+ const deployments = applyLiveDeploymentHint(
+ deploymentsResult.deployments,
+ currentLiveDeploymentId,
+ )
.slice()
- .sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
+ .sort(
+ (left, right) =>
+ right.createdAt.localeCompare(left.createdAt) ||
+ right.id.localeCompare(left.id),
+ );
await context.stateStore.setSelectedApp(projectId, {
id: deploymentsResult.app.id,
@@ -477,9 +569,10 @@ export async function runAppListDeploys(
deployments,
},
warnings: [],
- nextSteps: deployments.length > 0
- ? [`prisma-cli app show-deploy ${deployments[0]?.id}`]
- : ["prisma-cli app deploy"],
+ nextSteps:
+ deployments.length > 0
+ ? [`prisma-cli app show-deploy ${deployments[0]?.id}`]
+ : ["prisma-cli app deploy"],
};
}
@@ -490,11 +583,17 @@ export async function runAppShow(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app show",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app show",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, appName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ );
if (!selectedApp) {
return {
@@ -512,20 +611,33 @@ export async function runAppShow(
};
}
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to inspect app", error, ["prisma-cli app list-deploys"]);
- });
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to inspect app", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
deploymentsResult.app,
deploymentsResult.deployments,
);
- const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId)
+ const deployments = applyLiveDeploymentHint(
+ deploymentsResult.deployments,
+ currentLiveDeploymentId,
+ )
.slice()
- .sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
+ .sort(
+ (left, right) =>
+ right.createdAt.localeCompare(left.createdAt) ||
+ right.id.localeCompare(left.id),
+ );
const liveDeployment = currentLiveDeploymentId
- ? deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null
+ ? (deployments.find(
+ (deployment) => deployment.id === currentLiveDeploymentId,
+ ) ?? null)
: null;
await context.stateStore.setSelectedApp(projectId, {
@@ -547,7 +659,11 @@ export async function runAppShow(
recentDeployments: deployments.slice(0, 5),
},
warnings: [],
- nextSteps: buildAppShowNextSteps(deploymentsResult.app.liveUrl, liveDeployment, deployments),
+ nextSteps: buildAppShowNextSteps(
+ deploymentsResult.app.liveUrl,
+ liveDeployment,
+ deployments,
+ ),
};
}
@@ -558,9 +674,13 @@ export async function runAppShowDeploy(
ensurePreviewAppMode(context);
const provider = await requirePreviewAppProvider(context);
- const deployment = await provider.showDeployment(deploymentId, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to show deployment", error, ["prisma-cli app list-deploys"]);
- });
+ const deployment = await provider
+ .showDeployment(deploymentId, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to show deployment", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
if (!deployment) {
throw new CliError({
@@ -574,11 +694,19 @@ export async function runAppShowDeploy(
});
}
- const workspaceId = deployment?.app ? await readCurrentWorkspaceId(context) : null;
- const rememberedProject = workspaceId ? await context.stateStore.readRememberedProject(workspaceId) : null;
- const knownLiveDeploymentId = deployment?.app && rememberedProject
- ? await context.stateStore.readKnownLiveDeployment(rememberedProject.id, deployment.app.id)
+ const workspaceId = deployment?.app
+ ? await readCurrentWorkspaceId(context)
: null;
+ const rememberedProject = workspaceId
+ ? await context.stateStore.readRememberedProject(workspaceId)
+ : null;
+ const knownLiveDeploymentId =
+ deployment?.app && rememberedProject
+ ? await context.stateStore.readKnownLiveDeployment(
+ rememberedProject.id,
+ deployment.app.id,
+ )
+ : null;
const providerLiveDeploymentId = deployment.app?.liveDeploymentId ?? null;
return {
@@ -611,11 +739,17 @@ export async function runAppOpen(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app open",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app open",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, appName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ );
if (!selectedApp) {
throw noDeploymentsError(
@@ -624,20 +758,33 @@ export async function runAppOpen(
);
}
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to resolve app URL", error, ["prisma-cli app show"]);
- });
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to resolve app URL", error, [
+ "prisma-cli app show",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
deploymentsResult.app,
deploymentsResult.deployments,
);
- const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId)
+ const deployments = applyLiveDeploymentHint(
+ deploymentsResult.deployments,
+ currentLiveDeploymentId,
+ )
.slice()
- .sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
+ .sort(
+ (left, right) =>
+ right.createdAt.localeCompare(left.createdAt) ||
+ right.id.localeCompare(left.id),
+ );
const liveDeployment = currentLiveDeploymentId
- ? deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null
+ ? (deployments.find(
+ (deployment) => deployment.id === currentLiveDeploymentId,
+ ) ?? null)
: null;
await context.stateStore.setSelectedApp(projectId, {
@@ -683,7 +830,10 @@ export async function runAppOpen(
opened: shouldOpen,
},
warnings: [],
- nextSteps: ["prisma-cli app show", `prisma-cli app show-deploy ${liveDeployment.id}`],
+ nextSteps: [
+ "prisma-cli app show",
+ `prisma-cli app show-deploy ${liveDeployment.id}`,
+ ],
};
}
@@ -697,15 +847,21 @@ export async function runAppDomainAdd(
},
): Promise> {
const normalizedHostname = normalizeDomainHostname(hostname);
- const target = await resolveAppDomainTarget(context, options, `app domain add ${normalizedHostname}`);
+ const target = await resolveAppDomainTarget(
+ context,
+ options,
+ `app domain add ${normalizedHostname}`,
+ );
- const added = await target.provider.addDomain({
- appId: target.app.id,
- hostname: normalizedHostname,
- signal: context.runtime.signal,
- }).catch((error) => {
- throw domainCommandError("add", error, normalizedHostname);
- });
+ const added = await target.provider
+ .addDomain({
+ appId: target.app.id,
+ hostname: normalizedHostname,
+ signal: context.runtime.signal,
+ })
+ .catch((error) => {
+ throw domainCommandError("add", error, normalizedHostname);
+ });
return {
command: "app.domain.add",
@@ -732,11 +888,23 @@ export async function runAppDomainShow(
},
): Promise> {
const normalizedHostname = normalizeDomainHostname(hostname);
- const target = await resolveAppDomainTarget(context, options, `app domain show ${normalizedHostname}`);
- const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "show", context.runtime.signal);
- const detail = await target.provider.showDomain(domain.id, { signal: context.runtime.signal }).catch((error) => {
- throw domainCommandError("show", error, normalizedHostname);
- });
+ const target = await resolveAppDomainTarget(
+ context,
+ options,
+ `app domain show ${normalizedHostname}`,
+ );
+ const domain = await resolveDomainByHostname(
+ target.provider,
+ target.app.id,
+ normalizedHostname,
+ "show",
+ context.runtime.signal,
+ );
+ const detail = await target.provider
+ .showDomain(domain.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw domainCommandError("show", error, normalizedHostname);
+ });
return {
command: "app.domain.show",
@@ -759,14 +927,26 @@ export async function runAppDomainRemove(
},
): Promise> {
const normalizedHostname = normalizeDomainHostname(hostname);
- const target = await resolveAppDomainTarget(context, options, `app domain remove ${normalizedHostname}`);
- const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "remove", context.runtime.signal);
+ const target = await resolveAppDomainTarget(
+ context,
+ options,
+ `app domain remove ${normalizedHostname}`,
+ );
+ const domain = await resolveDomainByHostname(
+ target.provider,
+ target.app.id,
+ normalizedHostname,
+ "remove",
+ context.runtime.signal,
+ );
await confirmDomainRemoval(context, target.resultTarget, normalizedHostname);
- await target.provider.removeDomain(domain.id, { signal: context.runtime.signal }).catch((error) => {
- throw domainCommandError("remove", error, normalizedHostname);
- });
+ await target.provider
+ .removeDomain(domain.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw domainCommandError("remove", error, normalizedHostname);
+ });
return {
command: "app.domain.remove",
@@ -790,11 +970,23 @@ export async function runAppDomainRetry(
},
): Promise> {
const normalizedHostname = normalizeDomainHostname(hostname);
- const target = await resolveAppDomainTarget(context, options, `app domain retry ${normalizedHostname}`);
- const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "retry", context.runtime.signal);
- const retried = await target.provider.retryDomain(domain.id, { signal: context.runtime.signal }).catch((error) => {
- throw domainCommandError("retry", error, normalizedHostname);
- });
+ const target = await resolveAppDomainTarget(
+ context,
+ options,
+ `app domain retry ${normalizedHostname}`,
+ );
+ const domain = await resolveDomainByHostname(
+ target.provider,
+ target.app.id,
+ normalizedHostname,
+ "retry",
+ context.runtime.signal,
+ );
+ const retried = await target.provider
+ .retryDomain(domain.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw domainCommandError("retry", error, normalizedHostname);
+ });
return {
command: "app.domain.retry",
@@ -819,8 +1011,18 @@ export async function runAppDomainWait(
): Promise {
const normalizedHostname = normalizeDomainHostname(hostname);
const timeoutMs = parseDomainWaitTimeout(options?.timeout);
- const target = await resolveAppDomainTarget(context, options, `app domain wait ${normalizedHostname}`);
- const domain = await resolveDomainByHostname(target.provider, target.app.id, normalizedHostname, "wait", context.runtime.signal);
+ const target = await resolveAppDomainTarget(
+ context,
+ options,
+ `app domain wait ${normalizedHostname}`,
+ );
+ const domain = await resolveDomainByHostname(
+ target.provider,
+ target.app.id,
+ normalizedHostname,
+ "wait",
+ context.runtime.signal,
+ );
if (!context.flags.json && !context.flags.quiet) {
context.output.stderr.write(
@@ -852,7 +1054,9 @@ export async function runAppDomainWait(
if (current.status === "active") {
if (!context.flags.json && !context.flags.quiet) {
- context.output.stderr.write(`\n${normalizedHostname} is live at https://${normalizedHostname}\n`);
+ context.output.stderr.write(
+ `\n${normalizedHostname} is live at https://${normalizedHostname}\n`,
+ );
}
return;
}
@@ -863,7 +1067,9 @@ export async function runAppDomainWait(
domain: "app",
summary: `Custom domain "${normalizedHostname}" failed verification`,
why: formatDomainFailureWhy(current),
- fix: formatDomainFailureFix(current) ?? `Run prisma-cli app domain retry ${normalizedHostname}.`,
+ fix:
+ formatDomainFailureFix(current) ??
+ `Run prisma-cli app domain retry ${normalizedHostname}.`,
exitCode: 1,
nextSteps: [
`prisma-cli app domain show ${normalizedHostname}`,
@@ -884,10 +1090,15 @@ export async function runAppDomainWait(
});
}
- await sleep(Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)), context.runtime.signal);
- current = await target.provider.showDomain(current.id, { signal: context.runtime.signal }).catch((error) => {
- throw domainCommandError("wait", error, normalizedHostname);
- });
+ await sleep(
+ Math.min(pollIntervalMs, Math.max(deadline - Date.now(), 0)),
+ context.runtime.signal,
+ );
+ current = await target.provider
+ .showDomain(current.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw domainCommandError("wait", error, normalizedHostname);
+ });
}
}
@@ -899,18 +1110,36 @@ export async function runAppLogs(
): Promise {
ensurePreviewAppMode(context);
- const { provider, target: resolvedTarget, projectId } = await requireProviderAndProjectContext(context, projectRef, {
+ const {
+ provider,
+ target: resolvedTarget,
+ projectId,
+ } = await requireProviderAndProjectContext(context, projectRef, {
commandName: "app logs",
});
const target = deploymentId
- ? await resolveExplicitLogDeployment(context, provider, projectId, resolvedTarget.branch.name, appName, deploymentId)
- : await resolveLiveLogDeployment(context, provider, projectId, resolvedTarget.branch.name, appName);
+ ? await resolveExplicitLogDeployment(
+ context,
+ provider,
+ projectId,
+ resolvedTarget.branch.name,
+ appName,
+ deploymentId,
+ )
+ : await resolveLiveLogDeployment(
+ context,
+ provider,
+ projectId,
+ resolvedTarget.branch.name,
+ appName,
+ );
if (!context.flags.json && !context.flags.quiet) {
const lines = renderCommandHeader(context.ui, {
commandLabel: "app logs",
description: "Streaming logs for the selected deployment.",
- docsPath: "docs/product/command-spec.md#prisma-cli-app-logs---app-name---deployment-id",
+ docsPath:
+ "docs/product/command-spec.md#prisma-cli-app-logs---app-name---deployment-id",
rows: [
{ key: "project", value: projectId },
{ key: "app", value: target.app.name },
@@ -922,16 +1151,18 @@ export async function runAppLogs(
}
}
- await provider.streamDeploymentLogs({
- deploymentId: target.deployment.id,
- signal: context.runtime.signal,
- onRecord: (record) => writeLogRecord(context, record),
- }).catch((error) => {
- throw deployFailedError("Failed to stream app logs", error, [
- `prisma-cli app show-deploy ${target.deployment.id}`,
- "prisma-cli app list-deploys",
- ]);
- });
+ await provider
+ .streamDeploymentLogs({
+ deploymentId: target.deployment.id,
+ signal: context.runtime.signal,
+ onRecord: (record) => writeLogRecord(context, record),
+ })
+ .catch((error) => {
+ throw deployFailedError("Failed to stream app logs", error, [
+ `prisma-cli app show-deploy ${target.deployment.id}`,
+ "prisma-cli app list-deploys",
+ ]);
+ });
}
async function resolveExplicitLogDeployment(
@@ -944,7 +1175,12 @@ async function resolveExplicitLogDeployment(
): Promise<{ app: PreviewAppRecord; deployment: AppDeploymentSummary }> {
if (appName) {
const apps = await listApps(context, provider, projectId, branchName);
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, appName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ );
if (!selectedApp) {
throw noDeploymentsError(
@@ -953,10 +1189,18 @@ async function resolveExplicitLogDeployment(
);
}
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
- });
- const deployment = requireDeploymentForApp(deploymentsResult.deployments, deploymentId, selectedApp.name);
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to list app deployments", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
+ const deployment = requireDeploymentForApp(
+ deploymentsResult.deployments,
+ deploymentId,
+ selectedApp.name,
+ );
await context.stateStore.setSelectedApp(projectId, {
id: deploymentsResult.app.id,
@@ -969,9 +1213,13 @@ async function resolveExplicitLogDeployment(
};
}
- const shown = await provider.showDeployment(deploymentId, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to show deployment", error, ["prisma-cli app list-deploys"]);
- });
+ const shown = await provider
+ .showDeployment(deploymentId, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to show deployment", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
if (!shown) {
throw new CliError({
@@ -1030,7 +1278,12 @@ async function resolveLiveLogDeployment(
appName: string | undefined,
): Promise<{ app: PreviewAppRecord; deployment: AppDeploymentSummary }> {
const apps = await listApps(context, provider, projectId, branchName);
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, appName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ );
if (!selectedApp) {
throw noDeploymentsError(
@@ -1039,18 +1292,27 @@ async function resolveLiveLogDeployment(
);
}
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
- });
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to list app deployments", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
deploymentsResult.app,
deploymentsResult.deployments,
);
- const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId);
+ const deployments = applyLiveDeploymentHint(
+ deploymentsResult.deployments,
+ currentLiveDeploymentId,
+ );
const deployment = currentLiveDeploymentId
- ? deployments.find((candidate) => candidate.id === currentLiveDeploymentId) ?? null
+ ? (deployments.find(
+ (candidate) => candidate.id === currentLiveDeploymentId,
+ ) ?? null)
: null;
await context.stateStore.setSelectedApp(projectId, {
@@ -1098,14 +1360,25 @@ export async function runAppPromote(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app promote",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app promote",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await requireReleaseAppSelection(context, projectId, apps, appName, "promote");
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
- });
+ const selectedApp = await requireReleaseAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ "promote",
+ );
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to list app deployments", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
@@ -1125,20 +1398,28 @@ export async function runAppPromote(
});
if (!targetAlreadyLive) {
- await provider.promoteDeployment({
- appId: selectedApp.id,
- deploymentId: targetDeployment.id,
- signal: context.runtime.signal,
- progress: createPreviewPromoteProgress(
- context.output.stderr,
- !context.flags.json && !context.flags.quiet,
- ),
- }).catch((error) => {
- throw deployFailedError("Failed to promote deployment", error, ["prisma-cli app list-deploys"]);
- });
+ await provider
+ .promoteDeployment({
+ appId: selectedApp.id,
+ deploymentId: targetDeployment.id,
+ signal: context.runtime.signal,
+ progress: createPreviewPromoteProgress(
+ context.output.stderr,
+ !context.flags.json && !context.flags.quiet,
+ ),
+ })
+ .catch((error) => {
+ throw deployFailedError("Failed to promote deployment", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
}
- await context.stateStore.setKnownLiveDeployment(projectId, deploymentsResult.app.id, targetDeployment.id);
+ await context.stateStore.setKnownLiveDeployment(
+ projectId,
+ deploymentsResult.app.id,
+ targetDeployment.id,
+ );
return {
command: "app.promote",
@@ -1155,8 +1436,13 @@ export async function runAppPromote(
live: true,
},
},
- warnings: targetAlreadyLive ? ["The selected deployment is already live for this app."] : [],
- nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${targetDeployment.id}`],
+ warnings: targetAlreadyLive
+ ? ["The selected deployment is already live for this app."]
+ : [],
+ nextSteps: [
+ "prisma-cli app list-deploys",
+ `prisma-cli app show-deploy ${targetDeployment.id}`,
+ ],
};
}
@@ -1168,14 +1454,25 @@ export async function runAppRollback(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app rollback",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app rollback",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await requireReleaseAppSelection(context, projectId, apps, appName, "rollback");
- const deploymentsResult = await provider.listDeployments(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
- });
+ const selectedApp = await requireReleaseAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ "rollback",
+ );
+ const deploymentsResult = await provider
+ .listDeployments(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw deployFailedError("Failed to list app deployments", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(
context,
projectId,
@@ -1183,11 +1480,20 @@ export async function runAppRollback(
deploymentsResult.deployments,
);
const currentLiveDeployment = currentLiveDeploymentId
- ? deploymentsResult.deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null
+ ? (deploymentsResult.deployments.find(
+ (deployment) => deployment.id === currentLiveDeploymentId,
+ ) ?? null)
: null;
const targetDeployment = deploymentId
- ? requireDeploymentForApp(deploymentsResult.deployments, deploymentId, selectedApp.name)
- : resolveRollbackTarget(deploymentsResult.deployments, currentLiveDeploymentId);
+ ? requireDeploymentForApp(
+ deploymentsResult.deployments,
+ deploymentId,
+ selectedApp.name,
+ )
+ : resolveRollbackTarget(
+ deploymentsResult.deployments,
+ currentLiveDeploymentId,
+ );
const targetAlreadyLive = currentLiveDeploymentId === targetDeployment.id;
await context.stateStore.setSelectedApp(projectId, {
@@ -1196,20 +1502,28 @@ export async function runAppRollback(
});
if (!targetAlreadyLive) {
- await provider.promoteDeployment({
- appId: selectedApp.id,
- deploymentId: targetDeployment.id,
- signal: context.runtime.signal,
- progress: createPreviewPromoteProgress(
- context.output.stderr,
- !context.flags.json && !context.flags.quiet,
- ),
- }).catch((error) => {
- throw deployFailedError("Failed to roll back deployment", error, ["prisma-cli app list-deploys"]);
- });
+ await provider
+ .promoteDeployment({
+ appId: selectedApp.id,
+ deploymentId: targetDeployment.id,
+ signal: context.runtime.signal,
+ progress: createPreviewPromoteProgress(
+ context.output.stderr,
+ !context.flags.json && !context.flags.quiet,
+ ),
+ })
+ .catch((error) => {
+ throw deployFailedError("Failed to roll back deployment", error, [
+ "prisma-cli app list-deploys",
+ ]);
+ });
}
- await context.stateStore.setKnownLiveDeployment(projectId, deploymentsResult.app.id, targetDeployment.id);
+ await context.stateStore.setKnownLiveDeployment(
+ projectId,
+ deploymentsResult.app.id,
+ targetDeployment.id,
+ );
return {
command: "app.rollback",
@@ -1227,8 +1541,13 @@ export async function runAppRollback(
},
previousLiveDeploymentId: currentLiveDeployment?.id ?? null,
},
- warnings: targetAlreadyLive ? ["The selected deployment is already live for this app."] : [],
- nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${targetDeployment.id}`],
+ warnings: targetAlreadyLive
+ ? ["The selected deployment is already live for this app."]
+ : [],
+ nextSteps: [
+ "prisma-cli app list-deploys",
+ `prisma-cli app show-deploy ${targetDeployment.id}`,
+ ],
};
}
@@ -1239,19 +1558,35 @@ export async function runAppRemove(
): Promise> {
ensurePreviewAppMode(context);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef, {
- commandName: "app remove",
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, projectRef, {
+ commandName: "app remove",
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await requireReleaseAppSelection(context, projectId, apps, appName, "remove");
+ const selectedApp = await requireReleaseAppSelection(
+ context,
+ projectId,
+ apps,
+ appName,
+ "remove",
+ );
await confirmAppRemoval(context, selectedApp);
- const removedApp = await provider.removeApp(selectedApp.id, { signal: context.runtime.signal }).catch((error) => {
- throw removeFailedError("Failed to remove app", error, ["prisma-cli app show", "prisma-cli app list-deploys"]);
- });
+ const removedApp = await provider
+ .removeApp(selectedApp.id, { signal: context.runtime.signal })
+ .catch((error) => {
+ throw removeFailedError("Failed to remove app", error, [
+ "prisma-cli app show",
+ "prisma-cli app list-deploys",
+ ]);
+ });
- const warnings = await cleanupRemovedAppState(context, projectId, removedApp.id);
+ const warnings = await cleanupRemovedAppState(
+ context,
+ projectId,
+ removedApp.id,
+ );
return {
command: "app.remove",
@@ -1299,19 +1634,28 @@ async function resolveAppDomainTarget(
});
}
- const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
+ const envProjectId = readDeployEnvOverride(
+ context,
+ PRISMA_PROJECT_ID_ENV_VAR,
+ );
const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, options?.projectRef, {
- branch,
- commandName,
- envProjectId,
- });
+ const { provider, target, projectId } =
+ await requireProviderAndProjectContext(context, options?.projectRef, {
+ branch,
+ commandName,
+ envProjectId,
+ });
const apps = await listApps(context, provider, projectId, target.branch.name);
- const selectedApp = await resolveDomainAppSelection(context, projectId, apps, {
- explicitAppName: options?.appName,
- explicitAppId: envAppId,
- });
+ const selectedApp = await resolveDomainAppSelection(
+ context,
+ projectId,
+ apps,
+ {
+ explicitAppName: options?.appName,
+ explicitAppId: envAppId,
+ },
+ );
await context.stateStore.setSelectedApp(projectId, {
id: selectedApp.id,
@@ -1333,7 +1677,9 @@ async function resolveAppDomainTarget(
};
}
-function resolveDomainBranch(explicitBranchName: string | undefined): ResolvedDeployBranch {
+function resolveDomainBranch(
+ explicitBranchName: string | undefined,
+): ResolvedDeployBranch {
return {
name: explicitBranchName?.trim() || "production",
annotation: explicitBranchName ? "set by --branch" : "production default",
@@ -1363,7 +1709,12 @@ async function resolveDomainAppSelection(
return matched;
}
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, options.explicitAppName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ options.explicitAppName,
+ );
if (selectedApp) {
return selectedApp;
}
@@ -1384,10 +1735,14 @@ async function resolveDomainByHostname(
command: AppDomainCommand,
signal: AbortSignal,
): Promise {
- const domains = await provider.listDomains(appId, { signal }).catch((error) => {
- throw domainCommandError(command, error, hostname);
- });
- const matched = domains.find((domain) => sameDomainHostname(domain.hostname, hostname));
+ const domains = await provider
+ .listDomains(appId, { signal })
+ .catch((error) => {
+ throw domainCommandError(command, error, hostname);
+ });
+ const matched = domains.find((domain) =>
+ sameDomainHostname(domain.hostname, hostname),
+ );
if (matched) {
return matched;
}
@@ -1416,7 +1771,12 @@ function isValidDomainHostname(hostname: string): boolean {
if (hostname.length < 1 || hostname.length > 253) {
return false;
}
- if (hostname.includes("://") || hostname.includes("/") || hostname.includes(":") || hostname.startsWith("*.")) {
+ if (
+ hostname.includes("://") ||
+ hostname.includes("/") ||
+ hostname.includes(":") ||
+ hostname.startsWith("*.")
+ ) {
return false;
}
@@ -1425,11 +1785,16 @@ function isValidDomainHostname(hostname: string): boolean {
return false;
}
- return labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
+ return labels.every((label) =>
+ /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label),
+ );
}
function sameDomainHostname(left: string, right: string): boolean {
- return left.trim().replace(/\.$/, "").toLowerCase() === right.trim().replace(/\.$/, "").toLowerCase();
+ return (
+ left.trim().replace(/\.$/, "").toLowerCase() ===
+ right.trim().replace(/\.$/, "").toLowerCase()
+ );
}
function toAppDomainSummary(domain: PreviewDomainRecord): AppDomainSummary {
@@ -1450,7 +1815,9 @@ function toAppDomainSummary(domain: PreviewDomainRecord): AppDomainSummary {
};
}
-function toAppDomainDnsRecords(domain: Pick): AppDomainDnsRecord[] {
+function toAppDomainDnsRecords(
+ domain: Pick,
+): AppDomainDnsRecord[] {
return domain.dnsRecords.map((record) => ({
type: record.type,
name: record.name,
@@ -1482,11 +1849,14 @@ async function confirmDomainRemoval(
throw new CliError({
code: "CONFIRMATION_REQUIRED",
domain: "app",
- summary: "Custom domain removal requires confirmation in the current mode",
+ summary:
+ "Custom domain removal requires confirmation in the current mode",
why: "This command detaches a domain and cannot prompt for confirmation in the current mode.",
fix: `Pass --yes to confirm removal of "${hostname}", or rerun prisma-cli app domain remove in an interactive TTY.`,
exitCode: 1,
- nextSteps: [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`],
+ nextSteps: [
+ `prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`,
+ ],
});
}
@@ -1496,92 +1866,142 @@ async function confirmDomainRemoval(
message: `Detach ${hostname} from App "${target.app.name}"?`,
initialValue: false,
});
-
- if (!confirmed) {
- throw usageError(
- "Custom domain removal canceled",
- "The command was canceled before the domain was detached.",
- "Rerun the command and confirm removal, or pass --yes.",
- [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`],
- "app",
- );
- }
+
+ if (!confirmed) {
+ throw usageError(
+ "Custom domain removal canceled",
+ "The command was canceled before the domain was detached.",
+ "Rerun the command and confirm removal, or pass --yes.",
+ [
+ `prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`,
+ ],
+ "app",
+ );
+ }
+}
+
+function domainCommandError(
+ command: AppDomainCommand,
+ error: unknown,
+ hostname: string,
+): CliError {
+ if (error instanceof PreviewDomainApiError) {
+ return domainApiCommandError(command, error, hostname);
+ }
+
+ return new CliError({
+ code: "DEPLOY_FAILED",
+ domain: "app",
+ summary: `Custom domain ${command} failed`,
+ why: error instanceof Error ? error.message : String(error),
+ fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
+ debug: formatDebugDetails(error),
+ exitCode: 1,
+ nextSteps: [`prisma-cli app domain show ${hostname}`],
+ });
+}
+
+function domainApiCommandError(
+ command: AppDomainCommand,
+ error: PreviewDomainApiError,
+ hostname: string,
+): CliError {
+ if (command === "add") {
+ return domainAddCommandError(error, hostname);
+ }
+
+ if (error.status === 404) {
+ return domainNotFoundError(hostname);
+ }
+
+ if (command === "retry" && error.status === 409) {
+ return domainRetryNotEligibleError(error, hostname);
+ }
+
+ return domainGenericCommandError(command, error, hostname);
+}
+
+function domainAddCommandError(
+ error: PreviewDomainApiError,
+ hostname: string,
+): CliError {
+ if (
+ (error.status === 400 || error.status === 422) &&
+ isDomainDnsError(error)
+ ) {
+ return domainDnsNotConfiguredError(hostname, error);
+ }
+
+ if (error.status === 400) {
+ return new CliError({
+ code: "DOMAIN_HOSTNAME_INVALID",
+ domain: "app",
+ summary: `Invalid custom domain "${hostname}"`,
+ why: error.message,
+ fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
+ debug: formatDebugDetails(error),
+ exitCode: 2,
+ nextSteps: ["prisma-cli app domain add shop.acme.com"],
+ });
+ }
+
+ if (error.status === 429 || isDomainQuotaError(error)) {
+ return new CliError({
+ code: "DOMAIN_QUOTA_EXCEEDED",
+ domain: "app",
+ summary: "Custom domain quota exceeded",
+ why: error.message,
+ fix: "Remove an existing custom domain before adding another one.",
+ debug: formatDebugDetails(error),
+ exitCode: 1,
+ nextSteps: ["prisma-cli app domain remove "],
+ });
+ }
+
+ if (error.status === 409) {
+ return domainAlreadyRegisteredError(hostname, error);
+ }
+
+ if (error.status === 422) {
+ return new CliError({
+ code: "NO_DEPLOYMENTS",
+ domain: "app",
+ summary: "Custom domain requires a live production deployment",
+ why: "The selected production app does not have a promoted version that can receive a custom domain.",
+ fix: "Deploy the app to the production branch, then rerun the domain command.",
+ debug: formatDebugDetails(error),
+ exitCode: 1,
+ nextSteps: [
+ "prisma-cli app deploy --branch production",
+ `prisma-cli app domain add ${hostname}`,
+ ],
+ });
+ }
+
+ return domainGenericCommandError("add", error, hostname);
+}
+
+function domainRetryNotEligibleError(
+ error: PreviewDomainApiError,
+ hostname: string,
+): CliError {
+ return new CliError({
+ code: "DOMAIN_RETRY_NOT_ELIGIBLE",
+ domain: "app",
+ summary: `Custom domain "${hostname}" is not eligible for retry`,
+ why: error.message,
+ fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
+ debug: formatDebugDetails(error),
+ exitCode: 1,
+ nextSteps: [`prisma-cli app domain show ${hostname}`],
+ });
}
-function domainCommandError(
+function domainGenericCommandError(
command: AppDomainCommand,
error: unknown,
hostname: string,
): CliError {
- if (error instanceof PreviewDomainApiError) {
- if (command === "add" && (error.status === 400 || error.status === 422) && isDomainDnsError(error)) {
- return domainDnsNotConfiguredError(hostname, error);
- }
-
- if (command === "add" && error.status === 400) {
- return new CliError({
- code: "DOMAIN_HOSTNAME_INVALID",
- domain: "app",
- summary: `Invalid custom domain "${hostname}"`,
- why: error.message,
- fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
- debug: formatDebugDetails(error),
- exitCode: 2,
- nextSteps: ["prisma-cli app domain add shop.acme.com"],
- });
- }
-
- if (command === "add" && (error.status === 429 || isDomainQuotaError(error))) {
- return new CliError({
- code: "DOMAIN_QUOTA_EXCEEDED",
- domain: "app",
- summary: "Custom domain quota exceeded",
- why: error.message,
- fix: "Remove an existing custom domain before adding another one.",
- debug: formatDebugDetails(error),
- exitCode: 1,
- nextSteps: ["prisma-cli app domain remove "],
- });
- }
-
- if (command === "add" && error.status === 409) {
- return domainAlreadyRegisteredError(hostname, error);
- }
-
- if (command === "add" && error.status === 422) {
- return new CliError({
- code: "NO_DEPLOYMENTS",
- domain: "app",
- summary: "Custom domain requires a live production deployment",
- why: "The selected production app does not have a promoted version that can receive a custom domain.",
- fix: "Deploy the app to the production branch, then rerun the domain command.",
- debug: formatDebugDetails(error),
- exitCode: 1,
- nextSteps: [
- "prisma-cli app deploy --branch production",
- `prisma-cli app domain add ${hostname}`,
- ],
- });
- }
-
- if ((command === "show" || command === "remove" || command === "retry" || command === "wait") && error.status === 404) {
- return domainNotFoundError(hostname);
- }
-
- if (command === "retry" && error.status === 409) {
- return new CliError({
- code: "DOMAIN_RETRY_NOT_ELIGIBLE",
- domain: "app",
- summary: `Custom domain "${hostname}" is not eligible for retry`,
- why: error.message,
- fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
- debug: formatDebugDetails(error),
- exitCode: 1,
- nextSteps: [`prisma-cli app domain show ${hostname}`],
- });
- }
- }
-
return new CliError({
code: "DEPLOY_FAILED",
domain: "app",
@@ -1600,10 +2020,15 @@ function isDomainQuotaError(error: PreviewDomainApiError): boolean {
}
const text = `${error.message} ${error.hint ?? ""}`.toLowerCase();
- return text.includes("quota") || text.includes("maximum") || text.includes("limit");
+ return (
+ text.includes("quota") || text.includes("maximum") || text.includes("limit")
+ );
}
-function domainAlreadyRegisteredError(hostname: string, error: PreviewDomainApiError): CliError {
+function domainAlreadyRegisteredError(
+ hostname: string,
+ error: PreviewDomainApiError,
+): CliError {
return new CliError({
code: "DOMAIN_ALREADY_REGISTERED",
domain: "app",
@@ -1631,7 +2056,10 @@ function isDomainDnsError(error: PreviewDomainApiError): boolean {
);
}
-function domainDnsNotConfiguredError(hostname: string, error: PreviewDomainApiError): CliError {
+function domainDnsNotConfiguredError(
+ hostname: string,
+ error: PreviewDomainApiError,
+): CliError {
const target = extractDomainDnsTarget(error);
const record = target ? `CNAME ${hostname} -> ${target}` : null;
@@ -1646,10 +2074,7 @@ function domainDnsNotConfiguredError(hostname: string, error: PreviewDomainApiEr
debug: formatDebugDetails(error),
exitCode: 1,
nextSteps: record
- ? [
- `add ${record}`,
- `prisma-cli app domain add ${hostname}`,
- ]
+ ? [`add ${record}`, `prisma-cli app domain add ${hostname}`]
: [`prisma-cli app domain add ${hostname} --trace`],
});
}
@@ -1705,7 +2130,14 @@ function parseDomainWaitTimeout(value: string | undefined): number {
const amount = Number.parseInt(match[1], 10);
const unit = match[2];
- const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 60 * 1000 : unit === "s" ? 1000 : 1;
+ const multiplier =
+ unit === "h"
+ ? 60 * 60 * 1000
+ : unit === "m"
+ ? 60 * 1000
+ : unit === "s"
+ ? 1000
+ : 1;
return amount * multiplier;
}
@@ -1756,7 +2188,9 @@ function emitDomainWaitStatus(
const transition = event.previousStatus
? `${event.previousStatus} -> ${event.status}`
: event.status;
- context.output.stderr.write(` ${transition} (${formatElapsed(event.elapsedMs)})\n`);
+ context.output.stderr.write(
+ ` ${transition} (${formatElapsed(event.elapsedMs)})\n`,
+ );
}
function formatElapsed(milliseconds: number): string {
@@ -1805,7 +2239,12 @@ async function resolveDeployAppSelection(
if (options.explicitAppName) {
const matches = findAppsByName(apps, options.explicitAppName);
if (matches.length > 1) {
- return resolveAmbiguousDeployApp(context, matches, options.explicitAppName, options.firstDeploy);
+ return resolveAmbiguousDeployApp(
+ context,
+ matches,
+ options.explicitAppName,
+ options.firstDeploy,
+ );
}
const matched = matches[0];
if (matched) {
@@ -1849,7 +2288,12 @@ async function resolveDeployAppSelection(
const inferredName = await options.inferName();
const matches = findAppsByName(apps, inferredName.name);
if (matches.length > 1) {
- return resolveAmbiguousDeployApp(context, matches, inferredName.name, options.firstDeploy);
+ return resolveAmbiguousDeployApp(
+ context,
+ matches,
+ inferredName.name,
+ options.firstDeploy,
+ );
}
const matched = matches[0];
@@ -1866,9 +2310,10 @@ async function resolveDeployAppSelection(
appName: inferredName.name,
region: PREVIEW_DEFAULT_REGION,
displayName: inferredName.name,
- annotation: inferredName.source === "package-name"
- ? "created from package.json"
- : "created from directory name",
+ annotation:
+ inferredName.source === "package-name"
+ ? "created from package.json"
+ : "created from directory name",
firstDeploy: options.firstDeploy,
};
}
@@ -1889,7 +2334,9 @@ async function resolveAmbiguousDeployApp(
if (canPrompt(context)) {
const createNew = "__create_new_app__";
const cancel = "__cancel__";
- const selected = await selectPrompt({
+ const selected = await selectPrompt<
+ PreviewAppRecord | typeof createNew | typeof cancel
+ >({
input: context.runtime.stdin,
output: context.runtime.stderr,
message: `Multiple apps are named "${targetName}"`,
@@ -1974,7 +2421,9 @@ async function resolveExistingAppSelection(
const savedSelection = await context.stateStore.readSelectedApp(projectId);
if (savedSelection) {
- const matched = apps.find((app) => app.id === savedSelection.id) ?? findAppByName(apps, savedSelection.name);
+ const matched =
+ apps.find((app) => app.id === savedSelection.id) ??
+ findAppByName(apps, savedSelection.name);
if (matched) {
return matched;
}
@@ -2023,7 +2472,12 @@ async function requireReleaseAppSelection(
explicitAppName: string | undefined,
commandName: "promote" | "rollback" | "remove",
): Promise {
- const selectedApp = await resolveExistingAppSelection(context, projectId, apps, explicitAppName);
+ const selectedApp = await resolveExistingAppSelection(
+ context,
+ projectId,
+ apps,
+ explicitAppName,
+ );
if (selectedApp) {
return selectedApp;
}
@@ -2062,7 +2516,8 @@ async function confirmAppRemoval(
output: context.output.stderr,
message: `Type ${app.name} to confirm app removal`,
placeholder: app.name,
- validate: (value) => value === app.name ? undefined : `Type "${app.name}" to confirm removal.`,
+ validate: (value) =>
+ value === app.name ? undefined : `Type "${app.name}" to confirm removal.`,
});
}
@@ -2093,7 +2548,9 @@ function requireDeploymentForApp(
deploymentId: string,
appName: string,
): AppDeploymentSummary {
- const deployment = deployments.find((candidate) => candidate.id === deploymentId);
+ const deployment = deployments.find(
+ (candidate) => candidate.id === deploymentId,
+ );
if (deployment) {
return deployment;
}
@@ -2115,17 +2572,26 @@ async function resolveCurrentLiveDeploymentId(
app: Pick,
deployments: AppDeploymentSummary[],
): Promise {
- if (app.liveDeploymentId && deployments.some((deployment) => deployment.id === app.liveDeploymentId)) {
+ if (
+ app.liveDeploymentId &&
+ deployments.some((deployment) => deployment.id === app.liveDeploymentId)
+ ) {
return app.liveDeploymentId;
}
- const providerLiveDeployment = deployments.find((deployment) => deployment.live === true);
+ const providerLiveDeployment = deployments.find(
+ (deployment) => deployment.live === true,
+ );
if (providerLiveDeployment) {
return providerLiveDeployment.id;
}
- const knownLiveDeploymentId = await context.stateStore.readKnownLiveDeployment(projectId, app.id);
- if (knownLiveDeploymentId && deployments.some((deployment) => deployment.id === knownLiveDeploymentId)) {
+ const knownLiveDeploymentId =
+ await context.stateStore.readKnownLiveDeployment(projectId, app.id);
+ if (
+ knownLiveDeploymentId &&
+ deployments.some((deployment) => deployment.id === knownLiveDeploymentId)
+ ) {
return knownLiveDeploymentId;
}
@@ -2175,7 +2641,9 @@ function resolveRollbackTarget(
deployments: AppDeploymentSummary[],
currentLiveDeploymentId: string | null,
): AppDeploymentSummary {
- const previousDeployment = deployments.find((deployment) => deployment.id !== currentLiveDeploymentId);
+ const previousDeployment = deployments.find(
+ (deployment) => deployment.id !== currentLiveDeploymentId,
+ );
if (previousDeployment) {
return previousDeployment;
}
@@ -2197,21 +2665,29 @@ async function listApps(
projectId: string,
branchName?: string,
) {
- return provider.listApps(projectId, { branchName, signal: context.runtime.signal }).then(sortApps).catch((error) => {
- if (isMissingProjectError(error)) {
- throw new CliError({
- code: "PROJECT_NOT_FOUND",
- domain: "project",
- summary: "Project not found",
- why: `The resolved project "${projectId}" does not exist in the authenticated workspace or is no longer accessible.`,
- fix: "Pass --project , or run prisma-cli project show to inspect this directory's binding.",
- exitCode: 1,
- nextSteps: ["prisma-cli project show", "prisma-cli project link "],
- });
- }
+ return provider
+ .listApps(projectId, { branchName, signal: context.runtime.signal })
+ .then(sortApps)
+ .catch((error) => {
+ if (isMissingProjectError(error)) {
+ throw new CliError({
+ code: "PROJECT_NOT_FOUND",
+ domain: "project",
+ summary: "Project not found",
+ why: `The resolved project "${projectId}" does not exist in the authenticated workspace or is no longer accessible.`,
+ fix: "Pass --project , or run prisma-cli project show to inspect this directory's binding.",
+ exitCode: 1,
+ nextSteps: [
+ "prisma-cli project show",
+ "prisma-cli project link ",
+ ],
+ });
+ }
- throw deployFailedError("Failed to list apps", error, ["prisma-cli project show"]);
- });
+ throw deployFailedError("Failed to list apps", error, [
+ "prisma-cli project show",
+ ]);
+ });
}
async function requirePreviewAppProvider(context: CommandContext) {
@@ -2221,19 +2697,31 @@ async function requirePreviewAppProvider(context: CommandContext) {
async function requirePreviewAppProviderWithClient(
context: CommandContext,
-): Promise<{ client: ManagementApiClient; provider: ReturnType }> {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+): Promise<{
+ client: ManagementApiClient;
+ provider: ReturnType;
+}> {
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError(["prisma-cli auth login"]);
}
return {
client,
- provider: createPreviewAppProvider(client, createPreviewLogAuthOptions(context.runtime.env, context.runtime.signal)),
+ provider: createPreviewAppProvider(
+ client,
+ createPreviewLogAuthOptions(context.runtime.env, context.runtime.signal),
+ ),
};
}
-function createPreviewLogAuthOptions(env: NodeJS.ProcessEnv, signal: AbortSignal) {
+function createPreviewLogAuthOptions(
+ env: NodeJS.ProcessEnv,
+ signal: AbortSignal,
+) {
const rawToken = env[SERVICE_TOKEN_ENV_VAR]?.trim();
if (rawToken) {
return {
@@ -2248,7 +2736,9 @@ function createPreviewLogAuthOptions(env: NodeJS.ProcessEnv, signal: AbortSignal
getToken: async () => {
const tokens = await tokenStorage.getTokens();
if (!tokens) {
- throw new Error("Authentication token is no longer available. Run prisma-cli auth login and try again.");
+ throw new Error(
+ "Authentication token is no longer available. Run prisma-cli auth login and try again.",
+ );
}
return tokens.accessToken;
},
@@ -2281,8 +2771,14 @@ async function requireProviderAndProjectContext(
target: ResolvedAppProjectContext;
projectId: string;
}> {
- const { client, provider } = await requirePreviewAppProviderWithClient(context);
- const target = await resolveProjectContext(context, client, explicitProject, options);
+ const { client, provider } =
+ await requirePreviewAppProviderWithClient(context);
+ const target = await resolveProjectContext(
+ context,
+ client,
+ explicitProject,
+ options,
+ );
return {
client,
provider,
@@ -2306,8 +2802,15 @@ async function requireProviderAndDeployProjectContext(
target: ResolvedAppProjectContext;
projectId: string;
}> {
- const { client, provider } = await requirePreviewAppProviderWithClient(context);
- const target = await resolveDeployProjectContext(context, client, provider, explicitProject, options);
+ const { client, provider } =
+ await requirePreviewAppProviderWithClient(context);
+ const target = await resolveDeployProjectContext(
+ context,
+ client,
+ provider,
+ explicitProject,
+ options,
+ );
return {
client,
provider,
@@ -2327,23 +2830,26 @@ async function resolveProjectContext(
},
): Promise {
const authState = await requireAuthenticatedAuthState(context);
- if (!authState.workspace) {
+ const workspace = authState.workspace;
+ if (!workspace) {
throw workspaceRequiredError();
}
const resolvedResult = await resolveProjectTarget({
context,
- workspace: authState.workspace,
+ workspace,
explicitProject,
envProjectId: options?.envProjectId,
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace!, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
commandName: options?.commandName,
});
if (resolvedResult.isErr()) {
throw projectResolutionErrorToCliError(resolvedResult.error);
}
const resolved = resolvedResult.value;
- const branch = options?.branch ?? await resolveDeployBranch(context, undefined);
+ const branch =
+ options?.branch ?? (await resolveDeployBranch(context, undefined));
return {
...resolved,
@@ -2373,12 +2879,89 @@ async function resolveDeployProjectContext(
throw workspaceRequiredError();
}
- const branch = options.branch ?? await resolveDeployBranch(context, undefined);
- const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
+ const branch =
+ options.branch ?? (await resolveDeployBranch(context, undefined));
+ const projects = await listRealWorkspaceProjects(
+ client,
+ workspace,
+ context.runtime.signal,
+ );
+
+ const resolved = await resolveDeployProjectSetup(
+ context,
+ provider,
+ workspace,
+ projects,
+ explicitProject,
+ options,
+ );
+ return withRemoteDeployBranch(
+ provider,
+ resolved,
+ branch,
+ context.runtime.signal,
+ );
+}
+
+async function resolveDeployProjectSetup(
+ context: CommandContext,
+ provider: ReturnType,
+ workspace: AuthWorkspace,
+ projects: ProjectCandidate[],
+ explicitProject: string | undefined,
+ options: {
+ createProjectName?: string;
+ envProjectId?: string;
+ localPin: LocalResolutionPinReadResult;
+ },
+): Promise> {
+ const selected = await resolveNonInteractiveDeployProjectSetup(
+ context,
+ provider,
+ workspace,
+ projects,
+ explicitProject,
+ options,
+ );
+ if (selected) {
+ return selected;
+ }
+
+ if (canPrompt(context) && !context.flags.yes) {
+ return resolveInteractiveDeployProjectSetup(
+ context,
+ provider,
+ workspace,
+ projects,
+ );
+ }
+
+ const suggestedName = await inferTargetName(
+ context.runtime.cwd,
+ context.runtime.signal,
+ );
+ throw projectSetupRequiredError(projects, suggestedName);
+}
+async function resolveNonInteractiveDeployProjectSetup(
+ context: CommandContext,
+ provider: ReturnType,
+ workspace: AuthWorkspace,
+ projects: ProjectCandidate[],
+ explicitProject: string | undefined,
+ options: {
+ createProjectName?: string;
+ envProjectId?: string;
+ localPin: LocalResolutionPinReadResult;
+ },
+): Promise | null> {
if (explicitProject) {
- const project = resolveProjectForSetup(explicitProject, projects, workspace);
- return withRemoteDeployBranch(provider, {
+ const project = resolveProjectForSetup(
+ explicitProject,
+ projects,
+ workspace,
+ );
+ return {
workspace,
project: toProjectSummary(project),
resolution: {
@@ -2387,7 +2970,7 @@ async function resolveDeployProjectContext(
targetNameSource: "explicit",
},
localPinAction: "linked",
- }, branch, context.runtime.signal);
+ };
}
if (options.createProjectName) {
@@ -2396,8 +2979,13 @@ async function resolveDeployProjectContext(
throw projectSetupNameRequiredError("app deploy --create-project");
}
- const created = await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal);
- return withRemoteDeployBranch(provider, {
+ const created = await createProjectForDeploySetup(
+ provider,
+ projectName,
+ workspace,
+ context.runtime.signal,
+ );
+ return {
workspace,
project: toProjectSummary(created),
resolution: {
@@ -2406,15 +2994,17 @@ async function resolveDeployProjectContext(
targetNameSource: "explicit",
},
localPinAction: "created",
- }, branch, context.runtime.signal);
+ };
}
if (options.envProjectId) {
- const project = projects.find((candidate) => candidate.id === options.envProjectId);
+ const project = projects.find(
+ (candidate) => candidate.id === options.envProjectId,
+ );
if (!project) {
throw projectNotFoundError(options.envProjectId, workspace);
}
- return withRemoteDeployBranch(provider, {
+ return {
workspace,
project: toProjectSummary(project),
resolution: {
@@ -2422,7 +3012,7 @@ async function resolveDeployProjectContext(
targetName: options.envProjectId,
targetNameSource: "env",
},
- }, branch, context.runtime.signal);
+ };
}
const localPin = options.localPin;
@@ -2431,12 +3021,14 @@ async function resolveDeployProjectContext(
throw localResolutionPinStaleError();
}
- const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
+ const project = projects.find(
+ (candidate) => candidate.id === localPin.pin.projectId,
+ );
if (!project) {
throw localResolutionPinStaleError();
}
- return withRemoteDeployBranch(provider, {
+ return {
workspace,
project: toProjectSummary(project),
resolution: {
@@ -2444,12 +3036,12 @@ async function resolveDeployProjectContext(
targetName: project.name,
targetNameSource: "local-pin",
},
- }, branch, context.runtime.signal);
+ };
}
const platformMapping = await resolveDurablePlatformMapping();
if (platformMapping && platformMapping.workspace.id === workspace.id) {
- return withRemoteDeployBranch(provider, {
+ return {
workspace,
project: toProjectSummary(platformMapping),
resolution: {
@@ -2457,16 +3049,10 @@ async function resolveDeployProjectContext(
targetName: platformMapping.name,
targetNameSource: "platform-mapping",
},
- }, branch, context.runtime.signal);
- }
-
- if (canPrompt(context) && !context.flags.yes) {
- const resolved = await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects);
- return withRemoteDeployBranch(provider, resolved, branch, context.runtime.signal);
+ };
}
- const suggestedName = await inferTargetName(context.runtime.cwd, context.runtime.signal);
- throw projectSetupRequiredError(projects, suggestedName);
+ return null;
}
async function resolveInteractiveDeployProjectSetup(
@@ -2478,11 +3064,20 @@ async function resolveInteractiveDeployProjectSetup(
const setup = await promptForProjectSetupChoice({
context,
projects,
- createProject: (projectName) => createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal),
+ createProject: (projectName) =>
+ createProjectForDeploySetup(
+ provider,
+ projectName,
+ workspace,
+ context.runtime.signal,
+ ),
cancel: {
why: "Deploy needs a Project before it can continue.",
fix: "Choose an existing Project or create a new one, then rerun deploy.",
- nextSteps: ["prisma-cli app deploy --project ", "prisma-cli app deploy --create-project "],
+ nextSteps: [
+ "prisma-cli app deploy --project ",
+ "prisma-cli app deploy --create-project ",
+ ],
},
});
@@ -2504,17 +3099,21 @@ async function createProjectForDeploySetup(
workspace: AuthWorkspace,
signal: AbortSignal,
): Promise {
- const created = await provider.createProject({ name: projectName, signal }).catch((error) => {
- throw projectCreateFailedError(error, projectName, workspace, {
- nextSteps: [
- "prisma-cli project list",
- "prisma-cli app deploy --project ",
- `prisma-cli app deploy --create-project ${formatCommandArgument(projectName)}`,
- ],
- permissionFix: "Choose an existing Project with --project, or grant the token permission to create Projects in this workspace.",
- fallbackFix: "Choose an existing Project with --project, or retry after addressing the platform error above.",
+ const created = await provider
+ .createProject({ name: projectName, signal })
+ .catch((error) => {
+ throw projectCreateFailedError(error, projectName, workspace, {
+ nextSteps: [
+ "prisma-cli project list",
+ "prisma-cli app deploy --project ",
+ `prisma-cli app deploy --create-project ${formatCommandArgument(projectName)}`,
+ ],
+ permissionFix:
+ "Choose an existing Project with --project, or grant the token permission to create Projects in this workspace.",
+ fallbackFix:
+ "Choose an existing Project with --project, or retry after addressing the platform error above.",
+ });
});
- });
return {
id: created.id,
@@ -2548,7 +3147,9 @@ function toBranchKind(name: string): BranchKind {
return name === "production" || name === "main" ? "production" : "preview";
}
-function toResultBranch(branch: ResolvedAppProjectContext["branch"]): AppDeployResult["branch"] {
+function toResultBranch(
+ branch: ResolvedAppProjectContext["branch"],
+): AppDeployResult["branch"] {
return {
id: branch.id,
name: branch.name,
@@ -2556,7 +3157,9 @@ function toResultBranch(branch: ResolvedAppProjectContext["branch"]): AppDeployR
};
}
-function toAppVerboseContext(target: ResolvedAppProjectContext): AppResolvedContext {
+function toAppVerboseContext(
+ target: ResolvedAppProjectContext,
+): AppResolvedContext {
return {
workspace: target.workspace,
project: target.project,
@@ -2565,9 +3168,13 @@ function toAppVerboseContext(target: ResolvedAppProjectContext): AppResolvedCont
};
}
-function toBranchDatabaseDeployBranch(branch: ResolvedAppProjectContext["branch"]): BranchDatabaseDeployBranch {
+function toBranchDatabaseDeployBranch(
+ branch: ResolvedAppProjectContext["branch"],
+): BranchDatabaseDeployBranch {
if (!branch.id) {
- throw new Error(`Deploy branch "${branch.name}" was not resolved remotely.`);
+ throw new Error(
+ `Deploy branch "${branch.name}" was not resolved remotely.`,
+ );
}
return {
@@ -2610,7 +3217,10 @@ interface ResolvedDeployBranch {
annotation: string;
}
-async function resolveDeployBranch(context: CommandContext, explicitBranchName: string | undefined): Promise {
+async function resolveDeployBranch(
+ context: CommandContext,
+ explicitBranchName: string | undefined,
+): Promise {
if (explicitBranchName) {
return {
name: explicitBranchName,
@@ -2618,7 +3228,10 @@ async function resolveDeployBranch(context: CommandContext, explicitBranchName:
};
}
- const gitBranch = await readLocalGitBranch(context.runtime.cwd, context.runtime.signal);
+ const gitBranch = await readLocalGitBranch(
+ context.runtime.cwd,
+ context.runtime.signal,
+ );
if (gitBranch) {
return {
name: gitBranch,
@@ -2652,7 +3265,10 @@ async function resolveDeployFramework(
},
): Promise {
if (options.requestedFramework) {
- return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
+ return frameworkFromUserFacingValue(
+ options.requestedFramework,
+ "set by --framework",
+ );
}
if (options.entrypoint) {
@@ -2664,7 +3280,10 @@ async function resolveDeployFramework(
};
}
- const detected = await detectDeployFramework(context.runtime.cwd, context.runtime.signal);
+ const detected = await detectDeployFramework(
+ context.runtime.cwd,
+ context.runtime.signal,
+ );
if (detected) {
return detected;
}
@@ -2697,7 +3316,10 @@ function assertSupportedEntrypointForRequestedDeployShape(options: {
return;
}
- const framework = frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
+ const framework = frameworkFromUserFacingValue(
+ options.requestedFramework,
+ "set by --framework",
+ );
assertSupportedEntrypoint(framework.buildType, options.entrypoint, "deploy");
}
@@ -2737,7 +3359,10 @@ async function resolveDeployEntrypoint(
}
}
-async function detectDeployFramework(cwd: string, signal: AbortSignal): Promise {
+async function detectDeployFramework(
+ cwd: string,
+ signal: AbortSignal,
+): Promise {
const packageJson = await readBunPackageJson(cwd, signal);
const nextConfig = await detectNextConfig(cwd, signal);
@@ -2775,7 +3400,10 @@ async function detectDeployFramework(cwd: string, signal: AbortSignal): Promise<
return null;
}
-async function detectNextConfig(cwd: string, signal: AbortSignal): Promise<{ exists: boolean; standalone: boolean }> {
+async function detectNextConfig(
+ cwd: string,
+ signal: AbortSignal,
+): Promise<{ exists: boolean; standalone: boolean }> {
const candidates = [
"next.config.js",
"next.config.mjs",
@@ -2806,24 +3434,37 @@ async function detectNextConfig(cwd: string, signal: AbortSignal): Promise<{ exi
};
}
-function hasPackageDependency(packageJson: BunPackageJsonLike | null, dependencyName: string): boolean {
- return hasDependency(packageJson?.dependencies, dependencyName)
- || hasDependency(packageJson?.devDependencies, dependencyName);
+function hasPackageDependency(
+ packageJson: BunPackageJsonLike | null,
+ dependencyName: string,
+): boolean {
+ return (
+ hasDependency(packageJson?.dependencies, dependencyName) ||
+ hasDependency(packageJson?.devDependencies, dependencyName)
+ );
}
-function hasAnyPackageDependency(packageJson: BunPackageJsonLike | null, dependencyNames: readonly string[]): boolean {
- return dependencyNames.some((dependencyName) => hasPackageDependency(packageJson, dependencyName));
+function hasAnyPackageDependency(
+ packageJson: BunPackageJsonLike | null,
+ dependencyNames: readonly string[],
+): boolean {
+ return dependencyNames.some((dependencyName) =>
+ hasPackageDependency(packageJson, dependencyName),
+ );
}
function hasDependency(dependencies: unknown, dependencyName: string): boolean {
return Boolean(
- dependencies
- && typeof dependencies === "object"
- && dependencyName in dependencies,
+ dependencies &&
+ typeof dependencies === "object" &&
+ dependencyName in dependencies,
);
}
-function frameworkFromUserFacingValue(value: string, annotation: string): ResolvedDeployFramework {
+function frameworkFromUserFacingValue(
+ value: string,
+ annotation: string,
+): ResolvedDeployFramework {
switch (value.trim().toLowerCase()) {
case "next":
case "next.js":
@@ -2863,7 +3504,10 @@ function frameworkFromUserFacingValue(value: string, annotation: string): Resolv
}
}
-function frameworkNotDetectedError(cwd: string | undefined, requestedFramework?: string): CliError {
+function frameworkNotDetectedError(
+ cwd: string | undefined,
+ requestedFramework?: string,
+): CliError {
const supported = "Next.js, Hono, TanStack Start, Bun";
const directory = cwd ? ` in ${formatDeployDirectory(cwd)}` : "";
@@ -2900,8 +3544,12 @@ async function maybeRenderDeploySetupBlock(
}
const directory = formatDeployDirectory(context.runtime.cwd);
- const prefix = details.includeDirectory ? `Deploying ${directory} to` : "Deploying to";
- context.output.stderr.write(`${prefix} ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
+ const prefix = details.includeDirectory
+ ? `Deploying ${directory} to`
+ : "Deploying to";
+ context.output.stderr.write(
+ `${prefix} ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`,
+ );
}
function maybeRenderDeployBuildSettings(
@@ -2913,13 +3561,14 @@ function maybeRenderDeployBuildSettings(
}
const settings = resolution.settings;
- const title = resolution.status === "created"
- ? `Created ${resolution.relativeConfigPath}`
- : `Using ${resolution.relativeConfigPath}`;
+ const title =
+ resolution.status === "created"
+ ? `Created ${resolution.relativeConfigPath}`
+ : `Using ${resolution.relativeConfigPath}`;
context.output.stderr.write(
- `${title}\n`
- + `${renderDeployOutputRows(context.ui, [
+ `${title}\n` +
+ `${renderDeployOutputRows(context.ui, [
{
label: "Build Command",
value: settings.buildCommand ?? "none",
@@ -2945,8 +3594,8 @@ function maybeRenderProjectLinked(
}
context.output.stderr.write(
- `${context.ui.success("✔")} Linked "${directory}" to Project "${projectName}"\n`
- + `Saved ${localPinPath}\n\n`,
+ `${context.ui.success("✔")} Linked "${directory}" to Project "${projectName}"\n` +
+ `Saved ${localPinPath}\n\n`,
);
}
@@ -2960,14 +3609,17 @@ async function maybeCustomizeDeploySettings(
explicitEntrypoint: boolean;
explicitHttpPort: boolean;
},
-): Promise<{ framework: ResolvedDeployFramework; runtime: ResolvedDeployRuntime }> {
+): Promise<{
+ framework: ResolvedDeployFramework;
+ runtime: ResolvedDeployRuntime;
+}> {
if (
- !options.firstDeploy
- || context.flags.yes
- || options.explicitFramework
- || options.explicitEntrypoint
- || options.explicitHttpPort
- || !canPrompt(context)
+ !options.firstDeploy ||
+ context.flags.yes ||
+ options.explicitFramework ||
+ options.explicitEntrypoint ||
+ options.explicitHttpPort ||
+ !canPrompt(context)
) {
return {
framework: options.framework,
@@ -3012,24 +3664,41 @@ async function maybeCustomizeDeploySettings(
validate: validateDeployHttpPortText,
});
const runtime = {
- port: requestedPort.trim() ? parseDeployHttpPort(requestedPort) : options.runtime.port,
+ port: requestedPort.trim()
+ ? parseDeployHttpPort(requestedPort)
+ : options.runtime.port,
annotation: "set by you",
};
const changedRows = [
framework.key !== options.framework.key
- ? { label: "Framework", value: framework.displayName, annotation: framework.annotation }
+ ? {
+ label: "Framework",
+ value: framework.displayName,
+ annotation: framework.annotation,
+ }
: null,
runtime.port !== options.runtime.port
- ? { label: "Runtime", value: `HTTP ${runtime.port}`, annotation: runtime.annotation }
+ ? {
+ label: "Runtime",
+ value: `HTTP ${runtime.port}`,
+ annotation: runtime.annotation,
+ }
: null,
- ].filter((row): row is { label: string; value: string; annotation: string } => Boolean(row));
+ ].filter((row): row is { label: string; value: string; annotation: string } =>
+ Boolean(row),
+ );
if (changedRows.length > 0 && !context.flags.quiet && !context.flags.json) {
- context.output.stderr.write(`${renderDeployOutputRows(context.ui, changedRows.map((row) => ({
- label: row.label,
- value: row.value,
- origin: row.annotation,
- }))).join("\n")}\n\n`);
+ context.output.stderr.write(
+ `${renderDeployOutputRows(
+ context.ui,
+ changedRows.map((row) => ({
+ label: row.label,
+ value: row.value,
+ origin: row.annotation,
+ })),
+ ).join("\n")}\n\n`,
+ );
}
return {
@@ -3050,8 +3719,8 @@ function maybeRenderDeploySettingsPreview(
}
context.output.stderr.write(
- `Detected ${options.framework.displayName}\n`
- + `${renderDeploySettingsPreview(context.ui, [
+ `Detected ${options.framework.displayName}\n` +
+ `${renderDeploySettingsPreview(context.ui, [
{ key: "framework", value: options.framework.displayName },
{ key: "runtime", value: `HTTP ${options.runtime.port}` },
]).join("\n")}\n\n`,
@@ -3071,7 +3740,9 @@ function frameworkDisplayName(framework: DeployFramework): string {
}
}
-function validateDeployHttpPortText(value: string | undefined): string | undefined {
+function validateDeployHttpPortText(
+ value: string | undefined,
+): string | undefined {
if (!value?.trim()) {
return undefined;
}
@@ -3089,17 +3760,24 @@ function formatDeployDirectory(cwd: string): string {
return basename ? `./${basename}` : ".";
}
-async function readCurrentWorkspaceId(context: CommandContext): Promise {
+async function readCurrentWorkspaceId(
+ context: CommandContext,
+): Promise {
const state = await context.stateStore.read();
if (state.auth?.workspaceId) {
return state.auth.workspaceId;
}
- const authState = await readAuthState(context.runtime.env, context.runtime.signal);
+ const authState = await readAuthState(
+ context.runtime.env,
+ context.runtime.signal,
+ );
return authState.workspace?.id ?? null;
}
-function normalizeBuildType(requestedBuildType: string | undefined): PreviewBuildType {
+function normalizeBuildType(
+ requestedBuildType: string | undefined,
+): PreviewBuildType {
if (!requestedBuildType) {
return "auto";
}
@@ -3170,7 +3848,11 @@ async function requireLocalBuildType(
// Local dev server support is intentionally narrower than deploy build support.
// Nuxt, Astro, and TanStack Start can deploy via SDK strategies, but app run
// only starts the local dev servers currently documented for the preview.
- const resolvedBuildType = await resolveLocalBuildType(context.runtime.cwd, buildType, context.runtime.signal);
+ const resolvedBuildType = await resolveLocalBuildType(
+ context.runtime.cwd,
+ buildType,
+ context.runtime.signal,
+ );
if (resolvedBuildType) {
return resolvedBuildType;
}
@@ -3206,7 +3888,9 @@ function parseLocalPort(requestedPort: string | undefined): number {
return port;
}
-function parseDeployPortMapping(requestedPort: string | undefined): PortMapping | undefined {
+function parseDeployPortMapping(
+ requestedPort: string | undefined,
+): PortMapping | undefined {
if (!requestedPort) {
return undefined;
}
@@ -3243,7 +3927,11 @@ function ensurePreviewAppMode(context: CommandContext) {
);
}
-function deployFailedError(summary: string, error: unknown, nextSteps: string[]): CliError {
+function deployFailedError(
+ summary: string,
+ error: unknown,
+ nextSteps: string[],
+): CliError {
return new CliError({
code: "DEPLOY_FAILED",
domain: "app",
@@ -3256,66 +3944,35 @@ function deployFailedError(summary: string, error: unknown, nextSteps: string[])
});
}
-function appDeployFailedError(error: unknown, progress: PreviewDeployProgressState): CliError {
+function appDeployFailedError(
+ error: unknown,
+ progress: PreviewDeployProgressState,
+): CliError {
const why = error instanceof Error ? error.message : String(error);
const debug = formatDebugDetails(error);
if (progress.buildStarted && !progress.buildCompleted) {
- const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
- const fix = standaloneOutputFailure
- ? "Add output: \"standalone\" to next.config.*, then rerun deploy."
- : "Inspect the build output above, fix the error, and redeploy.";
- const nextSteps = standaloneOutputFailure
- ? ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"]
- : [];
- const nextActions = standaloneOutputFailure
- ? [
- {
- kind: "edit-file" as const,
- journey: "deploy-app" as const,
- label: "Add Next.js standalone output",
- reason: "Prisma Compute needs Next.js standalone output to build a deployable server artifact.",
- },
- {
- kind: "run-command" as const,
- journey: "deploy-app" as const,
- label: "Rerun deploy",
- command: "prisma-cli app deploy",
- },
- ]
- : [];
-
- return new CliError({
- code: "BUILD_FAILED",
- domain: "app",
- summary: "Build failed locally.",
- why,
- fix,
- debug,
- meta: { phase: "build" },
- humanLines: [
- "Build failed locally.",
- "",
- `✗ Built ${why}`,
- "",
- `Fix: ${fix}`,
- ],
- exitCode: 1,
- nextSteps,
- nextActions,
- });
+ return appBuildFailedError(why, debug);
}
if (!progress.buildStarted) {
- return deployFailedError("App deploy failed", error, ["prisma-cli app deploy"]);
+ return deployFailedError("App deploy failed", error, [
+ "prisma-cli app deploy",
+ ]);
}
const phaseHeadline = progress.containerLive
? "The deployment started, but the app is not ready yet."
: "Deploy failed after the build completed.";
const recoveryLines = progress.versionId
- ? ["See what happened", `prisma-cli app logs --deployment ${progress.versionId}`]
- : ["Fix", "Retry the command, or rerun with --trace for more detailed diagnostics."];
+ ? [
+ "See what happened",
+ `prisma-cli app logs --deployment ${progress.versionId}`,
+ ]
+ : [
+ "Fix",
+ "Retry the command, or rerun with --trace for more detailed diagnostics.",
+ ];
const urlLines = progress.deploymentUrl
? ["", "URL", progress.deploymentUrl]
: [];
@@ -3362,6 +4019,58 @@ function appDeployFailedError(error: unknown, progress: PreviewDeployProgressSta
});
}
+function appBuildFailedError(
+ why: string,
+ debug: string | null | undefined,
+): CliError {
+ const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
+ const fix = standaloneOutputFailure
+ ? 'Add output: "standalone" to next.config.*, then rerun deploy.'
+ : "Inspect the build output above, fix the error, and redeploy.";
+ const nextSteps = standaloneOutputFailure
+ ? [
+ 'Add output: "standalone" to next.config.*, then rerun prisma-cli app deploy',
+ ]
+ : [];
+ const nextActions = standaloneOutputFailure
+ ? [
+ {
+ kind: "edit-file" as const,
+ journey: "deploy-app" as const,
+ label: "Add Next.js standalone output",
+ reason:
+ "Prisma Compute needs Next.js standalone output to build a deployable server artifact.",
+ },
+ {
+ kind: "run-command" as const,
+ journey: "deploy-app" as const,
+ label: "Rerun deploy",
+ command: "prisma-cli app deploy",
+ },
+ ]
+ : [];
+
+ return new CliError({
+ code: "BUILD_FAILED",
+ domain: "app",
+ summary: "Build failed locally.",
+ why,
+ fix,
+ debug,
+ meta: { phase: "build" },
+ humanLines: [
+ "Build failed locally.",
+ "",
+ `✗ Built ${why}`,
+ "",
+ `Fix: ${fix}`,
+ ],
+ exitCode: 1,
+ nextSteps,
+ nextActions,
+ });
+}
+
function localResolutionPinStaleError(): CliError {
return new CliError({
code: "LOCAL_STATE_STALE",
@@ -3373,11 +4082,17 @@ function localResolutionPinStaleError(): CliError {
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
},
exitCode: 1,
- nextSteps: ["prisma-cli project list", "prisma-cli project link ", "prisma-cli app deploy --project "],
+ nextSteps: [
+ "prisma-cli project list",
+ "prisma-cli project link ",
+ "prisma-cli app deploy --project ",
+ ],
});
}
-function localPinReadErrorToDeployError(error: LocalResolutionPinReadError): CliError {
+function localPinReadErrorToDeployError(
+ error: LocalResolutionPinReadError,
+): CliError {
// Migration bridge: remove in Phase 20 when app controllers compose Result errors instead of throwing CliError.
return matchError(error, {
LocalResolutionPinInvalidJsonError: () => localResolutionPinStaleError(),
@@ -3391,7 +4106,10 @@ function localPinReadErrorToDeployError(error: LocalResolutionPinReadError): Cli
});
}
-function readDeployEnvOverride(context: CommandContext, name: string): string | undefined {
+function readDeployEnvOverride(
+ context: CommandContext,
+ name: string,
+): string | undefined {
const value = context.runtime.env[name]?.trim();
return value ? value : undefined;
}
@@ -3429,7 +4147,8 @@ function projectSetupRequiredError(
nextActions: buildProjectSetupNextActions({
commandName: "app deploy",
createCommand,
- reason: "This directory is not linked to a Prisma Project. Ask the user which Project to use before deploying; package and directory names are setup suggestions only.",
+ reason:
+ "This directory is not linked to a Prisma Project. Ask the user which Project to use before deploying; package and directory names are setup suggestions only.",
}),
});
}
@@ -3463,7 +4182,11 @@ function buildFailedError(summary: string, error: unknown): CliError {
});
}
-function runFailedError(summary: string, error: unknown, exitCode = 1): CliError {
+function runFailedError(
+ summary: string,
+ error: unknown,
+ exitCode = 1,
+): CliError {
return new CliError({
code: "RUN_FAILED",
domain: "app",
@@ -3480,7 +4203,10 @@ function formatFrameworkName(framework: AppRunResult["framework"]): string {
}
function isAutoBuildDetectionError(error: unknown): boolean {
- return error instanceof Error && error.message.startsWith("Entrypoint is required.");
+ return (
+ error instanceof Error &&
+ error.message.startsWith("Entrypoint is required.")
+ );
}
function formatBuildTypeName(buildType: PreviewBuildType): string {
@@ -3500,7 +4226,11 @@ function formatBuildTypeName(buildType: PreviewBuildType): string {
}
}
-function removeFailedError(summary: string, error: unknown, nextSteps: string[]): CliError {
+function removeFailedError(
+ summary: string,
+ error: unknown,
+ nextSteps: string[],
+): CliError {
return new CliError({
code: "REMOVE_FAILED",
domain: "app",
@@ -3530,18 +4260,27 @@ function isMissingProjectError(error: unknown): boolean {
return error instanceof Error && error.message === "Resource Not Found";
}
-function findAppByName(apps: PreviewAppRecord[], name: string): PreviewAppRecord | undefined {
+function findAppByName(
+ apps: PreviewAppRecord[],
+ name: string,
+): PreviewAppRecord | undefined {
return apps.find((app) => app.name === name);
}
-function findAppsByName(apps: PreviewAppRecord[], name: string): PreviewAppRecord[] {
+function findAppsByName(
+ apps: PreviewAppRecord[],
+ name: string,
+): PreviewAppRecord[] {
return apps.filter((app) => app.name === name);
}
function sortApps(apps: PreviewAppRecord[]): PreviewAppRecord[] {
return apps
.slice()
- .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
+ .sort(
+ (left, right) =>
+ left.name.localeCompare(right.name) || left.id.localeCompare(right.id),
+ );
}
function toOptionalEnvVars(
diff --git a/packages/cli/src/controllers/auth.ts b/packages/cli/src/controllers/auth.ts
index ccc60af..6d8aaa4 100644
--- a/packages/cli/src/controllers/auth.ts
+++ b/packages/cli/src/controllers/auth.ts
@@ -1,12 +1,16 @@
+import {
+ performLogin,
+ performLogout,
+ readAuthState,
+} from "../lib/auth/auth-ops";
import { authRequiredError, usageError } from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
-import { canPrompt, type CommandContext } from "../shell/runtime";
+import { type CommandContext, canPrompt } from "../shell/runtime";
import type { AuthStateResult } from "../types/auth";
import { createAuthUseCases } from "../use-cases/auth";
-import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways";
import type { LoginSelection, SelectPromptPort } from "../use-cases/contracts";
+import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways";
import { createSelectPromptPort } from "./select-prompt-port";
-import { performLogin, readAuthState, performLogout } from "../lib/auth/auth-ops";
export interface AuthLoginCommandOptions {
provider?: string;
@@ -15,7 +19,10 @@ export interface AuthLoginCommandOptions {
}
function isRealMode(context: CommandContext): boolean {
- return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
+ return (
+ !context.runtime.fixturePath &&
+ !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH
+ );
}
export async function runAuthLogin(
@@ -32,10 +39,15 @@ export async function runAuthLogin(
result = await loginWithSelectionFlow(context, useCases, options);
}
- return createAuthSuccess("auth.login", result, ["prisma-cli auth whoami", "prisma-cli project list"]);
+ return createAuthSuccess("auth.login", result, [
+ "prisma-cli auth whoami",
+ "prisma-cli project list",
+ ]);
}
-export async function runAuthLogout(context: CommandContext): Promise> {
+export async function runAuthLogout(
+ context: CommandContext,
+): Promise> {
let result: AuthStateResult;
if (isRealMode(context)) {
@@ -49,7 +61,9 @@ export async function runAuthLogout(context: CommandContext): Promise> {
+export async function runAuthWhoAmI(
+ context: CommandContext,
+): Promise> {
let result: AuthStateResult;
if (isRealMode(context)) {
@@ -59,12 +73,21 @@ export async function runAuthWhoAmI(context: CommandContext): Promise {
+export async function requireAuthenticatedAuthState(
+ context: CommandContext,
+): Promise {
if (isRealMode(context)) {
- const current = await readAuthState(context.runtime.env, context.runtime.signal);
+ const current = await readAuthState(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (current.authenticated) {
return current;
}
diff --git a/packages/cli/src/controllers/branch.ts b/packages/cli/src/controllers/branch.ts
index 72fcd3f..0e1a7d0 100644
--- a/packages/cli/src/controllers/branch.ts
+++ b/packages/cli/src/controllers/branch.ts
@@ -1,18 +1,32 @@
+// biome-ignore-all lint/performance/noAwaitInLoops: Branch pagination requests must run sequentially.
import type { ManagementApiClient } from "@prisma/management-api-sdk";
-
-import { authRequiredError, CliError, workspaceRequiredError } from "../shell/errors";
+import { requireComputeAuth } from "../lib/auth/guard";
+import {
+ projectResolutionErrorToCliError,
+ resolveProjectTarget,
+} from "../lib/project/resolution";
+import {
+ authRequiredError,
+ CliError,
+ workspaceRequiredError,
+} from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
import type { CommandContext } from "../shell/runtime";
-import type { BranchListResult, BranchRole, BranchSummary } from "../types/branch";
-import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways";
+import type {
+ BranchListResult,
+ BranchRole,
+ BranchSummary,
+} from "../types/branch";
import { createBranchUseCases } from "../use-cases/branch";
-import { requireComputeAuth } from "../lib/auth/guard";
-import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution";
+import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways";
import { requireAuthenticatedAuthState } from "./auth";
import { listRealWorkspaceProjects } from "./project";
function isRealMode(context: CommandContext): boolean {
- return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
+ return (
+ !context.runtime.fixturePath &&
+ !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH
+ );
}
interface RawBranchRecord {
@@ -21,7 +35,9 @@ interface RawBranchRecord {
role: BranchRole;
}
-export async function runBranchList(context: CommandContext): Promise> {
+export async function runBranchList(
+ context: CommandContext,
+): Promise> {
if (isRealMode(context)) {
return {
command: "branch.list",
@@ -42,9 +58,14 @@ export async function runBranchList(context: CommandContext): Promise {
+async function listRealBranches(
+ context: CommandContext,
+): Promise {
const authState = await requireAuthenticatedAuthState(context);
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError(["prisma-cli auth login"]);
}
@@ -57,14 +78,19 @@ async function listRealBranches(context: CommandContext): Promise listRealWorkspaceProjects(client, workspace, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
});
if (targetResult.isErr()) {
throw projectResolutionErrorToCliError(targetResult.error);
}
const target = targetResult.value;
- const branches = await listBranches(client, target.project.id, context.runtime.signal);
+ const branches = await listBranches(
+ client,
+ target.project.id,
+ context.runtime.signal,
+ );
return {
projectId: target.project.id,
@@ -110,15 +136,18 @@ async function listBranches(
query.cursor = cursor;
}
- const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", {
- params: { path: { projectId }, query },
- signal,
- });
+ const { data, error, response } = await client.GET(
+ "/v1/projects/{projectId}/branches",
+ {
+ params: { path: { projectId }, query },
+ signal,
+ },
+ );
if (error || !data) {
throw branchApiError("Failed to list branches", response, error);
}
- collected.push(...data.data as RawBranchRecord[]);
+ collected.push(...(data.data as RawBranchRecord[]));
if (!data.pagination.hasMore || !data.pagination.nextCursor) {
break;
@@ -156,8 +185,12 @@ function branchApiError(
code: error?.error?.code ?? "BRANCH_API_ERROR",
domain: "branch",
summary,
- why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
- fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
+ why:
+ error?.error?.message ??
+ `The Management API returned status ${status || "unknown"}.`,
+ fix:
+ error?.error?.hint ??
+ "Re-run with --trace for the underlying API response details.",
exitCode: 1,
nextSteps: [],
});
diff --git a/packages/cli/src/controllers/database.ts b/packages/cli/src/controllers/database.ts
index 4146b6b..eebde1c 100644
--- a/packages/cli/src/controllers/database.ts
+++ b/packages/cli/src/controllers/database.ts
@@ -3,12 +3,21 @@ import { randomBytes } from "node:crypto";
import { requireComputeAuth } from "../lib/auth/guard";
import {
createManagementDatabaseProvider,
+ type DatabaseProvider,
normalizeConnection,
normalizeDatabase,
- type DatabaseProvider,
} from "../lib/database/provider";
-import { projectResolutionErrorToCliError, resolveProjectTarget, type ResolvedProjectTarget } from "../lib/project/resolution";
-import { authRequiredError, CliError, usageError, workspaceRequiredError } from "../shell/errors";
+import {
+ projectResolutionErrorToCliError,
+ type ResolvedProjectTarget,
+ resolveProjectTarget,
+} from "../lib/project/resolution";
+import {
+ authRequiredError,
+ CliError,
+ usageError,
+ workspaceRequiredError,
+} from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
import type { CommandContext } from "../shell/runtime";
import type {
@@ -22,7 +31,10 @@ import type {
DatabaseSummary,
} from "../types/database";
import { requireAuthenticatedAuthState } from "./auth";
-import { listFixtureWorkspaceProjects, listRealWorkspaceProjects } from "./project";
+import {
+ listFixtureWorkspaceProjects,
+ listRealWorkspaceProjects,
+} from "./project";
interface DatabaseCommandFlags {
projectRef?: string;
@@ -51,19 +63,28 @@ interface ResolvedDatabaseContext {
}
function isRealMode(context: CommandContext): boolean {
- return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
+ return (
+ !context.runtime.fixturePath &&
+ !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH
+ );
}
export async function runDatabaseList(
context: CommandContext,
flags: DatabaseCommandFlags,
): Promise> {
- const { provider, target } = await requireDatabaseContext(context, flags, "database list");
- const databases = sortDatabases(await provider.listDatabases({
- projectId: target.project.id,
- branchName: flags.branchName,
- signal: context.runtime.signal,
- }));
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database list",
+ );
+ const databases = sortDatabases(
+ await provider.listDatabases({
+ projectId: target.project.id,
+ branchName: flags.branchName,
+ signal: context.runtime.signal,
+ }),
+ );
return {
command: "database.list",
@@ -84,9 +105,21 @@ export async function runDatabaseShow(
databaseRef: string,
flags: DatabaseCommandFlags,
): Promise> {
- const { provider, target } = await requireDatabaseContext(context, flags, "database show");
- const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
- const connections = await provider.listConnections(database.id, { signal: context.runtime.signal });
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database show",
+ );
+ const database = await resolveDatabase(
+ provider,
+ target,
+ databaseRef,
+ flags.branchName,
+ context.runtime.signal,
+ );
+ const connections = await provider.listConnections(database.id, {
+ signal: context.runtime.signal,
+ });
return {
command: "database.show",
@@ -118,7 +151,11 @@ export async function runDatabaseCreate(
);
}
- const { provider, target } = await requireDatabaseContext(context, flags, "database create");
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database create",
+ );
const created = await provider.createDatabase({
projectId: target.project.id,
name: databaseName,
@@ -147,8 +184,18 @@ export async function runDatabaseRemove(
databaseRef: string,
flags: DatabaseRemoveFlags,
): Promise> {
- const { provider, target } = await requireDatabaseContext(context, flags, "database remove");
- const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database remove",
+ );
+ const database = await resolveDatabase(
+ provider,
+ target,
+ databaseRef,
+ flags.branchName,
+ context.runtime.signal,
+ );
requireExactConfirmation({
resourceName: "database",
commandName: "database remove",
@@ -156,7 +203,9 @@ export async function runDatabaseRemove(
confirm: flags.confirm,
});
- await provider.removeDatabase(database.id, { signal: context.runtime.signal });
+ await provider.removeDatabase(database.id, {
+ signal: context.runtime.signal,
+ });
return {
command: "database.remove",
@@ -176,9 +225,21 @@ export async function runDatabaseConnectionList(
databaseRef: string,
flags: DatabaseCommandFlags,
): Promise> {
- const { provider, target } = await requireDatabaseContext(context, flags, "database connection list");
- const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
- const connections = await provider.listConnections(database.id, { signal: context.runtime.signal });
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database connection list",
+ );
+ const database = await resolveDatabase(
+ provider,
+ target,
+ databaseRef,
+ flags.branchName,
+ context.runtime.signal,
+ );
+ const connections = await provider.listConnections(database.id, {
+ signal: context.runtime.signal,
+ });
return {
command: "database.connection.list",
@@ -199,8 +260,18 @@ export async function runDatabaseConnectionCreate(
databaseRef: string,
flags: DatabaseConnectionCreateFlags,
): Promise> {
- const { provider, target } = await requireDatabaseContext(context, flags, "database connection create");
- const database = await resolveDatabase(provider, target, databaseRef, flags.branchName, context.runtime.signal);
+ const { provider, target } = await requireDatabaseContext(
+ context,
+ flags,
+ "database connection create",
+ );
+ const database = await resolveDatabase(
+ provider,
+ target,
+ databaseRef,
+ flags.branchName,
+ context.runtime.signal,
+ );
const created = await provider.createConnection({
databaseId: database.id,
name: flags.name?.trim() || defaultConnectionName(),
@@ -233,7 +304,9 @@ export async function runDatabaseConnectionRemove(
"Connection id required",
"Database connection removal needs a connection id.",
"Pass the connection id to remove.",
- ["prisma-cli database connection remove --confirm "],
+ [
+ "prisma-cli database connection remove --confirm ",
+ ],
"database",
);
}
@@ -246,7 +319,9 @@ export async function runDatabaseConnectionRemove(
});
const provider = await requireDatabaseProviderOnly(context);
- await provider.removeConnection(connectionId, { signal: context.runtime.signal });
+ await provider.removeConnection(connectionId, {
+ signal: context.runtime.signal,
+ });
return {
command: "database.connection.remove",
@@ -272,7 +347,10 @@ async function requireDatabaseContext(
}
if (isRealMode(context)) {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
@@ -281,7 +359,8 @@ async function requireDatabaseContext(
context,
workspace,
explicitProject: flags.projectRef,
- listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
commandName,
});
if (targetResult.isErr()) {
@@ -311,11 +390,16 @@ async function requireDatabaseContext(
};
}
-async function requireDatabaseProviderOnly(context: CommandContext): Promise {
+async function requireDatabaseProviderOnly(
+ context: CommandContext,
+): Promise {
await requireAuthenticatedAuthState(context);
if (isRealMode(context)) {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
@@ -325,7 +409,9 @@ async function requireDatabaseProviderOnly(context: CommandContext): Promise normalizeConnection(connection, connection.databaseId));
+ .map((connection) =>
+ normalizeConnection(connection, connection.databaseId),
+ );
},
async createConnection(options) {
@@ -369,7 +463,10 @@ function createFixtureDatabaseProvider(context: CommandContext): DatabaseProvide
throw databaseNotFoundError(options.databaseId);
}
return {
- connection: normalizeConnection(created.connection, created.connection.databaseId),
+ connection: normalizeConnection(
+ created.connection,
+ created.connection.databaseId,
+ ),
connectionString: created.connectionString,
};
},
@@ -406,7 +503,9 @@ async function resolveDatabase(
branchName,
signal,
});
- const matches = databases.filter((database) => database.id === ref || database.name === ref);
+ const matches = databases.filter(
+ (database) => database.id === ref || database.name === ref,
+ );
if (matches.length === 0) {
throw databaseNotFoundError(ref, target.project.name, branchName);
@@ -424,13 +523,18 @@ async function resolveDatabase(
return ensureProjectId(shown ?? selected, target.project.id);
}
-function ensureProjectId(database: DatabaseSummary, projectId: string): DatabaseSummary {
+function ensureProjectId(
+ database: DatabaseSummary,
+ projectId: string,
+): DatabaseSummary {
return database.projectId ? database : { ...database, projectId };
}
function sortDatabases(databases: DatabaseSummary[]): DatabaseSummary[] {
return databases.slice().sort((left, right) => {
- const branchOrder = (left.branchName ?? "").localeCompare(right.branchName ?? "");
+ const branchOrder = (left.branchName ?? "").localeCompare(
+ right.branchName ?? "",
+ );
if (branchOrder !== 0) {
return branchOrder;
}
@@ -457,7 +561,9 @@ function requireExactConfirmation(options: {
why: `Removing this ${options.resourceName} is destructive and requires the exact id.`,
fix: `Rerun with --confirm ${options.id}.`,
exitCode: 2,
- nextSteps: [`prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`],
+ nextSteps: [
+ `prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`,
+ ],
meta: {
expectedConfirm: options.id,
receivedConfirm: options.confirm ?? null,
@@ -466,12 +572,19 @@ function requireExactConfirmation(options: {
}
function defaultConnectionName(): string {
- const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 17);
+ const timestamp = new Date()
+ .toISOString()
+ .replace(/[-:.TZ]/g, "")
+ .slice(0, 17);
const suffix = randomBytes(2).toString("hex");
return `cli-${timestamp}-${suffix}`;
}
-function databaseNotFoundError(databaseRef: string, projectName?: string, branchName?: string): CliError {
+function databaseNotFoundError(
+ databaseRef: string,
+ projectName?: string,
+ branchName?: string,
+): CliError {
const scope = projectName
? ` in project "${projectName}"${branchName ? ` on branch "${branchName}"` : ""}`
: "";
@@ -486,7 +599,11 @@ function databaseNotFoundError(databaseRef: string, projectName?: string, branch
});
}
-function databaseAmbiguousError(databaseRef: string, matches: DatabaseSummary[], branchName: string | undefined): CliError {
+function databaseAmbiguousError(
+ databaseRef: string,
+ matches: DatabaseSummary[],
+ branchName: string | undefined,
+): CliError {
return new CliError({
code: "DATABASE_AMBIGUOUS",
domain: "database",
diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts
index 503ab28..79ca714 100644
--- a/packages/cli/src/controllers/project.ts
+++ b/packages/cli/src/controllers/project.ts
@@ -3,23 +3,27 @@ import { matchError } from "better-result";
import open from "open";
import {
+ type GitHubRepositoryReference,
parseGitHubRepositoryUrl,
readGitOriginRemote,
- type GitHubRepositoryReference,
} from "../adapters/git";
+import { createPreviewAppProvider } from "../lib/app/preview-provider";
import { requireComputeAuth } from "../lib/auth/guard";
+import { promptForProjectSetupChoice } from "../lib/project/interactive-setup";
+import {
+ type LocalResolutionPinReadError,
+ readLocalResolutionPin,
+} from "../lib/project/local-pin";
import {
buildProjectSetupNextActions,
inferTargetName,
inspectProjectBinding,
+ type ProjectCandidate,
projectResolutionErrorToCliError,
+ type ResolvedProjectTarget,
resolveProjectTarget,
sortProjects,
- type ProjectCandidate,
- type ResolvedProjectTarget,
} from "../lib/project/resolution";
-import { promptForProjectSetupChoice } from "../lib/project/interactive-setup";
-import { readLocalResolutionPin, type LocalResolutionPinReadError } from "../lib/project/local-pin";
import {
bindProjectToDirectory,
formatCommandArgument,
@@ -30,10 +34,15 @@ import {
resolveProjectForSetup,
toProjectSummary,
} from "../lib/project/setup";
-import { createPreviewAppProvider } from "../lib/app/preview-provider";
-import { authRequiredError, CliError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors";
+import {
+ authRequiredError,
+ CliError,
+ featureUnavailableError,
+ usageError,
+ workspaceRequiredError,
+} from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
-import { canPrompt, type CommandContext } from "../shell/runtime";
+import { type CommandContext, canPrompt } from "../shell/runtime";
import { renderSummaryLine } from "../shell/ui";
import type { AuthWorkspace } from "../types/auth";
import type {
@@ -60,7 +69,10 @@ const GITHUB_INSTALL_POLL_INTERVAL_MS = 2_000;
const GITHUB_INSTALL_POLL_TIMEOUT_MS = 120_000;
function isRealMode(context: CommandContext): boolean {
- return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
+ return (
+ !context.runtime.fixturePath &&
+ !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH
+ );
}
async function readProjectListLocalBinding(
@@ -76,14 +88,17 @@ async function readProjectListLocalBinding(
const pin = pinResult.value;
if (pin.kind === "present") {
- return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId)
+ return pin.pin.workspaceId === workspace.id &&
+ projects.some((project) => project.id === pin.pin.projectId)
? { status: "linked" }
: { status: "invalid" };
}
return { status: "not-linked" };
}
-function localPinReadErrorToInvalidLocalBinding(error: LocalResolutionPinReadError): ProjectListResult["localBinding"] {
+function localPinReadErrorToInvalidLocalBinding(
+ error: LocalResolutionPinReadError,
+): ProjectListResult["localBinding"] {
// Migration bridge: remove in Phase 20 when local-pin read errors are composed before controller output shaping.
return matchError(error, {
LocalResolutionPinInvalidJsonError: () => ({ status: "invalid" }),
@@ -97,7 +112,9 @@ function localPinReadErrorToInvalidLocalBinding(error: LocalResolutionPinReadErr
});
}
-export async function runProjectList(context: CommandContext): Promise> {
+export async function runProjectList(
+ context: CommandContext,
+): Promise> {
const authState = await requireAuthenticatedAuthState(context);
const workspace = authState.workspace;
if (!workspace) {
@@ -105,12 +122,26 @@ export async function runProjectList(context: CommandContext): Promise",
- reason: localBinding?.status === "invalid"
- ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands."
- : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory.",
+ reason:
+ localBinding?.status === "invalid"
+ ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands."
+ : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory.",
});
}
@@ -166,20 +207,26 @@ export async function runProjectShow(
const result = isRealMode(context)
? await resolveProjectShowInRealMode(context, workspace, explicitProject)
- : await resolveProjectShowInFixtureMode(context, workspace, explicitProject);
+ : await resolveProjectShowInFixtureMode(
+ context,
+ workspace,
+ explicitProject,
+ );
return {
command: "project.show",
result,
warnings: [],
nextSteps: [],
- nextActions: result.project === null
- ? buildProjectSetupNextActions({
- commandName: "project show",
- suggestedProjectName: result.suggestedProjectName,
- reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project.",
- })
- : [],
+ nextActions:
+ result.project === null
+ ? buildProjectSetupNextActions({
+ commandName: "project show",
+ suggestedProjectName: result.suggestedProjectName,
+ reason:
+ "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project.",
+ })
+ : [],
};
}
@@ -207,24 +254,39 @@ export async function runProjectCreate(
);
}
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
const provider = createPreviewAppProvider(client);
const name = projectName.trim();
- const created = await provider.createProject({ name, signal: context.runtime.signal }).catch((error) => {
- throw projectCreateFailedError(error, name, workspace, {
- nextSteps: ["prisma-cli project list", "prisma-cli project link "],
- permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
- fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link .",
+ const created = await provider
+ .createProject({ name, signal: context.runtime.signal })
+ .catch((error) => {
+ throw projectCreateFailedError(error, name, workspace, {
+ nextSteps: [
+ "prisma-cli project list",
+ "prisma-cli project link ",
+ ],
+ permissionFix:
+ "Grant the token permission to create Projects in this workspace, or link an existing Project.",
+ fallbackFix:
+ "Retry the command, or choose an existing Project with prisma-cli project link .",
+ });
});
- });
- const bindResult = await bindProjectToDirectory(context, workspace, {
- id: created.id,
- name: created.name,
- }, "created");
+ const bindResult = await bindProjectToDirectory(
+ context,
+ workspace,
+ {
+ id: created.id,
+ name: created.name,
+ },
+ "created",
+ );
if (bindResult.isErr()) {
throw projectDirectoryBindingErrorToCliError(bindResult.error);
}
@@ -251,20 +313,36 @@ export async function runProjectLink(
let provider: ReturnType | null = null;
let projects: ProjectCandidate[];
if (isRealMode(context)) {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
provider = createPreviewAppProvider(client);
- projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
+ projects = await listRealWorkspaceProjects(
+ client,
+ workspace,
+ context.runtime.signal,
+ );
} else {
projects = listFixtureWorkspaceProjects(context, workspace);
}
let result: ProjectSetupResult;
if (projectRef?.trim()) {
- const project = resolveProjectForSetup(projectRef.trim(), projects, workspace);
- result = await requireProjectDirectoryBinding(context, workspace, toProjectSummary(project), "linked");
+ const project = resolveProjectForSetup(
+ projectRef.trim(),
+ projects,
+ workspace,
+ );
+ result = await requireProjectDirectoryBinding(
+ context,
+ workspace,
+ toProjectSummary(project),
+ "linked",
+ );
} else if (canPrompt(context) && !context.flags.yes) {
result = await resolveInteractiveProjectLinkSetup(
context,
@@ -303,16 +381,29 @@ async function resolveInteractiveProjectLinkSetup(
"project",
);
}
- return createProjectForLinkSetup(provider, projectName, workspace, context.runtime.signal);
+ return createProjectForLinkSetup(
+ provider,
+ projectName,
+ workspace,
+ context.runtime.signal,
+ );
},
cancel: {
why: "Project link needs a Project before it can continue.",
fix: "Choose an existing Project or create a new one, then rerun project link.",
- nextSteps: ["prisma-cli project link ", "prisma-cli project create "],
+ nextSteps: [
+ "prisma-cli project link ",
+ "prisma-cli project create ",
+ ],
},
});
- return requireProjectDirectoryBinding(context, workspace, setup.project, setup.action);
+ return requireProjectDirectoryBinding(
+ context,
+ workspace,
+ setup.project,
+ setup.action,
+ );
}
async function requireProjectDirectoryBinding(
@@ -321,7 +412,12 @@ async function requireProjectDirectoryBinding(
project: ProjectSummary,
action: ProjectSetupResult["action"],
): Promise {
- const bindResult = await bindProjectToDirectory(context, workspace, project, action);
+ const bindResult = await bindProjectToDirectory(
+ context,
+ workspace,
+ project,
+ action,
+ );
if (bindResult.isErr()) {
throw projectDirectoryBindingErrorToCliError(bindResult.error);
}
@@ -335,17 +431,21 @@ async function createProjectForLinkSetup(
workspace: AuthWorkspace,
signal: AbortSignal,
): Promise {
- const created = await provider.createProject({ name: projectName, signal }).catch((error) => {
- throw projectCreateFailedError(error, projectName, workspace, {
- nextSteps: [
- "prisma-cli project list",
- "prisma-cli project link ",
- `prisma-cli project create ${formatCommandArgument(projectName)}`,
- ],
- permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
- fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link .",
+ const created = await provider
+ .createProject({ name: projectName, signal })
+ .catch((error) => {
+ throw projectCreateFailedError(error, projectName, workspace, {
+ nextSteps: [
+ "prisma-cli project list",
+ "prisma-cli project link ",
+ `prisma-cli project create ${formatCommandArgument(projectName)}`,
+ ],
+ permissionFix:
+ "Grant the token permission to create Projects in this workspace, or link an existing Project.",
+ fallbackFix:
+ "Retry the command, or choose an existing Project with prisma-cli project link .",
+ });
});
- });
return {
id: created.id,
@@ -358,7 +458,10 @@ async function projectLinkTargetRequiredError(
context: CommandContext,
projects: ProjectCandidate[],
): Promise {
- const suggestedName = await inferTargetName(context.runtime.cwd, context.runtime.signal);
+ const suggestedName = await inferTargetName(
+ context.runtime.cwd,
+ context.runtime.signal,
+ );
const createCommand = `prisma-cli project create ${formatCommandArgument(suggestedName.name)}`;
const recoveryCommands = [
"prisma-cli project link ",
@@ -382,7 +485,8 @@ async function projectLinkTargetRequiredError(
nextActions: buildProjectSetupNextActions({
suggestedProjectName: suggestedName.name,
createCommand,
- reason: "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections.",
+ reason:
+ "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections.",
}),
});
}
@@ -399,19 +503,36 @@ export async function runGitConnect(
}
if (isRealMode(context)) {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
- const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git connect");
+ const target = await resolveRequiredProjectInRealMode(
+ context,
+ workspace,
+ options.project,
+ "git connect",
+ );
const repository = await resolveRepositoryForConnect(context, gitUrl);
const api = client as unknown as SourceRepositoryApiClient;
- const existing = await readFirstSourceRepository(api, target.project.id, context.runtime.signal);
+ const existing = await readFirstSourceRepository(
+ api,
+ target.project.id,
+ context.runtime.signal,
+ );
if (existing) {
const existingConnection = toRepositoryConnection(existing);
- if (repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) {
+ if (
+ repositoryFullNamesMatch(
+ existingConnection.repository.fullName,
+ repository.fullName,
+ )
+ ) {
return {
command: "git.connect",
result: {
@@ -426,19 +547,31 @@ export async function runGitConnect(
throw repoAlreadyConnectedError(existingConnection.repository.fullName);
}
- const resolvedRepository = await resolveInstalledRepository(context, api, workspace.id, repository);
- const { data, error, response } = await api.POST("/v1/source-repositories", {
- body: {
- projectId: target.project.id,
- provider: "github",
- providerRepositoryId: resolvedRepository.repository.id,
- installationId: resolvedRepository.installation.id,
+ const resolvedRepository = await resolveInstalledRepository(
+ context,
+ api,
+ workspace.id,
+ repository,
+ );
+ const { data, error, response } = await api.POST(
+ "/v1/source-repositories",
+ {
+ body: {
+ projectId: target.project.id,
+ provider: "github",
+ providerRepositoryId: resolvedRepository.repository.id,
+ installationId: resolvedRepository.installation.id,
+ },
+ signal: context.runtime.signal,
},
- signal: context.runtime.signal,
- });
+ );
if (error || !data) {
- throw repoConnectionApiError("Failed to connect GitHub repository", response, error);
+ throw repoConnectionApiError(
+ "Failed to connect GitHub repository",
+ response,
+ error,
+ );
}
return {
@@ -452,12 +585,24 @@ export async function runGitConnect(
};
}
- const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git connect");
+ const target = await resolveRequiredProjectInFixtureMode(
+ context,
+ workspace,
+ options.project,
+ "git connect",
+ );
const repository = await resolveRepositoryForConnect(context, gitUrl);
- const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
+ const existingConnection = await context.stateStore.readRepositoryConnection(
+ target.project.id,
+ );
if (existingConnection) {
- if (repositoryFullNamesMatch(existingConnection.repository.fullName, repository.fullName)) {
+ if (
+ repositoryFullNamesMatch(
+ existingConnection.repository.fullName,
+ repository.fullName,
+ )
+ ) {
return {
command: "git.connect",
result: {
@@ -473,7 +618,10 @@ export async function runGitConnect(
}
const connection = createPendingRepositoryConnection(repository);
- await context.stateStore.setRepositoryConnection(target.project.id, connection);
+ await context.stateStore.setRepositoryConnection(
+ target.project.id,
+ connection,
+ );
return {
command: "git.connect",
@@ -497,30 +645,49 @@ export async function runGitDisconnect(
}
if (isRealMode(context)) {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
- const target = await resolveRequiredProjectInRealMode(context, workspace, options.project, "git disconnect");
+ const target = await resolveRequiredProjectInRealMode(
+ context,
+ workspace,
+ options.project,
+ "git disconnect",
+ );
const api = client as unknown as SourceRepositoryApiClient;
- const existing = await readFirstSourceRepository(api, target.project.id, context.runtime.signal);
+ const existing = await readFirstSourceRepository(
+ api,
+ target.project.id,
+ context.runtime.signal,
+ );
if (!existing) {
throw repoNotConnectedError();
}
- const { error, response } = await api.DELETE("/v1/source-repositories/{id}", {
- params: {
- path: {
- id: existing.id,
+ const { error, response } = await api.DELETE(
+ "/v1/source-repositories/{id}",
+ {
+ params: {
+ path: {
+ id: existing.id,
+ },
},
+ signal: context.runtime.signal,
},
- signal: context.runtime.signal,
- });
+ );
if (error) {
- throw repoConnectionApiError("Failed to disconnect GitHub repository", response, error);
+ throw repoConnectionApiError(
+ "Failed to disconnect GitHub repository",
+ response,
+ error,
+ );
}
return {
@@ -534,8 +701,15 @@ export async function runGitDisconnect(
};
}
- const target = await resolveRequiredProjectInFixtureMode(context, workspace, options.project, "git disconnect");
- const existingConnection = await context.stateStore.readRepositoryConnection(target.project.id);
+ const target = await resolveRequiredProjectInFixtureMode(
+ context,
+ workspace,
+ options.project,
+ "git disconnect",
+ );
+ const existingConnection = await context.stateStore.readRepositoryConnection(
+ target.project.id,
+ );
if (!existingConnection) {
throw repoNotConnectedError();
@@ -559,7 +733,10 @@ async function resolveProjectShowInRealMode(
workspace: AuthWorkspace,
explicitProject: string | undefined,
): Promise {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
@@ -568,7 +745,8 @@ async function resolveProjectShowInRealMode(
context,
workspace,
explicitProject,
- listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
commandName: "project show",
});
if (result.isErr()) {
@@ -583,7 +761,10 @@ async function resolveRequiredProjectInRealMode(
explicitProject: string | undefined,
commandName: string,
): Promise {
- const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
+ const client = await requireComputeAuth(
+ context.runtime.env,
+ context.runtime.signal,
+ );
if (!client) {
throw authRequiredError();
}
@@ -592,7 +773,8 @@ async function resolveRequiredProjectInRealMode(
context,
workspace,
explicitProject,
- listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
+ listProjects: () =>
+ listRealWorkspaceProjects(client, workspace, context.runtime.signal),
commandName,
});
if (result.isErr()) {
@@ -650,8 +832,13 @@ export async function listRealWorkspaceProjects(
.map((project) => ({
id: project.id,
name: project.name,
- ...("url" in project && typeof project.url === "string" ? { url: project.url } : {}),
- slug: "slug" in project && typeof project.slug === "string" ? project.slug : null,
+ ...("url" in project && typeof project.url === "string"
+ ? { url: project.url }
+ : {}),
+ slug:
+ "slug" in project && typeof project.slug === "string"
+ ? project.slug
+ : null,
workspace: {
id: project.workspace.id,
name: project.workspace.name,
@@ -749,6 +936,25 @@ interface SourceRepositoryApiClient {
signal?: AbortSignal;
},
): Promise>;
+ POST(
+ path: "/v1/scm-installations/install-intents",
+ options: {
+ body: {
+ provider: "github";
+ workspaceId: string;
+ };
+ signal?: AbortSignal;
+ },
+ ): Promise<
+ SourceRepositoryApiResult<{
+ data: {
+ type: "install-intent";
+ provider: "github";
+ workspaceId: string;
+ installUrl: string;
+ };
+ }>
+ >;
GET(
path: "/v1/source-repositories",
options: {
@@ -761,13 +967,15 @@ interface SourceRepositoryApiClient {
};
signal?: AbortSignal;
},
- ): Promise>;
+ ): Promise<
+ SourceRepositoryApiResult<{
+ data: SourceRepositoryResponse[];
+ pagination: {
+ nextCursor: string | null;
+ hasMore: boolean;
+ };
+ }>
+ >;
GET(
path: "/v1/scm-installations",
options: {
@@ -780,13 +988,15 @@ interface SourceRepositoryApiClient {
};
signal?: AbortSignal;
},
- ): Promise>;
+ ): Promise<
+ SourceRepositoryApiResult<{
+ data: ScmInstallationResponse[];
+ pagination: {
+ nextCursor: string | null;
+ hasMore: boolean;
+ };
+ }>
+ >;
GET(
path: "/v1/scm-installations/{installationId}/repositories",
options: {
@@ -801,30 +1011,15 @@ interface SourceRepositoryApiClient {
};
signal?: AbortSignal;
},
- ): Promise>;
- POST(
- path: "/v1/scm-installations/install-intents",
- options: {
- body: {
- provider: "github";
- workspaceId: string;
+ ): Promise<
+ SourceRepositoryApiResult<{
+ data: ScmRepositoryResponse[];
+ pagination: {
+ nextCursor: string | null;
+ hasMore: boolean;
};
- signal?: AbortSignal;
- },
- ): Promise>;
+ }>
+ >;
DELETE(
path: "/v1/source-repositories/{id}",
options: {
@@ -842,7 +1037,9 @@ async function resolveRepositoryForConnect(
context: CommandContext,
gitUrl: string | undefined,
): Promise {
- const remoteUrl = gitUrl ?? await readGitOriginRemote(context.runtime.cwd, context.runtime.signal);
+ const remoteUrl =
+ gitUrl ??
+ (await readGitOriginRemote(context.runtime.cwd, context.runtime.signal));
if (!remoteUrl) {
throw usageError(
@@ -868,13 +1065,26 @@ async function resolveInstalledRepository(
workspaceId: string,
repository: GitHubRepositoryReference,
): Promise {
- const installations = await listScmInstallations(api, workspaceId, context.runtime.signal);
- const lookup = await findRepositoryInInstallations(api, installations, repository, context.runtime.signal);
+ const installations = await listScmInstallations(
+ api,
+ workspaceId,
+ context.runtime.signal,
+ );
+ const lookup = await findRepositoryInInstallations(
+ api,
+ installations,
+ repository,
+ context.runtime.signal,
+ );
if (lookup.match) {
return lookup.match;
}
- const installUrl = await createGitHubInstallIntent(api, workspaceId, context.runtime.signal);
+ const installUrl = await createGitHubInstallIntent(
+ api,
+ workspaceId,
+ context.runtime.signal,
+ );
const canWait = canPrompt(context);
const opened = await openInstallUrlIfInteractive(context, installUrl);
@@ -888,7 +1098,12 @@ async function resolveInstalledRepository(
writeInstallWaitStatus(context, opened, installUrl);
- const result = await waitForInstalledRepository(context, api, workspaceId, repository);
+ const result = await waitForInstalledRepository(
+ context,
+ api,
+ workspaceId,
+ repository,
+ );
if (result.match) {
return result.match;
}
@@ -913,7 +1128,13 @@ async function findRepositoryInInstallations(
continue;
}
- const matchedRepository = await findRepositoryInInstallationIfAvailable(api, installation.id, repository, signal);
+ // biome-ignore lint/performance/noAwaitInLoops: Installation access is inspected in order so we can stop at the first matching repository.
+ const matchedRepository = await findRepositoryInInstallationIfAvailable(
+ api,
+ installation.id,
+ repository,
+ signal,
+ );
if (matchedRepository === "unavailable") {
continue;
}
@@ -941,7 +1162,10 @@ async function waitForInstalledRepository(
api: SourceRepositoryApiClient,
workspaceId: string,
repository: GitHubRepositoryReference,
-): Promise<{ match: InstalledRepositoryMatch | null; inspectableInstallationCount: number }> {
+): Promise<{
+ match: InstalledRepositoryMatch | null;
+ inspectableInstallationCount: number;
+}> {
const timeoutMs = readPositiveIntegerEnv(
context.runtime.env.PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS,
GITHUB_INSTALL_POLL_TIMEOUT_MS,
@@ -955,9 +1179,19 @@ async function waitForInstalledRepository(
while (Date.now() <= deadline) {
context.runtime.signal.throwIfAborted();
- const installations = await listScmInstallations(api, workspaceId, context.runtime.signal);
+ // biome-ignore lint/performance/noAwaitInLoops: Polling intentionally waits for each remote inspection before sleeping or retrying.
+ const installations = await listScmInstallations(
+ api,
+ workspaceId,
+ context.runtime.signal,
+ );
- const lookup = await findRepositoryInInstallations(api, installations, repository, context.runtime.signal);
+ const lookup = await findRepositoryInInstallations(
+ api,
+ installations,
+ repository,
+ context.runtime.signal,
+ );
inspectableInstallationCount = lookup.inspectableInstallationCount;
if (lookup.match) {
return { match: lookup.match, inspectableInstallationCount };
@@ -974,7 +1208,10 @@ async function waitForInstalledRepository(
return { match: null, inspectableInstallationCount };
}
-function readPositiveIntegerEnv(value: string | undefined, fallback: number): number {
+function readPositiveIntegerEnv(
+ value: string | undefined,
+ fallback: number,
+): number {
if (value === undefined) {
return fallback;
}
@@ -1034,6 +1271,7 @@ async function listScmInstallations(
const seenCursors = new Set();
do {
+ // biome-ignore lint/performance/noAwaitInLoops: Cursor pagination is sequential by API contract.
const { data, error, response } = await api.GET("/v1/scm-installations", {
params: {
query: {
@@ -1046,7 +1284,11 @@ async function listScmInstallations(
});
if (error || !data) {
- throw repoConnectionApiError("Failed to inspect GitHub App installations", response, error);
+ throw repoConnectionApiError(
+ "Failed to inspect GitHub App installations",
+ response,
+ error,
+ );
}
installations.push(...data.data);
@@ -1072,24 +1314,34 @@ async function findRepositoryInInstallation(
const seenCursors = new Set();
do {
- const { data, error, response } = await api.GET("/v1/scm-installations/{installationId}/repositories", {
- params: {
- path: {
- installationId,
- },
- query: {
- limit: 100,
- ...(cursor ? { cursor } : {}),
+ // biome-ignore lint/performance/noAwaitInLoops: Cursor pagination is sequential by API contract.
+ const { data, error, response } = await api.GET(
+ "/v1/scm-installations/{installationId}/repositories",
+ {
+ params: {
+ path: {
+ installationId,
+ },
+ query: {
+ limit: 100,
+ ...(cursor ? { cursor } : {}),
+ },
},
+ signal,
},
- signal,
- });
+ );
if (error || !data) {
- throw repoConnectionApiError("Failed to inspect GitHub repositories", response, error);
+ throw repoConnectionApiError(
+ "Failed to inspect GitHub repositories",
+ response,
+ error,
+ );
}
- const matchedRepository = data.data.find((candidate) => candidate.fullName.toLowerCase() === expectedFullName);
+ const matchedRepository = data.data.find(
+ (candidate) => candidate.fullName.toLowerCase() === expectedFullName,
+ );
if (matchedRepository) {
return matchedRepository;
}
@@ -1111,7 +1363,10 @@ function readNextPaginationCursor(
summary: string,
response: Response | undefined,
): string | undefined {
- const nextCursor = pagination.hasMore && pagination.nextCursor ? pagination.nextCursor : undefined;
+ const nextCursor =
+ pagination.hasMore && pagination.nextCursor
+ ? pagination.nextCursor
+ : undefined;
if (!nextCursor) {
return undefined;
}
@@ -1135,7 +1390,12 @@ async function findRepositoryInInstallationIfAvailable(
signal: AbortSignal,
): Promise {
try {
- return await findRepositoryInInstallation(api, installationId, repository, signal);
+ return await findRepositoryInInstallation(
+ api,
+ installationId,
+ repository,
+ signal,
+ );
} catch (error) {
if (signal.aborted) throw error;
if (isUnavailableScmInstallationError(error)) {
@@ -1159,16 +1419,23 @@ async function createGitHubInstallIntent(
workspaceId: string,
signal: AbortSignal,
): Promise {
- const { data, error, response } = await api.POST("/v1/scm-installations/install-intents", {
- body: {
- provider: "github",
- workspaceId,
+ const { data, error, response } = await api.POST(
+ "/v1/scm-installations/install-intents",
+ {
+ body: {
+ provider: "github",
+ workspaceId,
+ },
+ signal,
},
- signal,
- });
+ );
if (error || !data) {
- throw repoConnectionApiError("Failed to create GitHub App installation link", response, error);
+ throw repoConnectionApiError(
+ "Failed to create GitHub App installation link",
+ response,
+ error,
+ );
}
return data.data.installUrl;
@@ -1210,7 +1477,11 @@ async function readFirstSourceRepository(
});
if (error || !data) {
- throw repoConnectionApiError("Failed to inspect GitHub repository connection", response, error);
+ throw repoConnectionApiError(
+ "Failed to inspect GitHub repository connection",
+ response,
+ error,
+ );
}
return data.data[0] ?? null;
@@ -1241,7 +1512,9 @@ function createPendingRepositoryConnection(
};
}
-function toRepositoryConnection(record: SourceRepositoryResponse): GitRepositoryConnection {
+function toRepositoryConnection(
+ record: SourceRepositoryResponse,
+): GitRepositoryConnection {
const [owner = "", name = ""] = record.repoFullName.split("/");
return {
@@ -1314,10 +1587,7 @@ function repoInstallationRequiredError(
opened,
},
exitCode: 1,
- nextSteps: [
- installUrl,
- `prisma-cli git connect ${repository.url}`,
- ],
+ nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`],
});
}
@@ -1338,10 +1608,7 @@ function repoNotAccessibleError(
opened,
},
exitCode: 1,
- nextSteps: [
- installUrl,
- `prisma-cli git connect ${repository.url}`,
- ],
+ nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`],
});
}
@@ -1382,7 +1649,9 @@ function repoConnectionApiError(
code: "REPO_CONNECTION_FAILED",
domain: "project",
summary,
- why: apiMessage ?? `The Management API returned status ${status || "unknown"}.`,
+ why:
+ apiMessage ??
+ `The Management API returned status ${status || "unknown"}.`,
fix: apiHint ?? repoConnectionFixForStatus(status),
meta: {
status,
diff --git a/packages/cli/src/controllers/select-prompt-port.ts b/packages/cli/src/controllers/select-prompt-port.ts
index fe5dd91..c3eb89b 100644
--- a/packages/cli/src/controllers/select-prompt-port.ts
+++ b/packages/cli/src/controllers/select-prompt-port.ts
@@ -2,7 +2,9 @@ import { selectPrompt } from "../shell/prompt";
import type { CommandContext } from "../shell/runtime";
import type { SelectPromptPort } from "../use-cases/contracts";
-export function createSelectPromptPort(context: CommandContext): SelectPromptPort {
+export function createSelectPromptPort(
+ context: CommandContext,
+): SelectPromptPort {
return {
select: ({ message, choices }) =>
selectPrompt({
diff --git a/packages/cli/src/controllers/version.ts b/packages/cli/src/controllers/version.ts
index 69c898e..ae1ae92 100644
--- a/packages/cli/src/controllers/version.ts
+++ b/packages/cli/src/controllers/version.ts
@@ -3,7 +3,9 @@ import type { CommandSuccess } from "../shell/output";
import type { CommandContext } from "../shell/runtime";
import type { VersionResult } from "../types/version";
-export async function runVersion(context: CommandContext): Promise> {
+export async function runVersion(
+ context: CommandContext,
+): Promise> {
const result = buildVersionResult(context.runtime.env, context.runtime.argv);
return {
diff --git a/packages/cli/src/lib/app/branch-database-deploy.ts b/packages/cli/src/lib/app/branch-database-deploy.ts
index 6288762..a1f4394 100644
--- a/packages/cli/src/lib/app/branch-database-deploy.ts
+++ b/packages/cli/src/lib/app/branch-database-deploy.ts
@@ -1,26 +1,25 @@
import path from "node:path";
-
-import type { AppDeployResult } from "../../types/app";
import { CliError, usageError } from "../../shell/errors";
import { confirmPrompt } from "../../shell/prompt";
import type { CommandContext } from "../../shell/runtime";
import { canPrompt } from "../../shell/runtime";
import { renderSummaryLine } from "../../shell/ui";
+import type { AppDeployResult } from "../../types/app";
import { formatCommandArgument } from "../project/setup";
-import type {
- PreviewAppProvider,
- PreviewBranchDatabaseRecord,
- PreviewEnvironmentVariableRecord,
-} from "./preview-provider";
import {
- hasBranchDatabaseSignal,
- inspectBranchDatabaseSignal,
- runBranchDatabaseSchemaSetup,
type BranchDatabaseSchema,
type BranchDatabaseSchemaSetupResult,
type BranchDatabaseSignal,
+ hasBranchDatabaseSignal,
+ inspectBranchDatabaseSignal,
+ runBranchDatabaseSchemaSetup,
type UnsupportedBranchDatabaseSchema,
} from "./branch-database";
+import type {
+ PreviewAppProvider,
+ PreviewBranchDatabaseRecord,
+ PreviewEnvironmentVariableRecord,
+} from "./preview-provider";
export interface BranchDatabaseDeployBranch {
id: string;
@@ -54,6 +53,79 @@ export async function maybeSetupBranchDatabase(
return emptyBranchDatabaseSetupOutcome();
}
+ const preflight = branchDatabasePreflight(branch, options);
+ if (preflight) {
+ return preflight;
+ }
+
+ const envState = await inspectBranchDatabaseEnv(
+ provider,
+ projectId,
+ branch,
+ context.runtime.signal,
+ );
+ const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
+
+ const existingEnvOutcome = existingBranchDatabaseEnvOutcome(
+ context,
+ branch,
+ targetEnvVars,
+ envState,
+ options.db,
+ );
+ if (existingEnvOutcome) {
+ return existingEnvOutcome;
+ }
+
+ const localSignal = await inspectBranchDatabaseSignal(
+ context.runtime.cwd,
+ context.runtime.signal,
+ );
+ if (localSignal.unsupportedSchema) {
+ if (options.db === true) {
+ throw unsupportedBranchDatabaseSchemaError(
+ localSignal.unsupportedSchema,
+ branch,
+ context,
+ );
+ }
+
+ return emptyBranchDatabaseSetupOutcome();
+ }
+
+ const promptOutcome = await branchDatabasePromptOutcome(
+ context,
+ branch,
+ localSignal,
+ envState,
+ options.db,
+ );
+ if (promptOutcome) {
+ return promptOutcome;
+ }
+
+ if (options.db === true && !canPrompt(context) && !context.flags.yes) {
+ throw nonInteractiveDatabaseSetupRequiresYesError(branch);
+ }
+
+ return setupBranchDatabase(
+ context,
+ provider,
+ projectId,
+ branch,
+ localSignal,
+ envState,
+ );
+}
+
+function branchDatabasePreflight(
+ branch: BranchDatabaseDeployBranch,
+ options: {
+ db: boolean | undefined;
+ providedEnvVars: Record | undefined;
+ firstProductionDeploy: boolean;
+ },
+): BranchDatabaseSetupOutcome | null {
if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
if (options.db === true) {
throw usageError(
@@ -79,17 +151,31 @@ export async function maybeSetupBranchDatabase(
return emptyBranchDatabaseSetupOutcome();
}
- const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
- const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
+ return null;
+}
- if (hasExistingDatabaseEnvForTarget(branch, envState)) {
- const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
- if (warning) {
- emitBranchDatabaseWarning(context, warning);
- }
+function existingBranchDatabaseEnvOutcome(
+ context: CommandContext,
+ branch: BranchDatabaseDeployBranch,
+ targetEnvVars: string[],
+ envState: BranchDatabaseEnvState,
+ requested: boolean | undefined,
+): BranchDatabaseSetupOutcome | null {
+ if (!hasExistingDatabaseEnvForTarget(branch, envState)) {
+ return null;
+ }
- return {
- result: options.db === true
+ const warning =
+ requested === true
+ ? existingDatabaseEnvWarning(branch, targetEnvVars)
+ : null;
+ if (warning) {
+ emitBranchDatabaseWarning(context, warning);
+ }
+
+ return {
+ result:
+ requested === true
? {
status: "skipped",
reason: existingDatabaseEnvReason(branch),
@@ -97,50 +183,46 @@ export async function maybeSetupBranchDatabase(
schema: null,
}
: undefined,
- warnings: warning ? [warning] : [],
- };
- }
+ warnings: warning ? [warning] : [],
+ };
+}
- const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
- if (localSignal.unsupportedSchema) {
- if (options.db === true) {
- throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
- }
+async function branchDatabasePromptOutcome(
+ context: CommandContext,
+ branch: BranchDatabaseDeployBranch,
+ localSignal: BranchDatabaseSignal,
+ envState: BranchDatabaseEnvState,
+ requested: boolean | undefined,
+): Promise {
+ if (requested === true) {
+ return null;
+ }
+ const hasSignal =
+ hasBranchDatabaseSignal(localSignal) ||
+ Boolean(envState.inheritedPreviewDatabaseUrl);
+ if (!hasSignal) {
return emptyBranchDatabaseSetupOutcome();
}
- const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
- if (options.db !== true) {
- if (!hasSignal) {
- return emptyBranchDatabaseSetupOutcome();
- }
-
- if (!canPrompt(context) || context.flags.yes) {
- const warning = databasePromptSuppressedWarning(branch);
- emitBranchDatabaseWarning(context, warning);
- return {
- result: undefined,
- warnings: [warning],
- };
- }
-
- maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
- const shouldCreate = await confirmPrompt({
- input: context.runtime.stdin,
- output: context.output.stderr,
- message: databasePromptMessage(branch),
- initialValue: false,
- });
-
- if (!shouldCreate) {
- return emptyBranchDatabaseSetupOutcome();
- }
- } else if (!canPrompt(context) && !context.flags.yes) {
- throw nonInteractiveDatabaseSetupRequiresYesError(branch);
+ if (!canPrompt(context) || context.flags.yes) {
+ const warning = databasePromptSuppressedWarning(branch);
+ emitBranchDatabaseWarning(context, warning);
+ return {
+ result: undefined,
+ warnings: [warning],
+ };
}
- return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
+ maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
+ const shouldCreate = await confirmPrompt({
+ input: context.runtime.stdin,
+ output: context.output.stderr,
+ message: databasePromptMessage(branch),
+ initialValue: false,
+ });
+
+ return shouldCreate ? null : emptyBranchDatabaseSetupOutcome();
}
async function setupBranchDatabase(
@@ -152,37 +234,65 @@ async function setupBranchDatabase(
envState: BranchDatabaseEnvState,
): Promise {
emitBranchDatabaseProgress(context, "pending", "Creating database");
- const database = await provider.createBranchDatabase({
- projectId,
- branchId: branch.id,
- branchName: branch.name,
- signal: context.runtime.signal,
- }).catch((error) => {
- throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
- });
+ const database = await provider
+ .createBranchDatabase({
+ projectId,
+ branchId: branch.id,
+ branchName: branch.name,
+ signal: context.runtime.signal,
+ })
+ .catch((error) => {
+ throw branchDatabaseSetupFailedError(
+ "Failed to create database",
+ error,
+ branch,
+ );
+ });
emitBranchDatabaseProgress(context, "success", "Created database");
try {
let schemaSetup: BranchDatabaseSchemaSetupResult | null = null;
const warnings: string[] = [];
let skippedSchemaWarning: string | null = null;
- if (signal.schema) {
- emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`);
+ const schema = signal.schema;
+ if (schema) {
+ emitBranchDatabaseProgress(
+ context,
+ "pending",
+ `Applying database schema with ${formatSchemaSetupCommand(schema.command)}`,
+ );
schemaSetup = await runBranchDatabaseSchemaSetup({
context,
- schema: signal.schema,
+ schema,
databaseUrl: database.databaseUrl,
directUrl: database.directUrl,
}).catch((error) => {
- throw schemaSetupFailedError(error, signal.schema!, branch, context.runtime.cwd);
+ throw schemaSetupFailedError(
+ error,
+ schema,
+ branch,
+ context.runtime.cwd,
+ );
});
emitBranchDatabaseProgress(context, "success", "Applied database schema");
} else {
- skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
+ skippedSchemaWarning =
+ "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
}
- const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
- emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
+ const envVars = await upsertBranchDatabaseEnvVars(
+ context,
+ provider,
+ projectId,
+ branch,
+ database,
+ envState,
+ );
+ emitBranchDatabaseProgress(
+ context,
+ "success",
+ `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`,
+ );
if (skippedSchemaWarning) {
emitBranchDatabaseWarning(context, skippedSchemaWarning);
warnings.push(skippedSchemaWarning);
@@ -207,7 +317,13 @@ async function setupBranchDatabase(
warnings,
};
} catch (error) {
- throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
+ throw await cleanupCreatedBranchDatabaseAfterFailure(
+ context,
+ provider,
+ database,
+ branch,
+ error,
+ );
}
}
@@ -242,12 +358,18 @@ async function upsertBranchDatabaseEnvVars(
});
written.push("DIRECT_URL");
} else if (branch.kind === "preview" && envState.targetDirectUrl) {
- await provider.deleteEnvironmentVariable({
- envVarId: envState.targetDirectUrl.id,
- signal: context.runtime.signal,
- }).catch((error) => {
- throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
- });
+ await provider
+ .deleteEnvironmentVariable({
+ envVarId: envState.targetDirectUrl.id,
+ signal: context.runtime.signal,
+ })
+ .catch((error) => {
+ throw branchDatabaseSetupFailedError(
+ "Failed to remove stale DIRECT_URL",
+ error,
+ branch,
+ );
+ });
}
return written;
@@ -267,26 +389,38 @@ async function upsertBranchDatabaseEnvVar(
},
): Promise {
if (options.existing) {
- await provider.updateEnvironmentVariable({
- envVarId: options.existing.id,
- value: options.value,
- signal: context.runtime.signal,
- }).catch((error) => {
- throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
- });
+ await provider
+ .updateEnvironmentVariable({
+ envVarId: options.existing.id,
+ value: options.value,
+ signal: context.runtime.signal,
+ })
+ .catch((error) => {
+ throw branchDatabaseSetupFailedError(
+ `Failed to update ${options.key}`,
+ error,
+ options.branch,
+ );
+ });
return;
}
- await provider.createEnvironmentVariable({
- projectId: options.projectId,
- className: options.className,
- key: options.key,
- value: options.value,
- ...(options.branchId ? { branchId: options.branchId } : {}),
- signal: context.runtime.signal,
- }).catch((error) => {
- throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
- });
+ await provider
+ .createEnvironmentVariable({
+ projectId: options.projectId,
+ className: options.className,
+ key: options.key,
+ value: options.value,
+ ...(options.branchId ? { branchId: options.branchId } : {}),
+ signal: context.runtime.signal,
+ })
+ .catch((error) => {
+ throw branchDatabaseSetupFailedError(
+ `Failed to write ${options.key}`,
+ error,
+ options.branch,
+ );
+ });
}
async function inspectBranchDatabaseEnv(
@@ -313,11 +447,14 @@ async function inspectBranchDatabaseEnv(
const targetBranchId = branch.kind === "preview" ? branch.id : null;
return {
- targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
+ targetDatabaseUrl: findEnvVar(databaseUrlRows, {
+ branchId: targetBranchId,
+ }),
targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
- inheritedPreviewDatabaseUrl: branch.kind === "preview"
- ? findEnvVar(databaseUrlRows, { branchId: null })
- : null,
+ inheritedPreviewDatabaseUrl:
+ branch.kind === "preview"
+ ? findEnvVar(databaseUrlRows, { branchId: null })
+ : null,
};
}
@@ -328,11 +465,18 @@ function findEnvVar(
return rows.find((row) => row.branchId === options.branchId) ?? null;
}
-function hasProvidedDatabaseEnvVars(envVars: Record | undefined): boolean {
- return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
+function hasProvidedDatabaseEnvVars(
+ envVars: Record | undefined,
+): boolean {
+ return Boolean(
+ envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars),
+ );
}
-function envScopeForBranch(branch: BranchDatabaseDeployBranch): { className: "production" | "preview"; branchId?: string } {
+function envScopeForBranch(branch: BranchDatabaseDeployBranch): {
+ className: "production" | "preview";
+ branchId?: string;
+} {
return branch.kind === "production"
? { className: "production" }
: { className: "preview", branchId: branch.id };
@@ -342,9 +486,13 @@ function envScopeLabel(branch: BranchDatabaseDeployBranch): string {
return branch.kind === "production" ? "production" : "branch";
}
-function getTargetDatabaseEnvVarKeys(envState: BranchDatabaseEnvState): string[] {
+function getTargetDatabaseEnvVarKeys(
+ envState: BranchDatabaseEnvState,
+): string[] {
return [envState.targetDatabaseUrl, envState.targetDirectUrl]
- .filter((variable): variable is PreviewEnvironmentVariableRecord => Boolean(variable))
+ .filter((variable): variable is PreviewEnvironmentVariableRecord =>
+ Boolean(variable),
+ )
.map((variable) => variable.key)
.sort();
}
@@ -360,11 +508,18 @@ function hasExistingDatabaseEnvForTarget(
return Boolean(envState.targetDatabaseUrl);
}
-function existingDatabaseEnvReason(branch: BranchDatabaseDeployBranch): "branch-env-exists" | "production-env-exists" {
- return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
+function existingDatabaseEnvReason(
+ branch: BranchDatabaseDeployBranch,
+): "branch-env-exists" | "production-env-exists" {
+ return branch.kind === "production"
+ ? "production-env-exists"
+ : "branch-env-exists";
}
-function existingDatabaseEnvWarning(branch: BranchDatabaseDeployBranch, envVars: string[]): string {
+function existingDatabaseEnvWarning(
+ branch: BranchDatabaseDeployBranch,
+ envVars: string[],
+): string {
if (branch.kind === "production") {
return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
}
@@ -372,7 +527,9 @@ function existingDatabaseEnvWarning(branch: BranchDatabaseDeployBranch, envVars:
return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
}
-function databasePromptSuppressedWarning(branch: BranchDatabaseDeployBranch): string {
+function databasePromptSuppressedWarning(
+ branch: BranchDatabaseDeployBranch,
+): string {
if (branch.kind === "production") {
return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
}
@@ -409,13 +566,15 @@ function maybeRenderBranchDatabaseSignal(
].filter((row): row is string => Boolean(row));
context.output.stderr.write(
- `Database signal found for ${databaseTargetLabel(branch)}\n`
- + `${rows.join("\n")}\n\n`,
+ `Database signal found for ${databaseTargetLabel(branch)}\n` +
+ `${rows.join("\n")}\n\n`,
);
}
function databaseTargetLabel(branch: BranchDatabaseDeployBranch): string {
- return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
+ return branch.kind === "production"
+ ? `production branch "${branch.name}"`
+ : `branch "${branch.name}"`;
}
function emitBranchDatabaseProgress(
@@ -427,18 +586,24 @@ function emitBranchDatabaseProgress(
return;
}
- const line = status === "pending"
- ? `${context.ui.warning("◇")} ${message}...`
- : renderSummaryLine(context.ui, "success", message);
+ const line =
+ status === "pending"
+ ? `${context.ui.warning("◇")} ${message}...`
+ : renderSummaryLine(context.ui, "success", message);
context.output.stderr.write(`${line}\n`);
}
-function emitBranchDatabaseWarning(context: CommandContext, warning: string): void {
+function emitBranchDatabaseWarning(
+ context: CommandContext,
+ warning: string,
+): void {
if (context.flags.json || context.flags.quiet) {
return;
}
- context.output.stderr.write(`${renderSummaryLine(context.ui, "warning", warning)}\n`);
+ context.output.stderr.write(
+ `${renderSummaryLine(context.ui, "warning", warning)}\n`,
+ );
}
function emptyBranchDatabaseSetupOutcome(): BranchDatabaseSetupOutcome {
@@ -461,10 +626,13 @@ function productionDatabaseSetupAfterFirstDeployError(): CliError {
);
}
-function nonInteractiveDatabaseSetupRequiresYesError(branch: BranchDatabaseDeployBranch): CliError {
- const command = branch.kind === "production"
- ? "prisma-cli app deploy --prod --db --yes"
- : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`;
+function nonInteractiveDatabaseSetupRequiresYesError(
+ branch: BranchDatabaseDeployBranch,
+): CliError {
+ const command =
+ branch.kind === "production"
+ ? "prisma-cli app deploy --prod --db --yes"
+ : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`;
return usageError(
"Database setup requires --yes in non-interactive mode",
@@ -475,7 +643,9 @@ function nonInteractiveDatabaseSetupRequiresYesError(branch: BranchDatabaseDeplo
);
}
-function formatSchemaSetupCommand(command: BranchDatabaseSchemaSetupResult["command"]): string {
+function formatSchemaSetupCommand(
+ command: BranchDatabaseSchemaSetupResult["command"],
+): string {
switch (command) {
case "migrate-deploy":
return "prisma migrate deploy";
@@ -486,7 +656,11 @@ function formatSchemaSetupCommand(command: BranchDatabaseSchemaSetupResult["comm
}
}
-function branchDatabaseSetupFailedError(summary: string, error: unknown, branch: BranchDatabaseDeployBranch): CliError {
+function branchDatabaseSetupFailedError(
+ summary: string,
+ error: unknown,
+ branch: BranchDatabaseDeployBranch,
+): CliError {
return new CliError({
code: "BRANCH_DATABASE_SETUP_FAILED",
domain: "app",
@@ -512,19 +686,33 @@ async function cleanupCreatedBranchDatabaseAfterFailure(
branch: BranchDatabaseDeployBranch,
error: unknown,
): Promise {
- const setupError = error instanceof CliError
- ? error
- : branchDatabaseSetupFailedError("Database setup failed", error, branch);
-
- emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
+ const setupError =
+ error instanceof CliError
+ ? error
+ : branchDatabaseSetupFailedError("Database setup failed", error, branch);
+
+ emitBranchDatabaseProgress(
+ context,
+ "pending",
+ "Removing database after setup failed",
+ );
try {
await provider.deleteBranchDatabase({
databaseId: database.id,
signal: context.runtime.signal,
});
- emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
+ emitBranchDatabaseProgress(
+ context,
+ "success",
+ "Removed database after setup failed",
+ );
} catch (cleanupError) {
- return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
+ return branchDatabaseCleanupFailedError(
+ setupError,
+ cleanupError,
+ database,
+ branch,
+ );
}
return setupError;
@@ -536,7 +724,8 @@ function branchDatabaseCleanupFailedError(
database: PreviewBranchDatabaseRecord,
branch: BranchDatabaseDeployBranch,
): CliError {
- const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
+ const cleanupWhy =
+ cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
const setupWhy = setupError.why ?? "Database setup failed.";
return new CliError({
@@ -590,7 +779,9 @@ function unsupportedBranchDatabaseSchemaError(
branch: BranchDatabaseDeployBranch,
context: CommandContext,
): CliError {
- const sourcePath = path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema);
+ const sourcePath =
+ path.relative(context.runtime.cwd, schema.path) ||
+ defaultUnsupportedSchemaSourcePath(schema);
return usageError(
"Database setup is not available for this Prisma schema",
`${sourcePath} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`,
@@ -603,29 +794,43 @@ function unsupportedBranchDatabaseSchemaError(
);
}
-function formatAppDeployWithDbNextStep(branch: BranchDatabaseDeployBranch): string {
+function formatAppDeployWithDbNextStep(
+ branch: BranchDatabaseDeployBranch,
+): string {
return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
}
-function formatProjectEnvListNextStep(branch: BranchDatabaseDeployBranch): string {
+function formatProjectEnvListNextStep(
+ branch: BranchDatabaseDeployBranch,
+): string {
return branch.kind === "production"
? "prisma-cli project env list --role production"
: `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
}
-function formatProjectEnvAddNextStep(branch: BranchDatabaseDeployBranch): string {
+function formatProjectEnvAddNextStep(
+ branch: BranchDatabaseDeployBranch,
+): string {
return branch.kind === "production"
? "prisma-cli project env add DATABASE_URL= --role production"
: `prisma-cli project env add DATABASE_URL= --branch ${formatCommandArgument(branch.name)}`;
}
-function formatSchemaSetupNextSteps(schema: BranchDatabaseSchema, cwd: string): string[] {
- const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
+function formatSchemaSetupNextSteps(
+ schema: BranchDatabaseSchema,
+ cwd: string,
+): string[] {
+ const sourcePath =
+ path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
switch (schema.command) {
case "migrate-deploy":
- return [`npx --no-install prisma migrate deploy --schema ${formatCommandArgument(sourcePath)}`];
+ return [
+ `npx --no-install prisma migrate deploy --schema ${formatCommandArgument(sourcePath)}`,
+ ];
case "db-push":
- return [`npx --no-install prisma db push --schema ${formatCommandArgument(sourcePath)}`];
+ return [
+ `npx --no-install prisma db push --schema ${formatCommandArgument(sourcePath)}`,
+ ];
case "prisma-next-db-init":
return [
`npx --no-install prisma-next contract emit --config ${formatCommandArgument(sourcePath)}`,
@@ -635,14 +840,22 @@ function formatSchemaSetupNextSteps(schema: BranchDatabaseSchema, cwd: string):
}
function defaultSchemaSourcePath(schema: BranchDatabaseSchema): string {
- return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
+ return schema.kind === "prisma-next"
+ ? "prisma-next.config.ts"
+ : "schema.prisma";
}
-function defaultUnsupportedSchemaSourcePath(schema: UnsupportedBranchDatabaseSchema): string {
- return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
+function defaultUnsupportedSchemaSourcePath(
+ schema: UnsupportedBranchDatabaseSchema,
+): string {
+ return schema.kind === "prisma-next"
+ ? "prisma-next.config.ts"
+ : "schema.prisma";
}
-function formatUnsupportedSchemaTarget(target: UnsupportedBranchDatabaseSchema["target"]): string {
+function formatUnsupportedSchemaTarget(
+ target: UnsupportedBranchDatabaseSchema["target"],
+): string {
switch (target) {
case "cockroachdb":
return "CockroachDB";
@@ -665,12 +878,19 @@ function formatDebugDetails(error: unknown): string | null {
return typeof error === "string" ? error : null;
}
-function formatCombinedDebugDetails(setupError: CliError, cleanupError: unknown): string | null {
+function formatCombinedDebugDetails(
+ setupError: CliError,
+ cleanupError: unknown,
+): string | null {
const setupDebug = setupError.debug ?? setupError.stack ?? setupError.message;
const cleanupDebug = formatDebugDetails(cleanupError);
- return [
- setupDebug ? `Setup error:\n${setupDebug}` : null,
- cleanupDebug ? `Cleanup error:\n${cleanupDebug}` : null,
- ].filter((line): line is string => Boolean(line)).join("\n\n") || null;
+ return (
+ [
+ setupDebug ? `Setup error:\n${setupDebug}` : null,
+ cleanupDebug ? `Cleanup error:\n${cleanupDebug}` : null,
+ ]
+ .filter((line): line is string => Boolean(line))
+ .join("\n\n") || null
+ );
}
diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts
index 843af73..779433b 100644
--- a/packages/cli/src/lib/app/branch-database.ts
+++ b/packages/cli/src/lib/app/branch-database.ts
@@ -1,3 +1,6 @@
+// biome-ignore-all lint/performance/noAwaitInLoops: Schema setup and filesystem scans are intentionally sequential.
+// biome-ignore-all lint/performance/useTopLevelRegex: Existing schema inspection regexes are kept inline for readability.
+// biome-ignore-all lint/style/noNestedTernary: Existing schema selection expression is intentionally compact.
import { spawn } from "node:child_process";
import type { Dirent } from "node:fs";
import { access, readdir, readFile, stat } from "node:fs/promises";
@@ -5,7 +8,10 @@ import path from "node:path";
import type { CommandContext } from "../../shell/runtime";
-export type BranchDatabaseSchemaCommand = "migrate-deploy" | "db-push" | "prisma-next-db-init";
+export type BranchDatabaseSchemaCommand =
+ | "migrate-deploy"
+ | "db-push"
+ | "prisma-next-db-init";
export type BranchDatabaseSchemaSourceKind = "prisma-orm" | "prisma-next";
export type UnsupportedBranchDatabaseSchemaTarget =
@@ -88,11 +94,25 @@ export async function inspectBranchDatabaseSignal(
await scanDirectory(cwd, cwd, 0, state, signal);
const prismaNextConfigs = await Promise.all(
- state.prismaNextConfigCandidates.map((configPath) => classifyPrismaNextConfig(configPath, signal)),
+ state.prismaNextConfigCandidates.map((configPath) =>
+ classifyPrismaNextConfig(configPath, signal),
+ ),
+ );
+ const supportedPrismaNextConfig = selectPrismaNextConfig(
+ cwd,
+ prismaNextConfigs,
+ "supported",
+ );
+ const unsupportedPrismaNextConfig = selectPrismaNextConfig(
+ cwd,
+ prismaNextConfigs,
+ "unsupported",
+ );
+ const selectedPrismaOrmSchema = await selectPrismaOrmSchema(
+ cwd,
+ state.schemaCandidates,
+ signal,
);
- const supportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "supported");
- const unsupportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "unsupported");
- const selectedPrismaOrmSchema = await selectPrismaOrmSchema(cwd, state.schemaCandidates, signal);
const schema = supportedPrismaNextConfig
? {
@@ -133,9 +153,16 @@ export async function runBranchDatabaseSchemaSetup(options: {
databaseUrl: string;
directUrl: string | null;
}): Promise {
- const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
+ const schemaPath =
+ path.relative(options.context.runtime.cwd, options.schema.path) ||
+ defaultSchemaSourcePath(options.schema);
const prisma = await resolvePrismaInvocation(options.context.runtime.cwd);
- const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl, prisma);
+ const commands = buildSchemaSetupCommands(
+ options.schema,
+ schemaPath,
+ options.databaseUrl,
+ prisma,
+ );
for (const command of commands) {
await runPrismaCommand({
@@ -167,6 +194,14 @@ interface ClassifiedPrismaNextConfig {
target: "postgresql" | "unknown" | UnsupportedBranchDatabaseSchemaTarget;
}
+interface SupportedPrismaNextConfig extends ClassifiedPrismaNextConfig {
+ target: "postgresql" | "unknown";
+}
+
+interface UnsupportedPrismaNextConfig extends ClassifiedPrismaNextConfig {
+ target: UnsupportedBranchDatabaseSchemaTarget;
+}
+
interface PrismaOrmSchemaSelection {
schema: BranchDatabaseSchema | null;
unsupportedSchema: UnsupportedBranchDatabaseSchema | null;
@@ -185,15 +220,9 @@ async function scanDirectory(
return;
}
- let entries: Dirent[];
- try {
- entries = await readdir(directory, { withFileTypes: true });
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
- return;
- }
- throw error;
- }
+ const entries = await readDirectoryEntries(directory);
+ if (!entries) return;
+
entries.sort((left, right) => left.name.localeCompare(right.name));
for (const entry of entries) {
@@ -202,38 +231,82 @@ async function scanDirectory(
return;
}
- const entryPath = path.join(directory, entry.name);
- if (entry.isDirectory()) {
- if (!SKIPPED_DIRECTORIES.has(entry.name)) {
- await scanDirectory(cwd, entryPath, depth + 1, state, signal);
- }
- continue;
+ await scanDirectoryEntry(cwd, directory, entry, depth, state, signal);
+ }
+}
+
+async function readDirectoryEntries(
+ directory: string,
+): Promise {
+ try {
+ return await readdir(directory, { withFileTypes: true });
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ return null;
}
+ throw error;
+ }
+}
- if (!entry.isFile()) {
- continue;
+async function scanDirectoryEntry(
+ cwd: string,
+ directory: string,
+ entry: Dirent,
+ depth: number,
+ state: ScanState,
+ signal: AbortSignal,
+): Promise